/*
 * 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: DNMapper.java,v 1.13 2009/11/20 23:52:56 ww203982 Exp $
 *
 * Portions Copyrighted 2011-2015 ForgeRock AS.
 */
package com.sun.identity.sm;

import static org.forgerock.openam.ldap.LDAPUtils.rdnValue;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;

import com.sun.identity.shared.debug.Debug;
import org.forgerock.openam.ldap.LDAPUtils;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.RDN;

/**
 * This class is used to convert a DN to iplanet UID and vice versa.
 */
public class DNMapper {

    private static Debug debug = SMSEntry.debug;

    private static HashMap cache = new HashMap(2);

    // Look for realmEnabled and cache the value.
    protected static boolean realmEnabled = ServiceManager.isRealmEnabled();

    protected static String serviceDN;

    // This set is used in reversing the realm names to sdk format.
    static boolean migration = false;

    static {
        DN dn = DN.valueOf(SMSEntry.baseDN).child(SMSEntry.SERVICES_RDN);
        serviceDN = dn.toString().toLowerCase();
    }

    /**
     * Converts orgname which is "/" seperated to DN, else if DN normalize the
     * DN and return
     */
    public static String orgNameToDN(String orgName) {
        // Check if it is null or empty
        if (orgName == null || orgName.trim().length() == 0
                || orgName.equals(SMSEntry.SLASH_STR)) {
            return (SMSEntry.baseDN);
        }

        // Check in cache
        String orgdn = (String) cache.get(orgName);
        if (orgdn != null) {
            return (orgdn);
        }

        /*
         * Check if orgName is a valid DN. If so, check if realmEnabled. if
         * realmEnabled, 1) Check if rest of the DN (before the baseDN) has "o"
         * as the naming attribute. If not, replace it with 'o' and concat the
         * value with the previous naming attribute. eg.,if orgName is
         * dc=abc,l=xyz,o=coke,ou=services,dc=iplanet,dc=com then, the final
         * string should be
         * o=dc_abc,o=l_xyz,o=coke,ou=services,dc=iplanet,dc=com 2) Check if
         * "ou=services" is present in the orgName. If not add it to the
         * orgName.
         */
        if (!orgName.startsWith("/")) {
            DN orgdnObject = LDAPUtils.newDN(orgName);
            if (orgdnObject.size() > 0) {
                // If orgName is either the baseDN or root service's DN
                // return the baseDN
                orgdn = orgdnObject.toString();
                String orgdnlc = orgdn.toLowerCase();

                // Check if orgdn is a hidden internal realm, if so return
                if (orgdnlc.startsWith(SMSEntry.SUN_INTERNAL_REALM_PREFIX)) {
                    orgdn = orgdnObject.rdn().toString() + "," + serviceDN;
                    // Add to cache and return
                    updateCache(orgName, orgdn);
                    return orgdn;
                }
                // Check for root suffix and SMS base DN
                if (orgdnlc.equals(SMSEntry.baseDN) ||
                        orgdnlc.equals(serviceDN) ||
                        orgdnlc.equals(SMSEntry.amsdkbaseDN)) {
                    // Add to cache and return
                    updateCache(orgName, SMSEntry.baseDN);
                    return (SMSEntry.baseDN);
                }
                // If realm is enabled, normalize the DN and return
                if (realmEnabled) {
                    int ndx = orgdn.indexOf(serviceDN);
                    if (ndx == -1) {
                        // Check for baseDN
                        ndx = orgdn.lastIndexOf(SMSEntry.baseDN);
                    }
                    if (ndx > 0) {
                        orgdn = orgdn.substring(0, ndx - 1);
                    }
                    int indx = orgdn.lastIndexOf(SMSEntry.COMMA);
                    if (indx >= 0) {
                        if (orgdn.substring(indx).equals(SMSEntry.COMMA)) {
                            orgdn = orgdn.substring(0, indx);
                        }
                    }
                    if (debug.messageEnabled()) {
                        debug.message("DNMapper.orgNameToDN():orgdn " + orgdn);
                    }
                    String answer = normalizeDN(orgdn) + serviceDN;
                    if (debug.messageEnabled()) {
                        debug.message("DNMapper.orgNameToDN(" + orgName + ")="
                                + answer);
                    }
                    // Add to cache and return
                    updateCache(orgName, answer);
                    return (answer);
                } else if (!migration) {
                    // Check if "ou=services" is present, if present remove it
                    orgdn = replaceString(orgdn, ",ou=services,", ",");
                    // Add to cache and return
                    updateCache(orgName, orgdn);
                    return (orgdn);
                } else {
                    // When SMS Migration to 7.0 happens, the coexist mode is
                    // 'true' and realm is 'false'. In coexist mode, the
                    // 'ou=services' gets removed. But we need the new realm node
                    // for data migration from old DIT to new realm tree.
                    // So after creation of the realm during SMSMigration70,
                    // we set the DNMapper.migration flag to true to avoid
                    // removal of 'ou=services' from the newly formed realm DN
                    // and return the orgdn as such to the serviceconfig* class.

                    return (orgdn);
                }
            }
        }

        // The org name is "/" separated, construct the DN
        StringBuffer buf = convertToDN(orgName);

        if (realmEnabled
                || buf.toString().toLowerCase().indexOf(
                        SMSEntry.SUN_INTERNAL_REALM_NAME) != -1) {
            buf.append(",").append(serviceDN);
        } else if (SMSEntry.baseDN.length() > 0) {
            buf.append(",").append(SMSEntry.baseDN);
        }
        if (debug.messageEnabled()) {
            debug.message("DNMapper.orgNameToDN(" + orgName + ")="
                    + buf.toString());
        }
        String answer = buf.toString();
        // Add to cache
        updateCache(orgName, answer);
        return (answer);
    }

    private static void updateCache(String orgName, String realmName) {
        HashMap ncache = new HashMap(cache);
        // %%% TODO Need to check the size and remove least recently used
        ncache.put(orgName, realmName);
        cache = ncache;
    }

    /**
     * Converts realm name to AMSDK compliant organization name
     */
    public static String realmNameToAMSDKName(String realmName) {
        String dn = orgNameToDN(realmName);
        String dnlc = dn.toLowerCase();
        if (debug.messageEnabled()) {
            debug.message("DNMapper.realmNameToAMSDKName realmName ="
                    + realmName);
            debug.message("DNMapper.realmNameToAMSDKName orgDN =" + dn);
        }

        // Check for baseDN and internal hidden realm names
        if ((dnlc.equals(SMSEntry.baseDN)) &&
            (!dnlc.equals(SMSEntry.amsdkbaseDN))) {
            return (SMSEntry.amsdkbaseDN);
        }

        if (dnlc.equals(SMSEntry.baseDN)
                || dnlc.startsWith(SMSEntry.SUN_INTERNAL_REALM_PREFIX)) {
            return (SMSEntry.baseDN);
        }

        // If realm is not enabled, remove "ou=services" node
        StringBuilder buf = new StringBuilder(dn.length());
        String orgAttr = OrgConfigViaAMSDK.getNamingAttrForOrg();

        // If orgAttr is null or is "o", return after removing "ou=services"
        if (orgAttr == null
                || orgAttr.equalsIgnoreCase(SMSEntry.ORGANIZATION_RDN)) {
            String answer = replaceString(dn, ",ou=services,", ",");
            if (debug.messageEnabled()) {
                debug.message("DNMapper.realmNameToAMSDKName sdkName ="
                        + answer);
            }
            return (answer);
        }

        // Remove the baseDN and parse the DN
        int index = dnlc.indexOf(serviceDN);
        if (index == -1) {
            // Try the baseDN
            index = dnlc.indexOf(SMSEntry.baseDN);
        }

        String answer = (index == -1) ? dn : dn.substring(0, index - 1);
        DN answerDN = DN.valueOf(answer);
        for (RDN rdn : answerDN) {
            buf.append(orgAttr).append(SMSEntry.EQUALS).append(rdnValue(rdn));
            buf.append(',');
        }
        // Append baseDN and return
        buf.append(SMSEntry.baseDN);
        if (debug.messageEnabled()) {
            debug.message("DNMapper.realmNameToAMSDKName sdkName ="
                    + buf.toString());
        }
        return (buf.toString());
    }

    /**
     * Returns realm name in "/" separated format for the provided
     * realm/organization name in DN format.
     * 
     * @param orgName Name of organization.
     * @return DN format "/" separated realm name of organization name.
     */
    public static String orgNameToRealmName(String orgName) {
        if ((orgName == null) || (orgName.length() == 0)) {
            return "/";
        }
        if (orgName.equalsIgnoreCase(SMSEntry.baseDN) ||
            orgName.equalsIgnoreCase(serviceDN)
        ) {
            return "/";
        }
        if (!orgName.contains("=")) {
            return orgName;
        }
        DN orgdnObject = DN.valueOf(orgName);

        StringBuilder answer = new StringBuilder(100);
        answer.append("/");

        Set resultSet = new HashSet(2);
        resultSet.add(orgName);

        // Check if orgName ends with baseDN or serviceDN
        String orgdn = orgdnObject.toString();
        String orgdnlc = orgdn.toLowerCase();
        Set returnSet = null;

        if (orgdnlc.endsWith(serviceDN)) {
            returnSet = SMSEntry.parseResult(resultSet, serviceDN, true);
        } else if (orgdnlc.endsWith(SMSEntry.baseDN)) {
            returnSet = SMSEntry.parseResult(resultSet, serviceDN, true);
        }
        if (returnSet != null && !returnSet.isEmpty()) {
            answer.append(returnSet.iterator().next().toString());
        }
        return (answer.toString());
    }

    /*
     * Splits a string and returns the tokens.
     * 
     * @param str original String. 
     * @return a String Array object of tokens after split.
     */
    static String[] splitString(String str) {
        String[] strArray = new String[2];
        int idx = str.indexOf('=');

        if (idx != -1) {
            strArray[0] = str.substring(0, idx).trim();
            strArray[1] = str.substring(idx + 1).trim();
        }
        return strArray;
    }

    /*
     * Replaces a string with another string in a String object.
     * 
     * @param originalString original String. 
     * @param token string to be replaced. 
     * @param newString new string to replace token. 
     * @return a String object after replacement.
     * 
     */
    static String replaceString(String originalString, String token,
            String newString) {
        int lenToken = token.length();
        int idx = originalString.indexOf(token);

        if (!originalString.startsWith(SMSEntry.SLASH_STR)) {
            if (idx >= 0) {
                int slashndx =
                    originalString.substring(idx).indexOf(SMSEntry.SLASH_STR);
                // This is to escape "/" embedded in realm names.
                while (slashndx != -1) {
                    originalString = originalString.substring(0, slashndx) +
                        "&#47;" + originalString.substring(slashndx+1);
                    slashndx =
                        originalString.indexOf(SMSEntry.SLASH_STR, slashndx+5);
                }
            }
        }
        while (idx != -1) {
            originalString = originalString.substring(0, idx) + newString
                    + originalString.substring(idx + lenToken);
            idx = originalString.indexOf(token, idx + lenToken);
        }

        if (debug.messageEnabled()) {
            debug.message("DNMapper.replaceString() " + originalString);
        }
        return originalString;
    }

    /**
     * Normalized the DN as per the Realm requirements for organization name
     */
    static String normalizeDN(String orgName) {
        String orgAttr = "";
        StringBuilder buf = new StringBuilder(orgName.length());

        if (debug.messageEnabled()) {
            debug.message("DNMapper.normalizeDN():orgName "+ orgName);
        }
        if (!realmEnabled) {
            orgAttr = OrgConfigViaAMSDK.getNamingAttrForOrg();
        }
        String placeHold = (realmEnabled) ? SMSEntry.ORGANIZATION_RDN : orgAttr;
        DN dn = DN.valueOf(orgName);
        for (RDN rdn : dn) {
            // Check if orgName is a hidden internal realm,if so prepend with o
            if (orgName.toLowerCase().
                startsWith(SMSEntry.SUN_INTERNAL_REALM_PREFIX)) {
                buf.append(SMSEntry.ORGANIZATION_RDN);
            } else {
                buf.append(placeHold);
            }
            buf.append(SMSEntry.EQUALS).append(rdnValue(rdn)).append(SMSEntry.COMMA);
        }
        debug.message("DNMapper.normalizeDN():finalorgdn {}", buf);
        return buf.toString();
    }

    /**
     * Converts "/" separted organization names to DN
     */
    static StringBuffer convertToDN(String orgName) {
        StringBuffer buf = new StringBuffer();
        String placeHold = (realmEnabled) ? SMSEntry.ORGANIZATION_RDN
                : OrgConfigViaAMSDK.getNamingAttrForOrg();
        ArrayList arr = new ArrayList();
        StringTokenizer strtok = new StringTokenizer(orgName, "/");
        while (strtok.hasMoreElements()) {
            String token = strtok.nextToken().trim();
            if (token != null && token.length() != 0) {
                arr.add(token);
            }
        }
        int size = arr.size();
        for (int i = 0; i < size; i++) {
            String theOrg = (String) arr.get(size - i - 1);
            // Check if orgdn is a hidden internal realm, if so prepend with o
            if (theOrg.toLowerCase().startsWith(
                    SMSEntry.SUN_INTERNAL_REALM_NAME)) {
                placeHold = SMSEntry.ORGANIZATION_RDN;
            }
            buf.append(placeHold);
            buf.append('=').append(theOrg);
            if (i != size - 1) {
                buf.append(',');
            }
        }
        if (debug.messageEnabled()) {
            debug.message("DNMapper.convertToDN():finalorgdn "+
                buf.toString());
        }
        if ((buf.toString()).indexOf("&#47;") >= 0) {
            String realmName = SMSSchema.unescapeName(buf.toString());
            if (debug.messageEnabled()) {
                debug.message("DNMapper.convertToDN():realmName "+realmName);
            }
            StringBuffer newBuf =  new StringBuffer();
            newBuf.append(realmName);
            buf = newBuf;
            if (debug.messageEnabled()) {
                debug.message("DNMapper.convertToDN():newRealmName "+
                    buf.toString());
            }
        }
        return (buf);
    }

    static void clearCache() {
        cache = new HashMap();
        realmEnabled = ServiceManager.isRealmEnabled();
    }
}
