/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2004 Bull S.A.
 * 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 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * Initial developer(s): Florent BENOIT
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.deployment.web;

import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.ow2.jonas.deployment.common.xml.SecurityRole;
import org.ow2.jonas.deployment.web.xml.AuthConstraint;
import org.ow2.jonas.deployment.web.xml.SecurityConstraint;
import org.ow2.jonas.deployment.web.xml.UserDataConstraint;
import org.ow2.jonas.deployment.web.xml.WebApp;
import org.ow2.jonas.deployment.web.xml.WebResourceCollection;
import org.ow2.jonas.lib.util.Log;

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;


/**
 * This class is used to manage security constraint in web applications
 * useful for JACC implementation as it returns set of JACC permissions
 * @author Florent Benoit
 */
public class SecurityConstraintListDesc {

    /**
     * Default pattern
     */
    private static final String DEFAULT_PATTERN = "/";

    /**
     * SecurityConstraint root element
     */
    private WebApp webApp = null;

    /**
     * Map between pattern and its PatternEntry object
     */
    private Map mapPatterns = null;

    /**
     * Excluded permissions
     */
    private PermissionCollection excludedPermissions = null;

    /**
     * Unchecked permissions
     */
    private PermissionCollection uncheckedPermissions = null;


    /**
     * Permissions by role
     */
    private Map permissionsByRole = null;


    /**
     * Logger
     */
    private static Logger logger = Log.getLogger(Log.JONAS_JACC_SECURITY_PREFIX);


    /**
     * Constructor
     * @param webApp root element of security constraints
     */
    public SecurityConstraintListDesc(WebApp webApp) {
        this.webApp = webApp;

        // Init patterns Map
        mapPatterns = new HashMap();

        // Init permissions
        excludedPermissions = new Permissions();
        uncheckedPermissions = new Permissions();
        permissionsByRole = new HashMap();
        try {
            // Transform DD to constraints
            initConstraints();

            // Qualify the patterns
            qualifyPatterns();

            // Build JACC permissions
            buildPermissions();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * Initialize the constraints
     * @see JACC 3.1.3.1 for the rules
     * Excluding auth-constraint = auth-constraint naming no roles
     */
    private void initConstraints() {

        // All defined security-role
        // Used if role-name = "*" in auth-constraint
        String[] securityRoles = new String[webApp.getSecurityRoleList().size()];
        int r = 0;
        for (Iterator itSecurityRoles = webApp.getSecurityRoleList().iterator(); itSecurityRoles.hasNext(); r++) {
            securityRoles[r] = ((SecurityRole) itSecurityRoles.next()).getRoleName();
        }

        SecurityConstraint securityConstraint = null;
        for (Iterator it = webApp.getSecurityConstraintList().iterator(); it.hasNext();) {

            // Retrieve Security Constraint object if any
            securityConstraint = (SecurityConstraint) it.next();

            // Get the resource collection list
            List webResourceCollectionList = securityConstraint.getWebResourceCollectionList();

            // Auth Constraint where role are defined
            AuthConstraint authConstraint = securityConstraint.getAuthConstraint();

            // User data constraint where role are defined
            UserDataConstraint userDataConstraint = securityConstraint.getUserDataConstraint();

            // Get roles if any
            List rolesList = null;
            boolean hasAuthConstraint = false;
            boolean isExcludingAuthConstraint = false;
            if (authConstraint != null) {
                rolesList = authConstraint.getRoleNameList();
                // Excluding if no roles
                hasAuthConstraint = true;
                isExcludingAuthConstraint = (rolesList.size() == 0);
            }


            // Transport Guarantee
            String transportGuarantee = null;
            if (userDataConstraint != null) {
                transportGuarantee = userDataConstraint.getTransportGuarantee();
            }


            // Now, build the structure of patterns
            WebResourceCollection webRC = null;

            // For each web resource collection
            for (Iterator itWebRC = webResourceCollectionList.iterator(); itWebRC.hasNext();) {
                webRC = (WebResourceCollection) itWebRC.next();

                // Get the http-method
                List methodList = webRC.getHttpMethodList();

                // For each pattern, add the http-method and set the transport guarantee
                // If it is not an Excluding Auth constraint, add these values to each role

                // Get all the patterns and build objects
                String urlPatternString = null;
                for (Iterator itPattern = webRC.getUrlPatternList().iterator(); itPattern.hasNext();) {
                    urlPatternString = (String) itPattern.next();

                    // Get existing if one ?
                    PatternEntry patternEntry = (PatternEntry) mapPatterns.get(urlPatternString);
                    if (patternEntry == null) {
                        patternEntry = new PatternEntry(urlPatternString);
                        mapPatterns.put(urlPatternString, patternEntry);
                    }
                    // Add for all or for all specified roles
                    String[] methods = null;
                    if (methodList.isEmpty()) {
                        // All the methods are applied
                        methods = MethodsDesc.METHODS;
                    } else {
                        methods = (String[]) methodList.toArray(new String[methodList.size()]);
                    }
                    if (hasAuthConstraint) {
                        // Excluded or role based
                        if (isExcludingAuthConstraint) {
                            patternEntry.addExcludedMethods(methods, transportGuarantee);
                        } else {
                            // role based
                            for (Iterator itRole = rolesList.iterator(); itRole.hasNext();) {
                                String roleName = (String) itRole.next();

                                // Add methods to a specific or all existing roles
                                if (roleName.equals("*")) {
                                    patternEntry.addMethodsOnRoles(methods, securityRoles, transportGuarantee);
                                } else {
                                    patternEntry.addMethodsOnRole(methods, roleName, transportGuarantee);
                                }
                            }
                        }
                    } else {
                        // No auth Constraint --> unchecked
                        patternEntry.addUncheckedMethods(methods, transportGuarantee);
                    }
                }
            }
        }
    }


    /**
     * Qualify patterns
     * @see JACC 3.1.3.1 subsection Qualified URL Pattern Names
     */
    private synchronized void qualifyPatterns() {

        // Add default pattern
        PatternEntry defaultPatternEntry = (PatternEntry) mapPatterns.get(DEFAULT_PATTERN);
        if (defaultPatternEntry == null) {
            defaultPatternEntry = new PatternEntry(DEFAULT_PATTERN);
            // Last entry to unchecked
            defaultPatternEntry.setUncheckedLastEntry();
            mapPatterns.put(DEFAULT_PATTERN, defaultPatternEntry);
        }

        // For each pattern, qualify this pattern by all patterns
        PatternEntry patternEntry = null;
        Pattern otherPattern = null;
        String patternString = null;

        // Build list of patterns object
        List patterns = new ArrayList();
        for (Iterator it = mapPatterns.keySet().iterator(); it.hasNext();) {
            patternString = (String) it.next();
            patterns.add(new Pattern(patternString));
        }

        // Sort elements
        Collections.sort(patterns);

        Pattern pattern = null;
        for (Iterator it = mapPatterns.keySet().iterator(); it.hasNext();) {
            patternString = (String) it.next();
            pattern = new Pattern(patternString);
            patternEntry = (PatternEntry) mapPatterns.get(patternString);

            // Loop on all patterns
            for (Iterator itOther = patterns.iterator(); itOther.hasNext();) {
                otherPattern = (Pattern) itOther.next();

                if (pattern.isPathPrefix() && pattern.isMatching(otherPattern)) {
                    /* first case (path prefix)
                       If the pattern is a path prefix pattern, it must be
                       qualified by every path-prefix pattern in the
                       deployment descriptor matched by and different from
                       the pattern being qualified. The pattern must also be
                       qualified by every exact pattern appearing in the
                       deployment descriptor that is matched by the pattern being
                       qualified.
                    */
                    if (otherPattern.isPathPrefix() && !pattern.equals(otherPattern)) {
                        patternEntry.addQualifiedPattern(otherPattern);
                    } else if (otherPattern.isExactPattern()) {
                        patternEntry.addQualifiedPattern(otherPattern);
                    }
                } else if (pattern.isExtensionPattern()) {
                    // Case two : Extension pattern
                    /*
                      If the pattern is an extension pattern, it must be
                      qualified by every path-prefix pattern appearing in
                      the deployment descriptor and every exact pattern in
                      the deployment descriptor that is matched by the
                      pattern being qualified.
                    */
                    if (otherPattern.isPathPrefix()  || (pattern.isMatching(otherPattern) && otherPattern.isExactPattern())) {
                        patternEntry.addQualifiedPattern(otherPattern);
                    }
                } else if (pattern.isDefaultPattern()) {
                    // Case three : Default pattern
                    /*
                      If the pattern is the default pattern, "/", it must
                      be qualified by every other pattern except the default
                      pattern appearing in the deployment descriptor.
                    */
                    if (!otherPattern.isDefaultPattern()) {
                        patternEntry.addQualifiedPattern(otherPattern);
                    }
                    /*
                     } else if (pattern.isExactPattern()) {
                      // case 4 : Exact Pattern
                      // Nothing : must not contain any qualifying pattern
                      If the pattern is an exact pattern,
                      its qualified form must not contain any qualifying
                      patterns.
                    */
                }
            }
        }
    }


    /**
     * Build permissions
     * @see JACC 3.1.3.1
     */
    private void buildPermissions() {

        PatternEntry patternEntry = null;
        // For each pattern, build permissions (Exclude default pattern for now)
        for (Iterator it = mapPatterns.values().iterator(); it.hasNext();) {
            patternEntry = (PatternEntry) it.next();
            // No permissions if the pattern is irrelevant
            if (!patternEntry.isIrrelevant()) {
                if (patternEntry.isUncheckedLastEntry()) {
                    addUncheckedPermissions(patternEntry.getUncheckedPermissions());
                } else {
                    addExcludedPermissions(patternEntry.getExcludedPermissions());
                    addUncheckedPermissions(patternEntry.getUncheckedPermissions());
                    addRolePermissions(patternEntry.getRolesPermissionsMap());
                }
            }
        }
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "Excluded permissions = " + excludedPermissions);
            logger.log(BasicLevel.DEBUG, "Unchecked permissions = " + uncheckedPermissions);
            logger.log(BasicLevel.DEBUG, "Roles Permissions = ");

            String roleName = null;
            for (Iterator it = permissionsByRole.keySet().iterator(); it.hasNext();) {
                roleName = (String) it.next();
                logger.log(BasicLevel.DEBUG, "Permissions for role " + roleName + " are "
                           + permissionsByRole.get(roleName));
            }
        }

    }


    /**
     * Add Excluded permissions
     * @param permissions permissions to add. if permissions are null,
     *                    no permissions are added.
     */
    private void addExcludedPermissions(PermissionCollection permissions) {
        if (permissions == null) {
            return;
        }

        for (Enumeration e = permissions.elements(); e.hasMoreElements();) {
            excludedPermissions.add((Permission) e.nextElement());
        }
    }

    /**
     * Add Unchecked permissions
     * @param permissions permissions to add. if permissions are null,
     *                    no permissions are added.
     */
    private void addUncheckedPermissions(PermissionCollection permissions) {
        if (permissions == null) {
            return;
        }

        for (Enumeration e = permissions.elements(); e.hasMoreElements();) {
            uncheckedPermissions.add((Permission) e.nextElement());
        }
    }

    /**
     * Add permissions on the roles
     * @param rolePermissionsMap permissions to add.(Map between role and Permissions)
     */
    private void addRolePermissions(Map rolePermissionsMap) {
        if (rolePermissionsMap == null) {
            return;
        }

        // For each role, build permissions on actions found on the role.
        String roleName = null;
        PermissionCollection permissions = null;
        PermissionCollection existingRolePermissions = null;
        for (Iterator it = rolePermissionsMap.keySet().iterator(); it.hasNext();) {
            roleName = (String) it.next();
            permissions = (PermissionCollection) rolePermissionsMap.get(roleName);
            if (permissions != null) {
                existingRolePermissions = (PermissionCollection) permissionsByRole.get(roleName);
                if (existingRolePermissions == null) {
                    existingRolePermissions = new Permissions();
                    permissionsByRole.put(roleName, existingRolePermissions);
                }
                for (Enumeration e = permissions.elements(); e.hasMoreElements();) {
                    existingRolePermissions.add((Permission) e.nextElement());
                }
            }
        }
    }


    /**
     * Gets the excluded permissions
     * @return excluded permissions
     */
    public PermissionCollection getExcludedPermissions() {
        return excludedPermissions;
    }


    /**
     * Gets the unchecked permissions
     * @return unchecked permissions
     */
    public PermissionCollection getUncheckedPermissions() {
        return uncheckedPermissions;
    }


    /**
     * Gets the permissions by role Map
     * @return a Map containing permissions by role
     */
    public Map getPermissionsByRole() {
        return permissionsByRole;
    }

}





