/**
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.apache.openejb.client;

import javax.security.auth.login.FailedLoginException;
import java.net.URI;
import java.net.URISyntaxException;
import java.rmi.RemoteException;

public class ClientSecurity {

    public static final String IDENTITY_RESOLVER_STRATEGY = "openejb.client.identityResolver";

    private static ServerMetaData server;
    private static IdentityResolver identityResolver;
    private static Object staticClientIdentity;
    private static final InheritableThreadLocal<Object> threadClientIdentity = new InheritableThreadLocal<Object>();

    static {
        // determine the server uri
        final String serverUri = System.getProperty("openejb.server.uri");

        if (serverUri != null) {
            // determine the server location
            try {
                final URI location = new URI(serverUri);
                server = new ServerMetaData(location);
            } catch (final Exception e) {
                if (!serverUri.contains("://")) {
                    try {
                        final URI location = new URI("oejb://" + serverUri);
                        server = new ServerMetaData(location);
                    } catch (final URISyntaxException ignored) {
                    }
                }
            }
        }
    }

    public static ServerMetaData getServer() {
        return server;
    }

    public static void setServer(final ServerMetaData server) {
        ClientSecurity.server = server;
    }

    /**
     * Login the spedified user using the specified password.  This is a global login for the
     * entire Java Virtural Machine.  If you would like to have a thread scoped login, use
     * ClientSecurity.login(username, password, true);
     * </p>
     * This is the equivalent of ClientSecurity.login(username, password, false);
     *
     * @param username the user to login
     * @param password the password for the user
     * @throws FailedLoginException if the username and password combination are not valid or
     *                              if there is a problem communiating with the server
     */
    public static void login(final String username, final String password) throws FailedLoginException {
        login(username, password, false);
    }

    /**
     * Login the spedified user using the specified password either globally for the
     * entire Java Virtural Machine or scoped to the thread.
     * </p>
     * When using thread scoped login, you should logout in a finally block.  This particularly
     * when using thread pools.  If a thread is returned to the pool with a login attached to the
     * thread the next user of that thread will inherit the thread scoped login.
     *
     * @param username     the user to login
     * @param password     the password for the user
     * @param threadScoped if true the login is scoped to the thread; otherwise the login is global
     *                     for the entire Java Virtural Machine
     * @throws FailedLoginException if the username and password combination are not valid or
     *                              if there is a problem communiating with the server
     */
    public static void login(final String username, final String password, final boolean threadScoped) throws FailedLoginException {
        final Object clientIdentity = directAuthentication(username, password, server);
        if (threadScoped) {
            threadClientIdentity.set(clientIdentity);
        } else {
            staticClientIdentity = clientIdentity;
        }
        identityResolver = new SimpleIdentityResolver();
    }

    /**
     * Clears the thread and global login data.
     */
    public static void logout() {
        threadClientIdentity.set(null);
        staticClientIdentity = null;
    }

    /**
     * This is a helper method for login modules. Directly authenticates with the server using the specified
     * username and password returning the identity token for the client.  This methods does not store the
     * identity token and the caller must arrange for the to be available to the OpenEJB proxies via an
     * IdentityResolver.
     *
     * @param username the username for authentication
     * @param password the password for authentication
     * @param server   ServerMetaData
     * @return the client identity token
     * @throws FailedLoginException if the username password combination is not valid
     */
    public static Object directAuthentication(final String username, final String password, final ServerMetaData server) throws FailedLoginException {
        return directAuthentication(null, username, password, server);
    }

    public static Object directAuthentication(final String securityRealm, final String username, final String password, final ServerMetaData server) throws FailedLoginException {
        // authenticate
        final AuthenticationRequest authReq = new AuthenticationRequest(securityRealm, username, password);
        final AuthenticationResponse authRes;
        try {
            authRes = (AuthenticationResponse) Client.request(authReq, new AuthenticationResponse(), server);
        } catch (final RemoteException e) {
            throw (FailedLoginException) new FailedLoginException("Unable to authenticate with server " + server).initCause(e);
        }

        // check the response
        if (authRes.getResponseCode() != ResponseCodes.AUTH_GRANTED) {
            throw (FailedLoginException) new FailedLoginException("This principal is not authenticated.").initCause(authRes.getDeniedCause());
        }

        // return the response object
        return authRes.getIdentity().getClientIdentity();
    }

    public static Object getIdentity() {
        return getIdentityResolver().getIdentity();
    }

    public static IdentityResolver getIdentityResolver() {
        if (identityResolver == null) {
            final String strategy = System.getProperty(IDENTITY_RESOLVER_STRATEGY);
            if (strategy == null) {
                identityResolver = new JaasIdentityResolver();
            } else {
                // find the strategy class
                final ResourceFinder finder = new ResourceFinder("META-INF/");
                final Class identityResolverClass;
                try {
                    identityResolverClass = finder.findClass(IdentityResolver.class.getName() + "/" + strategy);
                } catch (final Exception e) {
                    throw new IllegalArgumentException("Could not find client identity strategy '" + strategy + "'");
                }

                // verify the interface
                if (!IdentityResolver.class.isAssignableFrom(identityResolverClass)) {
                    throw new IllegalArgumentException("Client identity strategy '" + strategy + "' " +
                        "class '" + identityResolverClass.getName() + "' does not implement the " +
                        "interface '" + IdentityResolver.class.getSimpleName() + "'");
                }

                // create the class
                try {
                    identityResolver = (IdentityResolver) identityResolverClass.newInstance();
                } catch (final Exception e) {
                    throw new IllegalArgumentException("Unable to create client identity strategy '" + strategy + "' " +
                        "class '" + identityResolverClass.getName() + "'", e);
                }
            }

        }
        return identityResolver;
    }

    public static void setIdentityResolver(final IdentityResolver identityResolver) {
        ClientSecurity.identityResolver = identityResolver;
    }

    private ClientSecurity() {
    }

    public static class SimpleIdentityResolver implements IdentityResolver {

        @Override
        public Object getIdentity() {
            Object clientIdentity = threadClientIdentity.get();
            if (clientIdentity == null) {
                clientIdentity = staticClientIdentity;
            }
            return clientIdentity;
        }
    }
}
