/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2011 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.web.jetty8.security;

import java.security.Principal;
import java.security.acl.Group;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import javax.security.auth.Subject;
import javax.security.auth.login.AccountExpiredException;
import javax.security.auth.login.CredentialExpiredException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

import org.eclipse.jetty.security.DefaultIdentityService;
import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.ow2.jonas.lib.security.context.SecurityContext;
import org.ow2.jonas.lib.security.context.SecurityCurrent;
import org.ow2.jonas.security.auth.callback.NoInputCallbackHandler;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;



/**
 * Realm used by Jetty 8 service.
 * @author Florent Benoit
 */
public class JOnASLoginService extends AbstractLifeCycle implements LoginService {

    /**
     * Name used in the JAAS config file.
     */
    private static final String JAAS_CONFIG_NAME = "jetty";

    /**
     * Logger.
     */
    private Log logger = LogFactory.getLog(JOnASLoginService.class);

    /**
     * Name of this realm.
     */
    private String name;

    /**
     * Used for create identities.
     */
    private IdentityService identityService;

    /**
     * List of authenticated users.
     */
    private Map<String, UserIdentity> users = null;


    /**
     * Default Constructor.
     */
    public JOnASLoginService() {
        this("JOnAS Login Service");
    }

    /**
     * Default Constructor.
     * @param name of the login service
     */
    public JOnASLoginService(final String name) {
        this.users = new ConcurrentHashMap<String, UserIdentity>();
        setName(name);
    }


    /**
     * Create a default identity service if there is no service.
     * @throws Exception if it can't be started
     */
    @Override
    protected void doStart() throws Exception {
        if (identityService == null) {
            this.identityService = new DefaultIdentityService();
        }
        super.doStart();
    }

    /** Login a user.
     * @param username The user name
     * @param credentials The users credentials
     * @return A UserIdentity if the credentials matched, otherwise null
     */
    public UserIdentity login(final String username, final Object credentials) {

        // No authentication can be made with a null username
        if (username == null) {
            return null;
        }

        UserIdentity userIdentity = getUsers().get(username);
        // User previously authenticated --> remove from the cache
        if (userIdentity != null) {
            removeUser(username);
        }

        NoInputCallbackHandler noInputCH = null;
        LoginContext loginContext = null;
        noInputCH = new NoInputCallbackHandler(username, (String) credentials, null);

        //Establish a LoginContext to use for authentication
        try {
            loginContext = new LoginContext(JAAS_CONFIG_NAME, noInputCH);
        } catch (LoginException e) {
            logger.error("Cannot create a login context for the user ''{0}''", username, e);
            return null;
        }

        // Negotiate a login via this LoginContext
        Subject subject = null;
        try {
            loginContext.login();
            subject = loginContext.getSubject();
            if (subject == null) {
                logger.error("No Subject for user ''{0}''", username);
                return null;
            }
        } catch (AccountExpiredException e) {
            logger.error("Account expired for user ''{0}''", username, e);
            return null;
        } catch (CredentialExpiredException e) {
            logger.error("Credential Expired for user ''{0}''", username, e);
            return null;
        } catch (FailedLoginException e) {
            logger.error("Failed Login exception for user ''{0}''", username, e);
            return null;
        } catch (LoginException e) {
            logger.error("Login exception for user ''{0}''", username, e);
            return null;
        }

        // Retrieve first principal name found (without groups)
        Iterator<Principal> iterator = subject.getPrincipals(Principal.class).iterator();
        String userName = null;
        while (iterator.hasNext() && (userName == null)) {
            Principal principal = iterator.next();
            if (!(principal instanceof Group)) {
                userName = principal.getName();
            }
        }

        // No name --> error
        if (userName == null) {
            logger.error("No Username found in the subject");
            return null;
        }

        // Retrieve all roles of the user (Roles are members of the Group class)
        Set<Group> groups = subject.getPrincipals(Group.class);
        List<String> roles = new ArrayList<String>();

        for (Group group : groups) {
            Enumeration<? extends Principal> e = group.members();
            while (e.hasMoreElements()) {
                Principal p = e.nextElement();
                roles.add(p.getName());
            }
        }

        // Create a JettyPrincipal for Jetty
        JettyPrincipal principal = new JettyPrincipal(userName, roles);

        // Register the subject in the security context
        //SecurityContext ctx = new SecurityContext(subject);
        SecurityContext ctx = new SecurityContext(userName, roles);
        SecurityCurrent current = SecurityCurrent.getCurrent();
        current.setSecurityContext(ctx);

        UserIdentity identity = identityService.newUserIdentity(subject, principal, roles.toArray(new String[roles.size()]));

        // Add to cache
        addUser(username, identity);

        return identity;
    }

    /** Validate a user identity.
     * Validate that a UserIdentity previously created by a call
     * to {@link #login(String, Object)} is still valid.
     * @param user The user to validate
     * @return true if authentication has not been revoked for the user.
     */
    public boolean validate(final UserIdentity user) {
        return true;
    }

    /**
     * @return the users.
     */
    protected Map<String, UserIdentity> getUsers() {
        return users;
    }

    /**
     * Add a user to the current map.
     * @param username name of the user
     * @param userIdentity object
     */
    protected void addUser(final String username, final UserIdentity userIdentity) {
        users.put(username, userIdentity);
    }

    /**
     * Remove a specific user.
     * @param username user to remove
     */
    protected void removeUser(final String username) {
        users.remove(username);
    }

    /**
     * Logout a user.
     * @param user the given user
     */
    public void logout(final UserIdentity user) {
        // Set anonymous identity
        SecurityCurrent.getCurrent().setSecurityContext(new SecurityContext());
    }


    /**
     * Sets the name of the realm.
     * @param name The name to set.
     */
    public void setName(final String name) {
        this.name = name;
    }

    /* ------------------------------------------------------------ */
    /**
     * @return Get the name of the login service (aka Realm name)
     */
    public String getName() {
        return name;
    }


    /**
     * Get the IdentityService associated with this Login Service.
     * @return the IdentityService associated with this Login Service.
     */
    public IdentityService getIdentityService() {
        return identityService;
    }

    /**
     * Set the IdentityService associated with this Login Service.
     * @param identityService the IdentityService associated with this Login
     *        Service.
     */
    public void setIdentityService(final IdentityService identityService) {
        this.identityService = identityService;
    }


}
