/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2005 Sun Microsystems Inc. All Rights Reserved
 *
 * The contents of this file are subject to the terms
 * of the Common Development and Distribution License
 * (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at
 * https://opensso.dev.java.net/public/CDDLv1.0.html or
 * opensso/legal/CDDLv1.0.txt
 * See the License for the specific language governing
 * permission and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL
 * Header Notice in each file and include the License file
 * at opensso/legal/CDDLv1.0.txt.
 * If applicable, add the following below the CDDL Header,
 * with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * $Id: COSManager.java,v 1.5 2009/01/28 05:34:51 ww203982 Exp $
 *
 * Portions Copyright 2015 ForgeRock AS.
 */

package com.iplanet.ums.cos;

import com.iplanet.services.ldap.Attr;
import com.iplanet.services.ldap.AttrSet;
import com.iplanet.services.util.I18n;
import com.iplanet.sso.SSOException;
import com.iplanet.sso.SSOToken;
import com.iplanet.sso.SSOTokenManager;
import com.iplanet.ums.Guid;
import com.iplanet.ums.IUMSConstants;
import com.iplanet.ums.PersistentObject;
import com.iplanet.ums.SchemaManager;
import com.iplanet.ums.SearchResults;
import com.iplanet.ums.UMSException;
import com.iplanet.ums.UMSObject;
import java.security.Principal;
import java.util.AbstractCollection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.StringTokenizer;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.Modification;
import org.forgerock.opendj.ldap.ModificationType;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchScope;

/**
 * This class has the responsibility of adding, removing and replacing COS
 * definitions. It also provides search capabilities for COS definitions.
 * @supported.api
 */
public class COSManager {

    /**
     * This constructor sets the parent Directory entry which identifies the
     * location of COS definitions which will be managed. It also gets an
     * instance of a SchemaManager which will be used to update schema entries
     * for COS assignments.
     * 
     * @param token Authenticated principal's single sign on token.
     * @param guid The unique identifier specifying where COS definitions will
     *        be managed.
     * @throws UMSException if the token authentication fails, or if
     *         the guid for the parent entry is not valid.
     */
    protected COSManager(SSOToken token, Guid guid) throws UMSException {
        try {
            SSOTokenManager.getInstance().validateToken(token);
        } catch (SSOException se) {
            throw new UMSException(i18n.getString(IUMSConstants.INVALID_TOKEN),
                    se);
        }
        _parentObject = UMSObject.getObject(token, guid);
        try {
            _schemaManager = SchemaManager.getSchemaManager(token
                    .getPrincipal());
        } catch (SSOException se) {
            throw new UMSException("Bad Authentication Token "
                    + se.getMessage());
        }
    }

    /**
     * This constructor sets the parent Directory entry which identifies the
     * location of COS definitions which will be managed. It also gets an
     * instance of a SchemaManager which will be used to update schema entries
     * for COS assignments.
     * 
     * @param principal
     *            Authenticated principal
     * @param guid
     *            The unique identifier specifying where COS definitions will be
     *            managed.
     * 
     * @throws UMSException
     *             The exception thrown if there is a problem determining the
     *             parent entry, or getting the SchemaManager instance.
     */
    protected COSManager(Principal principal, Guid guid) throws UMSException {
        _parentObject = UMSObject.getObject(principal, guid);
        _schemaManager = SchemaManager.getSchemaManager(principal);
    }

    /**
     * This method returns an instance of a COS Manager.
     * 
     * @param token Authenticated principal's single sign on token.
     * @param guid COS definitions will be managed under the level identified by
     *        this guid.
     * @throws UMSException
     *             The exception thrown from the COSManager constructor.
     * @supported.api
     */
    public static COSManager getCOSManager(SSOToken token, Guid guid)
            throws UMSException {
        return new COSManager(token, guid);
    }

    /**
     * This method returns an instance of a COS Manager.
     * 
     * @param principal Authenticated principal.
     * @param guid COS definitions will be managed under the level identified by
     *        this guid.
     * @throws UMSException
     *             The exception thrown from the data layer.
     */
    public static COSManager getCOSManager(Principal principal, Guid guid)
            throws UMSException {
        return new COSManager(principal, guid);
    }

    /**
     * This method adds a COS definition to the persistent store. The definition
     * is added under the specified "guid" parameter.
     * 
     * @param cosDef
     *            The COS definition to be added.
     * 
     * @throws UMSException
     *             The exception thrown from the data layer.
     * @supported.api
     */
    public void addDefinition(ICOSDefinition cosDef) throws UMSException {
        if (!(cosDef instanceof DirectCOSDefinition)) {
            String msg = i18n.getString(IUMSConstants.INVALID_COSDEFINITION);
            throw new UMSException(msg);
        }
        String[] cosAttributes = cosDef.getCOSAttributes();
        AbstractCollection aList = (AbstractCollection) Arrays
                .asList(ICOSDefinition.qualifiers);
        for (int i = 0; i < cosAttributes.length; i++) {
            String cosAttribute = null;
            String qualifier = null;
            StringTokenizer st = new StringTokenizer(cosAttributes[i]);
            if (st.hasMoreTokens()) {
                cosAttribute = st.nextToken();
            }
            if (cosAttribute == null) {
                String msg = i18n.getString(
                        IUMSConstants.INVALID_COS_ATTRIBUTE_QUALIFIER);
                throw new UMSException(msg);
            }
            if (st.hasMoreTokens())
                qualifier = st.nextToken();
            if (qualifier == null) {
                qualifier = ICOSDefinition.qualifiers[ICOSDefinition.DEFAULT];
                cosDef.removeCOSAttribute(cosAttribute);
                cosDef.addCOSAttribute(cosAttribute, ICOSDefinition.DEFAULT);
            }
            if (!aList.contains(qualifier)) {
                String msg = i18n.getString(
                        IUMSConstants.INVALID_COS_ATTRIBUTE_QUALIFIER);
                throw new UMSException(msg);
            }
        }
        PersistentObject po = (PersistentObject) cosDef;
        _parentObject.addChild(po);
    }

    /**
     * Removes the COS definition.
     * 
     * @param name
     *            The name of the definition to be removed.
     * 
     * @throws UMSException
     *             The exception thrown from the data layer.
     * @supported.api
     */
    public void removeDefinition(String name) throws UMSException {
        Guid guid = new Guid(ICOSDefinition.DEFAULT_NAMING_ATTR + "=" + name
                + "," + _parentObject.getGuid().getDn());
        _parentObject.removeChild(guid);
    }

    /**
     * Updates the contents of a COS definition with the new contents. The COS
     * definition must already exist in the persistent layer, before its
     * contents can be replaced.
     * 
     * @param cosDef
     *            The COS definition containing new contents, which will replace
     *            the same definition in the persistent layer.
     * 
     * @throws UMSException
     *             The exception thrown from the data layer.
     * @supported.api
     */
    public void updateDefinition(ICOSDefinition cosDef) throws UMSException {
        PersistentObject pObject = (PersistentObject) cosDef;
        if (pObject.getGuid() == null) {
            String msg = i18n
                    .getString(IUMSConstants.REPLACE_DEFINITION_NOT_PERSISTENT);
            throw new UMSException(msg);
        }
        pObject.save();
    }

    /**
     * Returns COS definition given the name.
     * 
     * @param name Name of the COS definition.
     * @return A COS definition with the specified name.
     * @throws UMSException if exception occurred at the data layer.
     * @throws COSNotFoundException if the COS object is not found.
     * @supported.api
     */
    public ICOSDefinition getDefinition(String name) throws UMSException,
            COSNotFoundException {
        ICOSDefinition cosDef = null;
        SearchResults sr = _parentObject.getChildren(
                ICOSDefinition.COSSUPERDEF_NAME_SEARCH + name + ")",
                DEF_ATTRIBUTE_NAMES, null);
        while (sr.hasMoreElements()) {
            cosDef = (ICOSDefinition) sr.next();
            if (cosDef.getName().equals(name)) {
                break;
            } else {
                cosDef = null;
            }
        }
        if (cosDef == null) {
            String msg = i18n.getString(IUMSConstants.COS_DEFINITION_NOT_FOUND);
            throw new COSNotFoundException(msg);
        }
        sr.abandon();
        return cosDef;
    }

    /**
     * Retrieves all COS definitions for the current organization. This
     * COSManager instance applies to an organization.
     * 
     * @return A collection of COS definition objects.
     * 
     * @throws UMSException
     *             The exception thrown from the data layer.
     * @supported.api
     */
    public Collection getDefinitions() throws UMSException {
        Collection cosDefinitions = new ArrayList();
        SearchResults sr = _parentObject.search(
                ICOSDefinition.COSSUPERDEF_SEARCH, DEF_ATTRIBUTE_NAMES, null);
        while (sr.hasMoreElements()) {
            cosDefinitions.add(sr.next());
        }
        return cosDefinitions;
    }

    /**
     * Assigns a COS (as defined by a COS definition) to the persistent object.
     * The COS target persistent object could be a user, group, organization,
     * organizationalunit, etc. The COS target object must be persistent before
     * this method can be used.
     * 
     * @param pObject
     *            The COS target persistent object.
     * @param cosDef
     *            A COS definition.
     * @param cosTemplate
     *            A COS template. This only applies for COS and Indirect COS
     *            definitions. For pointer COS definitions, this parameter can
     *            be null.
     * 
     * @throws UMSException
     *             If a data layer exception occurs.
     * @supported.api
     */
    public void assignCOSDef(PersistentObject pObject, ICOSDefinition cosDef,
            COSTemplate cosTemplate) throws UMSException {
        if (pObject == null || cosDef == null) {
            String msg = i18n
                    .getString(IUMSConstants.COS_DEF_OR_TARGET_OBJECT_NULL);
            throw new UMSException(msg);
        }

        // Do validation....
        //
        if (pObject.getGuid() == null) {
            String msg = i18n
                    .getString(IUMSConstants.COS_TARGET_OBJECT_NOT_PERSISTENT);
            throw new UMSException(msg);
        }

        if (!(cosDef instanceof DirectCOSDefinition)) {
            String msg = i18n.getString(IUMSConstants.INVALID_COSDEFINITION);
            throw new UMSException(msg);
        }

        if (cosDef instanceof DirectCOSDefinition) {
            assignDirectCOSDef(pObject, (DirectCOSDefinition) cosDef,
                    cosTemplate, _schemaManager);
        }
    }

    /**
     * Removes COS assignment from the persistent object. The COS target
     * persistent object could be a user, group, organization,
     * organizationalunit, etc. The COS target object must be persistent before
     * this method can be used.
     * 
     * @param pObject
     *            The COS target persistent object.
     * @param cosDef
     *            A COS definition.
     * @param cosTemplate
     *            A COS template.
     * 
     * @throws UMSException
     *             The exception thrown if any of the following occur: o the
     *             target persistent object or COS definition parameter is null.
     *             o the target object is not persistent. o the COS definition
     *             is not one of the valid COS definitions. o an exception is
     *             propagated from any of the "remove" methods.
     * @supported.api
     */
    public void removeCOSAssignment(PersistentObject pObject,
            ICOSDefinition cosDef, COSTemplate cosTemplate) throws UMSException 
            {
        if (pObject == null || cosDef == null) {
            String msg = i18n
                    .getString(IUMSConstants.COS_DEF_OR_TARGET_OBJECT_NULL);
            throw new UMSException(msg);
        }

        // Do validation....
        //
        if (pObject.getGuid() == null) {
            String msg = i18n
                    .getString(IUMSConstants.COS_TARGET_OBJECT_NOT_PERSISTENT);
            throw new UMSException(msg);
        }

        if (!(cosDef instanceof DirectCOSDefinition)) {
            String msg = i18n.getString(IUMSConstants.INVALID_COSDEFINITION);
            throw new UMSException(msg);
        }

        if (cosDef instanceof DirectCOSDefinition) {
            removeDirectCOSAssignment(pObject, (DirectCOSDefinition) cosDef,
                    cosTemplate, _schemaManager);
        }
    }

    /**
     * Removes a Direct COS assignment from a target persistent object. The COS
     * target persistent object could be a user, group, organization,
     * organizationalunit, etc. The COS target object must be persistent before
     * this method can be used.
     * 
     * @param pObject
     *            The COS target persistent object.
     * @param cosDef
     *            A COS definition.
     * @param sMgr
     *            A SchemaManager object, which is used to determine object
     *            classes for attributes.
     * 
     * @throws UMSException
     *             The exception thrown if any of the following occur: o an
     *             exception occurs determining the object class for the COS
     *             specifier. o an exception occurs determining the object class
     *             for the COS attributes. o there is an exception thrown rom
     *             the data layer.
     */
    private void removeDirectCOSAssignment(PersistentObject pObject,
            DirectCOSDefinition cosDef, COSTemplate cosTemplate,
            SchemaManager sMgr) throws UMSException {
        ArrayList aList;
        AttrSet attrSet = new AttrSet();

        try {
            // Include the attribute (whose name is the cosSpecifier)
            // in the attribute set for removal (only if it exists).
            //
            if (pObject.getAttribute(cosDef.getCOSSpecifier()) != null)
                attrSet.add(new Attr(cosDef.getCOSSpecifier(), cosTemplate
                        .getName()));

            // Get cosSpecifier object class - should only be one.
            // Include the cosSpecifier object class in the attribute
            // set for removal (only if itt exists).
            //
            aList = (ArrayList) sMgr.getObjectClasses(cosDef.getCOSSpecifier());
            String cosSpecObjectClass = (String) aList.get(0);
            if (objectClassExists(cosSpecObjectClass, pObject)) {
                attrSet.add(new Attr("objectclass", cosSpecObjectClass));
            }

            // Get the cos attributes from the definition (ex. mailquota).
            // For each of the attributes, get the objectclass. Include the
            // object classes in the attribute set for removal (if they exist).
            //
            String[] cosAttributes = cosDef.getCOSAttributes();
            String cosAttribute = null;
            for (int i = 0; i < cosAttributes.length; i++) {
                // Only get the attribute - not the qualifier
                //
                StringTokenizer st = new StringTokenizer(cosAttributes[i]);
                cosAttribute = st.nextToken();
                aList = (ArrayList) sMgr.getObjectClasses(cosAttribute);
                String cosAttributeObjectClass = (String) aList.get(0);
                if (objectClassExists(cosAttributeObjectClass, pObject)) {
                    attrSet
                            .add(new Attr("objectclass",
                                    cosAttributeObjectClass));
                }
            }

            if (attrSet.size() > 0) {
                pObject.modify(toModifications(ModificationType.DELETE, attrSet));
                pObject.save();
            }
        } catch (UMSException e) {
            LdapException le = (LdapException) e.getRootCause();
            // Ignore anything that is not a COS generated attribute's object class
            if (!ResultCode.OBJECTCLASS_VIOLATION.equals(le.getResult().getResultCode())) {
                throw e;
            }
        }
    }

    /**
     * Assigns a direct (Classic) COS definition to a persistent object.
     * 
     * @param pObject
     *            The target persistent object.
     * @param cosDef
     *            The direct (Classic) COS definition.
     * @param cosTemplate
     *            A COS template belonging to the definition.
     * @param sMgr
     *            A SchemaManager instance.
     * 
     * @throws UMSException
     *             if an exception occurs
     */
    private void assignDirectCOSDef(PersistentObject pObject,
            DirectCOSDefinition cosDef, COSTemplate cosTemplate,
            SchemaManager sMgr) throws UMSException {

        // Do validation....
        //
        if (cosDef.getGuid() == null) {
            String msg = i18n
                    .getString(IUMSConstants.COS_DEFINITION_NOT_PERSISTENT);
            throw new UMSException(msg);
        }

        // Make sure target entry is in same tree as COS Def parent.
        //
        DN targetDN = DN.valueOf(pObject.getGuid().getDn());
        DN cosParentDN = DN.valueOf(cosDef.getParentGuid().getDn());
        if (!(targetDN.isInScopeOf(cosParentDN, SearchScope.SUBORDINATES))) {
            String msg = i18n
                    .getString(IUMSConstants.COS_TARGET_OBJECT_DIFFERENT_TREE);
            throw new UMSException(msg);
        }

        // If cosSpecifier is "nsRole", then we don't need to go
        // any further (we don't need to update target entries).
        //
        if (cosDef.getCOSSpecifier().equalsIgnoreCase("nsrole"))
            return;

        ArrayList aList;
        AttrSet attrSet = new AttrSet();

        // Get cosSpecifier object class - should only be one.
        // Update the target entry with cosSpecifier object class.
        // Only add it if it doesn't already exist.
        //
        aList = (ArrayList) sMgr.getObjectClasses(cosDef.getCOSSpecifier());
        String cosSpecObjectClass = (String) aList.get(0);
        if (!objectClassExists(cosSpecObjectClass, pObject)) {
            attrSet.add(new Attr("objectclass", cosSpecObjectClass));
        }

        // Get the cos attributes from the definition (ex. mailquota).
        // For each of the attributes, get the objectclass. These
        // will be used to attach to the target entry. This is only
        // done if the cos attribute qualifier is not "operational"
        // (you don't need to add cos attribute object classes for
        // "operational" cos attribute qualifier.
        //
        String[] cosAttributes = cosDef.getCOSAttributes();
        String qualifier = null;
        Arrays.asList(ICOSDefinition.qualifiers);
        Attr attr = cosTemplate.getAttribute("objectclass");
        String[] cosTempObjClasses = attr.getStringValues();

        for (int i = 0; i < cosAttributes.length; i++) {
            StringTokenizer st = new StringTokenizer(cosAttributes[i]);
            st.nextToken();
            qualifier = st.nextToken();
            if ((!qualifier.equals(
                    ICOSDefinition.qualifiers[ICOSDefinition.OPERATIONAL]))) {
                for (int j = 0; j < cosTempObjClasses.length; j++) {
                    if (!cosTempObjClasses[j].equalsIgnoreCase("top")
                       && !cosTempObjClasses[j].equalsIgnoreCase("costemplate")
                       && !objectClassExists(cosTempObjClasses[j], pObject)) 
                    {
                        if (!attrSet.contains("objectclass",
                                cosTempObjClasses[j])) {
                            attrSet.add(new Attr("objectclass",
                                    cosTempObjClasses[j]));
                        }
                    }
                }
            }
        }

        // Add the attribute name (cosSpecifier value) and attribute
        // value (cosTemplate name) only if it doesn't exist.
        //
        if (pObject.getAttribute(cosDef.getCOSSpecifier()) == null)
            attrSet.add(new Attr(cosDef.getCOSSpecifier(), cosTemplate
                    .getName()));

        if (attrSet.size() > 0) {
            pObject.modify(toModifications(ModificationType.ADD, attrSet));
            pObject.save();
        }
    }

    private Collection<Modification> toModifications(ModificationType type, AttrSet attrSet) {
        Collection<Modification> modifications = new HashSet<>();
        Enumeration<Attr> attributes = attrSet.getAttributes();
        while (attributes.hasMoreElements()) {
            modifications.add(new Modification(type, attributes.nextElement().toLDAPAttribute()));
        }
        return modifications;
    }

    /**
     * Utility method to check if an object class exists in a persistent object.
     * 
     * @param objectClass
     *            The object class.
     * @param pObject
     *            The persistent object.
     */
    private boolean objectClassExists(String objectClass,
            PersistentObject pObject) {
        Attr attr = pObject.getAttribute("objectclass");
        String[] vals = attr.getStringValues();
        for (int i = 0; i < vals.length; i++) {
            if (objectClass.equalsIgnoreCase(vals[i])) {
                return true;
            }
        }
        return false;
    }

    //
    // Definition Search Attributes
    //
    private static final String[] DEF_ATTRIBUTE_NAMES = { "objectclass",
            ICOSDefinition.DEFAULT_NAMING_ATTR, ICOSDefinition.COSTEMPLATEDN,
            ICOSDefinition.COSSPECIFIER, ICOSDefinition.COSATTRIBUTE,
            ICOSDefinition.ICOSSPECIFIER };

    private PersistentObject _parentObject;

    private SchemaManager _schemaManager;

    private static I18n i18n = I18n.getInstance(IUMSConstants.UMS_PKG);
}
