/* Copyright 2004 Apache Software Foundation
 *
 * Licensed 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.jetspeed.security.impl;

import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.jetspeed.i18n.KeyedMessage;
import org.apache.jetspeed.security.AuthenticationProviderProxy;
import org.apache.jetspeed.security.Role;
import org.apache.jetspeed.security.RoleManager;
import org.apache.jetspeed.security.RolePrincipal;
import org.apache.jetspeed.security.SecurityException;
import org.apache.jetspeed.security.SecurityProvider;
import org.apache.jetspeed.security.spi.RoleSecurityHandler;
import org.apache.jetspeed.security.spi.SecurityMappingHandler;
import org.apache.jetspeed.util.ArgUtil;

/**
 * <p>
 * Implementation for managing roles.
 * </p>
 * <p>
 * Role hierarchy elements are being returned as a {@link Role}collection. The
 * backing implementation must appropriately map the role hierarchy to a
 * preferences sub-tree.
 * </p>
 * <p>
 * The convention {principal}.{subprincipal} has been chosen to name roles
 * hierachies in order to support declarative security. Implementation follow
 * the conventions enforced by the {@link Preferences}API.
 * </p>
 * 
 * @author <a href="mailto:dlestrat@apache.org">David Le Strat </a>
 * @author <a href="mailto:taylor@apache.org">David Sean Taylor </a>
 */
public class RoleManagerImpl implements RoleManager
{
    /** The logger. */
    private static final Log log = LogFactory.getLog(RoleManagerImpl.class);

    /** The authentication provider proxy. */
    private AuthenticationProviderProxy atnProviderProxy = null;
    
    /** The role security handler. */
    private RoleSecurityHandler roleSecurityHandler = null;

    /** The security mapping handler. */
    private SecurityMappingHandler securityMappingHandler = null;

    /**
     * @param securityProvider The security provider.
     */
    public RoleManagerImpl(SecurityProvider securityProvider)
    {
        this.atnProviderProxy = securityProvider.getAuthenticationProviderProxy();
        this.roleSecurityHandler = securityProvider.getRoleSecurityHandler();
        this.securityMappingHandler = securityProvider.getSecurityMappingHandler();
    }

    /**
     * @see org.apache.jetspeed.security.RoleManager#addRole(java.lang.String)
     */
    public void addRole(String roleFullPathName) throws SecurityException
    {
        ArgUtil.notNull(new Object[] { roleFullPathName }, new String[] { "roleFullPathName" },
                "addRole(java.lang.String)");

        // Check if role already exists.
        if (roleExists(roleFullPathName))
        {
            throw new SecurityException(SecurityException.ROLE_ALREADY_EXISTS.create(roleFullPathName));
        }

        RolePrincipal rolePrincipal = new RolePrincipalImpl(roleFullPathName);
        String fullPath = rolePrincipal.getFullPath();
        // Add the preferences.
        Preferences preferences = Preferences.userRoot().node(fullPath);
        if (log.isDebugEnabled())
        {
            log.debug("Added role preferences node: " + fullPath);
        }
        try
        {
            if ((null != preferences) && preferences.absolutePath().equals(fullPath))
            {
                // Add role principal.
                roleSecurityHandler.setRolePrincipal(rolePrincipal);
                if (log.isDebugEnabled())
                {
                    log.debug("Added role: " + fullPath);
                }
            }
        }
        catch (SecurityException se)
        {
            KeyedMessage msg = 
                SecurityException.UNEXPECTED.create("RoleManager.addRole",
                                                    "RoleSecurityHandler.setRolePrincipal("+rolePrincipal.getName()+")",
                                                    se.getMessage());
            log.error(msg, se);

            // Remove the preferences node.
            try
            {
                preferences.removeNode();
            }
            catch (BackingStoreException bse)
            {
                bse.printStackTrace();
            }
            throw new SecurityException(msg, se);
        }
    }

    /**
     * @see org.apache.jetspeed.security.RoleManager#removeRole(java.lang.String)
     */
    public void removeRole(String roleFullPathName) throws SecurityException
    {
        ArgUtil.notNull(new Object[] { roleFullPathName }, new String[] { "roleFullPathName" },
                "removeRole(java.lang.String)");

        // Resolve the role hierarchy.
        Preferences prefs = Preferences.userRoot().node(
                RolePrincipalImpl.getFullPathFromPrincipalName(roleFullPathName));
        String[] roles = securityMappingHandler.getRoleHierarchyResolver().resolveChildren(prefs);
        for (int i = 0; i < roles.length; i++)
        {
            try
            {
                roleSecurityHandler.removeRolePrincipal(new RolePrincipalImpl(RolePrincipalImpl
                        .getPrincipalNameFromFullPath((String) roles[i])));
            }
            catch (Exception e)
            {
                KeyedMessage msg = 
                    SecurityException.UNEXPECTED.create("RoleManager.removeRole",
                                                        "RoleSecurityHandler.removeRolePrincipal("+RolePrincipalImpl.getPrincipalNameFromFullPath((String) roles[i])+")",
                                                        e.getMessage());
                log.error(msg, e);
                throw new SecurityException(msg, e);
            }
            // Remove preferences
            Preferences rolePref = Preferences.userRoot().node((String) roles[i]);
            try
            {
                rolePref.removeNode();
            }
            catch (BackingStoreException bse)
            {
                KeyedMessage msg = 
                    SecurityException.UNEXPECTED.create("RoleManager.removeRole",
                                                        "Preferences.removeNode("+roles[i]+")",
                                                        bse.getMessage());
                log.error(msg, bse);
                throw new SecurityException(msg, bse);
            }
        }
    }

    /**
     * @see org.apache.jetspeed.security.RoleManager#roleExists(java.lang.String)
     */
    public boolean roleExists(String roleFullPathName)
    {
        ArgUtil.notNull(new Object[] { roleFullPathName }, new String[] { "roleFullPathName" },
                "roleExists(java.lang.String)");

        Principal principal = roleSecurityHandler.getRolePrincipal(roleFullPathName);
        boolean roleExists = (null != principal);
        if (log.isDebugEnabled())
        {
            log.debug("Role exists: " + roleExists);
            log.debug("Role: " + roleFullPathName);
        }
        return roleExists;
    }

    /**
     * @see org.apache.jetspeed.security.RoleManager#getRole(java.lang.String)
     */
    public Role getRole(String roleFullPathName) throws SecurityException
    {
        ArgUtil.notNull(new Object[] { roleFullPathName }, new String[] { "roleFullPathName" },
                "getRole(java.lang.String)");

        String fullPath = RolePrincipalImpl.getFullPathFromPrincipalName(roleFullPathName);

        Principal rolePrincipal = roleSecurityHandler.getRolePrincipal(roleFullPathName);
        if (null == rolePrincipal)
        {
            throw new SecurityException(SecurityException.ROLE_DOES_NOT_EXIST.create(roleFullPathName));
        }
        Preferences preferences = Preferences.userRoot().node(fullPath);
        Role role = new RoleImpl(rolePrincipal, preferences);
        return role;
    }

    /**
     * @see org.apache.jetspeed.security.RoleManager#getRolesForUser(java.lang.String)
     */
    public Collection getRolesForUser(String username) throws SecurityException
    {
        ArgUtil.notNull(new Object[] { username }, new String[] { "username" }, "getRolesForUser(java.lang.String)");

        Collection roles = new ArrayList();

        Set rolePrincipals = securityMappingHandler.getRolePrincipals(username);
        Iterator rolePrincipalsIter = rolePrincipals.iterator();
        while (rolePrincipalsIter.hasNext())
        {
            Principal rolePrincipal = (Principal) rolePrincipalsIter.next();
            Preferences preferences = Preferences.userRoot().node(
                    RolePrincipalImpl.getFullPathFromPrincipalName(rolePrincipal.getName()));
            roles.add(new RoleImpl(rolePrincipal, preferences));
        }
        return roles;
    }

    /**
     * @see org.apache.jetspeed.security.RoleManager#getRolesInGroup(java.lang.String)
     */
    public Collection getRolesInGroup(String groupFullPathName) throws SecurityException
    {
        ArgUtil.notNull(new Object[] { groupFullPathName }, new String[] { "groupFullPathName" },
                "getRolesInGroup(java.lang.String)");

        Collection roles = new ArrayList();

        Set rolePrincipals = securityMappingHandler.getRolePrincipalsInGroup(groupFullPathName);
        Iterator rolePrincipalsIter = rolePrincipals.iterator();
        while (rolePrincipalsIter.hasNext())
        {
            Principal rolePrincipal = (Principal) rolePrincipalsIter.next();
            Preferences preferences = Preferences.userRoot().node(
                    RolePrincipalImpl.getFullPathFromPrincipalName(rolePrincipal.getName()));
            roles.add(new RoleImpl(rolePrincipal, preferences));
        }
        return roles;
    }

    /**
     * @see org.apache.jetspeed.security.RoleManager#addRoleToUser(java.lang.String,
     *      java.lang.String)
     */
    public void addRoleToUser(String username, String roleFullPathName) throws SecurityException
    {
        ArgUtil.notNull(new Object[] { username, roleFullPathName }, new String[] { "username", "roleFullPathName" },
                "addUserToRole(java.lang.String, java.lang.String)");

        // Get the role principal to add to user.
        Principal rolePrincipal = roleSecurityHandler.getRolePrincipal(roleFullPathName);
        if (null == rolePrincipal)
        {
            throw new SecurityException(SecurityException.ROLE_DOES_NOT_EXIST.create(roleFullPathName));
        }
        // Check that user exists.
        Principal userPrincipal = atnProviderProxy.getUserPrincipal(username);
        if (null == userPrincipal)
        {
            throw new SecurityException(SecurityException.USER_DOES_NOT_EXIST.create(username));
        }
        // Get the user roles.
        Set rolePrincipals = securityMappingHandler.getRolePrincipals(username);
        // Add role to user.
        if (!rolePrincipals.contains(rolePrincipal))
        {
            securityMappingHandler.setRolePrincipal(username, roleFullPathName);
        }
    }

    /**
     * @see org.apache.jetspeed.security.RoleManager#removeRoleFromUser(java.lang.String,
     *      java.lang.String)
     */
    public void removeRoleFromUser(String username, String roleFullPathName) throws SecurityException
    {
        ArgUtil.notNull(new Object[] { username, roleFullPathName }, new String[] { "username", "roleFullPathName" },
                "removeRoleFromUser(java.lang.String, java.lang.String)");

        // Check that user exists.
        Principal userPrincipal = atnProviderProxy.getUserPrincipal(username);
        if (null == userPrincipal)
        {
            throw new SecurityException(SecurityException.USER_DOES_NOT_EXIST.create(username));
        }
        // Get the role principal to remove.
        Principal rolePrincipal = roleSecurityHandler.getRolePrincipal(roleFullPathName);
        if (null != rolePrincipal)
        {
            securityMappingHandler.removeRolePrincipal(username, roleFullPathName);
        }
    }

    /**
     * @see org.apache.jetspeed.security.RoleManager#isUserInRole(java.lang.String,
     *      java.lang.String)
     */
    public boolean isUserInRole(String username, String roleFullPathName) throws SecurityException
    {
        ArgUtil.notNull(new Object[] { username, roleFullPathName }, new String[] { "username", "roleFullPathName" },
                "isUserInRole(java.lang.String, java.lang.String)");

        boolean isUserInRole = false;

        Set rolePrincipals = securityMappingHandler.getRolePrincipals(username);
        Principal rolePrincipal = new RolePrincipalImpl(roleFullPathName);
        if (rolePrincipals.contains(rolePrincipal))
        {
            isUserInRole = true;
        }
        return isUserInRole;
    }

    /**
     * @see org.apache.jetspeed.security.RoleManager#addRoleToGroup(java.lang.String,
     *      java.lang.String)
     */
    public void addRoleToGroup(String roleFullPathName, String groupFullPathName) throws SecurityException
    {
        ArgUtil.notNull(new Object[] { roleFullPathName, groupFullPathName }, new String[] { "roleFullPathName",
                "groupFullPathName" }, "addRoleToGroup(java.lang.String, java.lang.String)");

        // Get the role principal to add to group.
        Principal rolePrincipal = roleSecurityHandler.getRolePrincipal(roleFullPathName);
        if (null == rolePrincipal)
        {
            throw new SecurityException(SecurityException.ROLE_DOES_NOT_EXIST.create(roleFullPathName));
        }
        securityMappingHandler.setRolePrincipalInGroup(groupFullPathName, roleFullPathName);
    }

    /**
     * @see org.apache.jetspeed.security.RoleManager#removeRoleFromGroup(java.lang.String,
     *      java.lang.String)
     */
    public void removeRoleFromGroup(String roleFullPathName, String groupFullPathName) throws SecurityException
    {
        ArgUtil.notNull(new Object[] { roleFullPathName, groupFullPathName }, new String[] { "roleFullPathName",
                "groupFullPathName" }, "removeRoleFromGroup(java.lang.String, java.lang.String)");
        
        // Get the role principal to remove.
        Principal rolePrincipal = roleSecurityHandler.getRolePrincipal(roleFullPathName);
        if (null != rolePrincipal)
        {
            securityMappingHandler.removeRolePrincipalInGroup(groupFullPathName, roleFullPathName);
        }
    }

    /**
     * @see org.apache.jetspeed.security.RoleManager#isGroupInRole(java.lang.String,
     *      java.lang.String)
     */
    public boolean isGroupInRole(String groupFullPathName, String roleFullPathName) throws SecurityException
    {
        ArgUtil.notNull(new Object[] { roleFullPathName, groupFullPathName }, new String[] { "roleFullPathName",
                "groupFullPathName" }, "isGroupInRole(java.lang.String, java.lang.String)");

        boolean isGroupInRole = false;

        Set rolePrincipals = securityMappingHandler.getRolePrincipalsInGroup(groupFullPathName);
        Principal rolePrincipal = new RolePrincipalImpl(roleFullPathName);
        if (rolePrincipals.contains(rolePrincipal))
        {
            isGroupInRole = true;
        }

        return isGroupInRole;
    }

    /**
     * @see org.apache.jetspeed.security.RoleManager#getRoles(java.lang.String)
     */
    public Iterator getRoles(String filter) throws SecurityException
    {
        List roles = new LinkedList();
        Iterator rolePrincipals = roleSecurityHandler.getRolePrincipals(filter).iterator();
        while (rolePrincipals.hasNext())
        {
            String roleName = ((Principal) rolePrincipals.next()).getName();
            Role role = getRole(roleName);
            roles.add(role);
        }
        return roles.iterator();
    }

}