/*
 *  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.harmony.javax.security.auth.login;

import java.io.IOException;
import java.security.AccessController;
import java.security.AccessControlContext;
import java.security.PrivilegedExceptionAction;
import java.security.PrivilegedActionException;

import java.security.Security;
import java.util.HashMap;
import java.util.Map;

import org.apache.harmony.javax.security.auth.Subject;
import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
import org.apache.harmony.javax.security.auth.callback.Callback;
import org.apache.harmony.javax.security.auth.callback.UnsupportedCallbackException;
import org.apache.harmony.javax.security.auth.spi.LoginModule;
import org.apache.harmony.javax.security.auth.AuthPermission;

import org.apache.harmony.javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;


public class LoginContext {

    private static final String DEFAULT_CALLBACK_HANDLER_PROPERTY = "auth.login.defaultCallbackHandler"; //$NON-NLS-1$

    /*
     * Integer constants which serve as a replacement for the corresponding
     * LoginModuleControlFlag.* constants. These integers are used later as
     * index in the arrays - see loginImpl() and logoutImpl() methods
     */
    private static final int OPTIONAL = 0;

    private static final int REQUIRED = 1;

    private static final int REQUISITE = 2;

    private static final int SUFFICIENT = 3;

    // Subject to be used for this LoginContext's operations
    private Subject subject;

    /*
     * Shows whether the subject was specified by user (true) or was created by
     * this LoginContext itself (false).
     */
    private boolean userProvidedSubject;

    // Shows whether we use installed or user-provided Configuration
    private boolean userProvidedConfig;

    // An user's AccessControlContext, used when user specifies 
    private AccessControlContext userContext;

    /*
     * Either a callback handler passed by the user or a wrapper for the user's
     * specified handler - see init() below.
     */
    private CallbackHandler callbackHandler;

    /*
     * An array which keeps the instantiated and init()-ialized login modules
     * and their states
     */
    private Module[] modules;

    // Stores a shared state
    private Map<String, ?> sharedState;

    // A context class loader used to load [mainly] LoginModules
    private ClassLoader contextClassLoader;

    // Shows overall status - whether this LoginContext was successfully logged 
    private boolean loggedIn;

    public LoginContext(String name) throws LoginException {
        super();
        init(name, null, null, null);
    }

    public LoginContext(String name, CallbackHandler cbHandler) throws LoginException {
        super();
        if (cbHandler == null) {
            throw new LoginException("auth.34"); //$NON-NLS-1$
        }
        init(name, null, cbHandler, null);
    }

    public LoginContext(String name, Subject subject) throws LoginException {
        super();
        if (subject == null) {
            throw new LoginException("auth.03"); //$NON-NLS-1$
        }
        init(name, subject, null, null);
    }

    public LoginContext(String name, Subject subject, CallbackHandler cbHandler)
            throws LoginException {
        super();
        if (subject == null) {
            throw new LoginException("auth.03"); //$NON-NLS-1$
        }
        if (cbHandler == null) {
            throw new LoginException("auth.34"); //$NON-NLS-1$
        }
        init(name, subject, cbHandler, null);
    }

    public LoginContext(String name, Subject subject, CallbackHandler cbHandler,
                        Configuration config) throws LoginException {
        super();
        init(name, subject, cbHandler, config);
    }

    // Does all the machinery needed for the initialization.
    private void init(String name, Subject subject, final CallbackHandler cbHandler,
                      Configuration config) throws LoginException {
        userProvidedSubject = (this.subject = subject) != null;

        //
        // Set config
        //
        if (name == null) {
            throw new LoginException("auth.00"); //$NON-NLS-1$
        }

        if (config == null) {
            config = Configuration.getAccessibleConfiguration();
        } else {
            userProvidedConfig = true;
        }

        SecurityManager sm = System.getSecurityManager();

        if (sm != null && !userProvidedConfig) {
            sm.checkPermission(new AuthPermission("createLoginContext." + name));//$NON-NLS-1$
        }

        AppConfigurationEntry[] entries = config.getAppConfigurationEntry(name);
        if (entries == null) {
            if (sm != null && !userProvidedConfig) {
                sm.checkPermission(new AuthPermission("createLoginContext.other")); //$NON-NLS-1$
            }
            entries = config.getAppConfigurationEntry("other"); //$NON-NLS-1$
            if (entries == null) {
                throw new LoginException("auth.35 " + name); //$NON-NLS-1$
            }
        }

        modules = new Module[entries.length];
        for (int i = 0; i < modules.length; i++) {
            modules[i] = new Module(entries[i]);
        }
        //
        // Set CallbackHandler and this.contextClassLoader
        //

        /*
         * as some of the operations to be executed (i.e. get*ClassLoader,
         * getProperty, class loading) are security-checked, then combine all of
         * them into a single doPrivileged() call.
         */
        try {
            AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
                public Void run() throws Exception {
                    // First, set the 'contextClassLoader'
                    contextClassLoader = Thread.currentThread().getContextClassLoader();
                    if (contextClassLoader == null) {
                        contextClassLoader = ClassLoader.getSystemClassLoader();
                    }
                    // then, checks whether the cbHandler is set
                    if (cbHandler == null) {
                        // well, let's try to find it
                        String klassName = Security
                                .getProperty(DEFAULT_CALLBACK_HANDLER_PROPERTY);
                        if (klassName == null || klassName.length() == 0) {
                            return null;
                        }
                        Class<?> klass = Class.forName(klassName, true, contextClassLoader);
                        callbackHandler = (CallbackHandler) klass.newInstance();
                    } else {
                        callbackHandler = cbHandler;
                    }
                    return null;
                }
            });
        } catch (PrivilegedActionException ex) {
            Throwable cause = ex.getCause();
            throw (LoginException) new LoginException("auth.36").initCause(cause);//$NON-NLS-1$
        }

        if (userProvidedConfig) {
            userContext = AccessController.getContext();
        } else if (callbackHandler != null) {
            userContext = AccessController.getContext();
            callbackHandler = new ContextedCallbackHandler(callbackHandler);
        }
    }

    public Subject getSubject() {
        if (userProvidedSubject || loggedIn) {
            return subject;
        }
        return null;
    }

    /**
     * Warning: calling the method more than once may result in undefined
     * behaviour if logout() method is not invoked before.
     */
    public void login() throws LoginException {
        PrivilegedExceptionAction<Void> action = new PrivilegedExceptionAction<Void>() {
            public Void run() throws LoginException {
                loginImpl();
                return null;
            }
        };
        try {
            if (userProvidedConfig) {
                AccessController.doPrivileged(action, userContext);
            } else {
                AccessController.doPrivileged(action);
            }
        } catch (PrivilegedActionException ex) {
            throw (LoginException) ex.getException();
        }
    }

    /**
     * The real implementation of login() method whose calls are wrapped into
     * appropriate doPrivileged calls in login().
     */
    private void loginImpl() throws LoginException {
        if (subject == null) {
            subject = new Subject();
        }

        if (sharedState == null) {
            sharedState = new HashMap<String, Object>();
        }

        // PHASE 1: Calling login()-s
        Throwable firstProblem = null;

        int[] logged = new int[4];
        int[] total = new int[4];

        for (Module module : modules) {
            try {
                // if a module fails during Class.forName(), then it breaks overall 
                // attempt - see catch() below
                module.create(subject, callbackHandler, sharedState);

                if (module.module.login()) {
                    ++total[module.getFlag()];
                    ++logged[module.getFlag()];
                    if (module.getFlag() == SUFFICIENT) {
                        break;
                    }
                }
            } catch (Throwable ex) {
                if (firstProblem == null) {
                    firstProblem = ex;
                }
                if (module.klass == null) {
                    /*
                     * an exception occurred during class lookup - overall
                     * attempt must fail a little trick: increase the REQUIRED's
                     * number - this will look like a failed REQUIRED module
                     * later, so overall attempt will fail
                     */
                    ++total[REQUIRED];
                    break;
                }
                ++total[module.getFlag()];
                // something happened after the class was loaded
                if (module.getFlag() == REQUISITE) {
                    // ... and no need to walk down anymore
                    break;
                }
            }
        }
        // end of PHASE1, 

        // Let's decide whether we have either overall success or a total failure
        boolean fail = true;

        /*
         * Note: 'failed[xxx]!=0' is not enough to check.
         * 
         * Use 'logged[xx] != total[xx]' instead. This is because some modules
         * might not be counted as 'failed' if an exception occurred during
         * preload()/Class.forName()-ing. But, such modules still get counted in
         * the total[].
         */

        // if any REQ* module failed - then it's failure
        if (logged[REQUIRED] != total[REQUIRED] || logged[REQUISITE] != total[REQUISITE]) {
            // fail = true;
        } else {
            if (total[REQUIRED] == 0 && total[REQUISITE] == 0) {
                // neither REQUIRED nor REQUISITE was configured.
                // must have at least one SUFFICIENT or OPTIONAL
                if (logged[OPTIONAL] != 0 || logged[SUFFICIENT] != 0) {
                    fail = false;
                }
                //else { fail = true; }
            } else {
                fail = false;
            }
        }

        int commited[] = new int[4];
        // clear it
        total[0] = total[1] = total[2] = total[3] = 0;
        if (!fail) {
            // PHASE 2: 

            for (Module module : modules) {
                if (module.klass != null) {
                    ++total[module.getFlag()];
                    try {
                        module.module.commit();
                        ++commited[module.getFlag()];
                    } catch (Throwable ex) {
                        if (firstProblem == null) {
                            firstProblem = ex;
                        }
                    }
                }
            }
        }

        // need to decide once again
        fail = true;
        if (commited[REQUIRED] != total[REQUIRED] || commited[REQUISITE] != total[REQUISITE]) {
            //fail = true;
        } else {
            if (total[REQUIRED] == 0 && total[REQUISITE] == 0) {
                /*
                 * neither REQUIRED nor REQUISITE was configured. must have at
                 * least one SUFFICIENT or OPTIONAL
                 */
                if (commited[OPTIONAL] != 0 || commited[SUFFICIENT] != 0) {
                    fail = false;
                } else {
                    //fail = true;
                }
            } else {
                fail = false;
            }
        }

        if (fail) {
            // either login() or commit() failed. aborting...

            for (Module module : modules) {
                try {
                    module.module.abort();
                } catch ( /*LoginException*/Throwable ex) {
                    if (firstProblem == null) {
                        firstProblem = ex;
                    }
                }
            }
            if (firstProblem instanceof PrivilegedActionException
                    && firstProblem.getCause() != null) {
                firstProblem = firstProblem.getCause();
            }
            if (firstProblem instanceof LoginException) {
                throw (LoginException) firstProblem;
            }
            throw (LoginException) new LoginException("auth.37").initCause(firstProblem); //$NON-NLS-1$
        }
        loggedIn = true;
    }

    public void logout() throws LoginException {
        PrivilegedExceptionAction<Void> action = new PrivilegedExceptionAction<Void>() {
            public Void run() throws LoginException {
                logoutImpl();
                return null;
            }
        };
        try {
            if (userProvidedConfig) {
                AccessController.doPrivileged(action, userContext);
            } else {
                AccessController.doPrivileged(action);
            }
        } catch (PrivilegedActionException ex) {
            throw (LoginException) ex.getException();
        }
    }

    /**
     * The real implementation of logout() method whose calls are wrapped into
     * appropriate doPrivileged calls in logout().
     */
    private void logoutImpl() throws LoginException {
        if (subject == null) {
            throw new LoginException("auth.38"); //$NON-NLS-1$
        }
        loggedIn = false;
        Throwable firstProblem = null;
        int total = 0;
        for (Module module : modules) {
            try {
                module.module.logout();
                ++total;
            } catch (Throwable ex) {
                if (firstProblem == null) {
                    firstProblem = ex;
                }
            }
        }
        if (firstProblem != null || total == 0) {
            if (firstProblem instanceof PrivilegedActionException
                    && firstProblem.getCause() != null) {
                firstProblem = firstProblem.getCause();
            }
            if (firstProblem instanceof LoginException) {
                throw (LoginException) firstProblem;
            }
            throw (LoginException) new LoginException("auth.37").initCause(firstProblem); //$NON-NLS-1$
        }
    }

    /**
     * <p>A class that servers as a wrapper for the CallbackHandler when we use
     * installed Configuration, but not a passed one. See API docs on the
     * LoginContext.</p>
     * <p/>
     * <p>Simply invokes the given handler with the given AccessControlContext.</p>
     */
    private class ContextedCallbackHandler implements CallbackHandler {
        private final CallbackHandler hiddenHandlerRef;

        ContextedCallbackHandler(CallbackHandler handler) {
            super();
            this.hiddenHandlerRef = handler;
        }

        public void handle(final Callback[] callbacks) throws IOException,
                UnsupportedCallbackException {
            try {
                AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
                    public Void run() throws IOException, UnsupportedCallbackException {
                        hiddenHandlerRef.handle(callbacks);
                        return null;
                    }
                }, userContext);
            } catch (PrivilegedActionException ex) {
                if (ex.getCause() instanceof UnsupportedCallbackException) {
                    throw (UnsupportedCallbackException) ex.getCause();
                }
                throw (IOException) ex.getCause();
            }
        }
    }

    /**
     * A private class that stores an instantiated LoginModule.
     */
    private final class Module {

        // An initial info about the module to be used
        AppConfigurationEntry entry;

        // A mapping of LoginModuleControlFlag onto a simple int constant
        int flag;

        // The LoginModule itself 
        LoginModule module;

        // A class of the module
        Class<?> klass;

        Module(AppConfigurationEntry entry) {
            this.entry = entry;
            LoginModuleControlFlag flg = entry.getControlFlag();
            if (flg == LoginModuleControlFlag.OPTIONAL) {
                flag = OPTIONAL;
            } else if (flg == LoginModuleControlFlag.REQUISITE) {
                flag = REQUISITE;
            } else if (flg == LoginModuleControlFlag.SUFFICIENT) {
                flag = SUFFICIENT;
            } else {
                flag = REQUIRED;
                //if(flg!=LoginModuleControlFlag.REQUIRED) throw new Error()
            }
        }

        int getFlag() {
            return flag;
        }

        /**
         * Loads class of the LoginModule, instantiates it and then calls
         * initialize().
         */
        void create(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState)
                throws LoginException {
            String klassName = entry.getLoginModuleName();
            if (klass == null) {
                try {
                    klass = Class.forName(klassName, false, contextClassLoader);
                } catch (ClassNotFoundException ex) {
                    throw (LoginException) new LoginException(
                            "auth.39 " + klassName).initCause(ex); //$NON-NLS-1$
                }
            }

            if (module == null) {
                try {
                    module = (LoginModule) klass.newInstance();
                } catch (IllegalAccessException ex) {
                    throw (LoginException) new LoginException(
                            "auth.3A " + klassName) //$NON-NLS-1$
                            .initCause(ex);
                } catch (InstantiationException ex) {
                    throw (LoginException) new LoginException(
                            "auth.3A" + klassName) //$NON-NLS-1$
                            .initCause(ex);
                }
                module.initialize(subject, callbackHandler, sharedState, entry.getOptions());
            }
        }
    }
}
