/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2008 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: IdRepoJAXRPCObjectImpl.java,v 1.13 2010/01/06 01:58:27 veiming Exp $
 *
 * Portions Copyrighted 2011-2016 ForgeRock AS.
 */

package com.sun.identity.idm.server;

import static org.forgerock.openam.utils.Time.*;

import com.iplanet.am.sdk.remote.*;
import com.iplanet.am.util.SystemProperties;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import com.iplanet.services.comm.server.PLLServer;
import com.iplanet.services.comm.server.SendNotificationException;
import com.iplanet.services.comm.share.Notification;
import com.iplanet.services.comm.share.NotificationSet;
import com.iplanet.services.naming.ServerEntryNotFoundException;
import com.iplanet.services.naming.WebtopNaming;
import com.iplanet.sso.SSOException;
import com.iplanet.sso.SSOToken;
import com.iplanet.sso.SSOTokenManager;
import com.sun.identity.common.CaseInsensitiveHashMap;
import com.sun.identity.idm.AMIdentity;
import com.sun.identity.idm.IdOperation;
import com.sun.identity.idm.IdRepo;
import com.sun.identity.idm.IdRepoException;
import com.sun.identity.idm.IdSearchControl;
import com.sun.identity.idm.IdSearchOpModifier;
import com.sun.identity.idm.IdSearchResults;
import com.sun.identity.idm.IdServices;
import com.sun.identity.idm.IdServicesFactory;
import com.sun.identity.idm.IdType;
import com.sun.identity.idm.IdUtils;
import com.sun.identity.idm.remote.IdRemoteServicesImpl;
import com.sun.identity.session.util.RestrictedTokenAction;
import com.sun.identity.session.util.RestrictedTokenContext;
import com.sun.identity.shared.Constants;
import com.sun.identity.shared.debug.Debug;
import com.sun.identity.shared.encode.Base64;
import com.sun.identity.shared.xml.XMLUtils;
import com.sun.identity.sm.SMSUtils;
import com.sun.identity.sm.SchemaType;
import org.forgerock.openam.utils.CrestQuery;

import java.util.Collections;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;

/**
 * Provides service side implementation of IdRepo for JAX-RPC interface
 * <class>DirectoryManagerIF</class>. Does not provide implementation for
 * deprecated AMSDK interfaces.
 * Remote clients using openssoclientsdk.jar would be calling these
 * methods using SOAP.
 * Implements identity changes notifications via PLL and also stores
 * notifications changes for polling clients.
 */
public abstract class IdRepoJAXRPCObjectImpl implements DirectoryManagerIF {

    protected static Debug idRepoDebug = Debug.getInstance("amIdmJAXRPCServer");

    protected static SSOTokenManager tokenManager;

    protected static IdServices idServices;

    protected static final String IDREPO_SERVICE = "IdRepoServiceIF";

    // Cache of modifications for last 30 minutes & notification URLs
    protected static int cacheSize = -1;

    private static final ConcurrentSkipListMap<Long, Set<String>> idrepoCache =
            new ConcurrentSkipListMap<Long, Set<String>>();

    protected static final Map<String, URL> idRepoNotificationURLs = new ConcurrentHashMap<String, URL>();

    protected static String serverURL ;
    protected static URL urlServer;

    protected static String serverPort;

    protected static void initializeCacheSize() {
        if (cacheSize <= 0) {
            // Obtain the cache size, if configured
            String cacheSizeStr = SystemProperties.get(Constants.EVENT_LISTENER_REMOTE_CLIENT_BACKLOG_CACHE);
            if (cacheSizeStr == null) {
                cacheSize = 30;
            } else {
                try {
                    cacheSize = Integer.parseInt(cacheSizeStr);
                    if (cacheSize < 0) {
                        cacheSize = 30;
                    }
                } catch (NumberFormatException nfe) {
                    cacheSize = 30;
                }
            }
            if (idRepoDebug.messageEnabled()) {
                idRepoDebug.message("IdRepoJAXRPCObjectImpl.initialize_idrepo EventNotification cache size is set to "
                        + cacheSize);
            }
        }
    }

    /**
     * Initializes this class with system properties.
     * Called only by getSSOToken() method. Hence all other methods
     * must call either getSSOToken() or initialize() directly.
     */
    protected static void initialize_idrepo() {
        initializeCacheSize();
        // Construct serverURL
        serverPort = SystemProperties.get(Constants.AM_SERVER_PORT);
        serverURL = SystemProperties.get(Constants.AM_SERVER_PROTOCOL) +
            "://" + SystemProperties.get(Constants.AM_SERVER_HOST) +
             ":" + serverPort;
        if (idRepoDebug.messageEnabled()) {
            idRepoDebug.message("IdRepoJAXRPCObjectImpl.static server URL " +
                serverURL);
        }

        // Initialize IdRepo Service Factory
        if (idServices == null) {
            idServices = IdServicesFactory.getDataStoreServices();
        }
    }

    protected boolean isClientOnSameServer(String clientURL) {
        // Check URL is not the local server

        boolean success = true;

        URL urlClient = null;
        try {
            urlClient = new URL(clientURL);
            if (urlServer == null) {
                urlServer = new URL(serverURL);
            }
        } catch (MalformedURLException e) {
            if (idRepoDebug.warningEnabled()) {
                idRepoDebug.warning("IdRepoJAXRPCObjectImpl." +
                    "isClientOnSameServer() - clientURL is malformed " +
                    clientURL);
            }
            success = false;
        }

        if (success) { // check if it is the same server
            int port = urlClient.getPort();
            if (port == -1) {
                // If it is Port 80, and is not explicilty in the URL
                port = urlClient.getDefaultPort();
            }
            String clientPort = Integer.toString(port);

            // Protocol is same - http, so no need to check that
            boolean sameServer = ((urlServer.getHost().equalsIgnoreCase(
                    urlClient.getHost())) && serverPort.equals(clientPort));

            idRepoDebug.message("IdRepoJAXRPCObjectImpl:" +
                    "checkIfClientOnSameServer() "
                    + "Received registerNotification request from client: "
                    + clientURL + " Server URL " + serverURL
                    + " Port determined as: " + clientPort + " Check is: "
                    + sameServer);

            return sameServer;
        } else {
            return false;
        }
    }

    public void assignService_idrepo(
        String token,
        String type,
        String name,
        String serviceName,
        String stype,
        Map attrMap,
        String amOrgName,
        String amsdkDN
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        SchemaType schemaType = new SchemaType(stype);
        idServices.assignService(ssoToken, idtype, name, serviceName,
            schemaType, attrMap, amOrgName, amsdkDN);

    }

    public String create_idrepo(
        String token,
        String type,
        String name,
        Map attrMap,
        String amOrgName
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        return IdUtils.getUniversalId(idServices.create(ssoToken, idtype, name,
            attrMap, amOrgName));
    }

    public void delete_idrepo(
        String token,
        String type,
        String name,
        String orgName,
        String amsdkDN
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        idServices.delete(ssoToken, idtype, name, orgName, amsdkDN);

    }

    public Set getAssignedServices_idrepo(
        String token,
        String type,
        String name,
        Map mapOfServiceNamesAndOCs,
        String amOrgName,
        String amsdkDN
    ) throws RemoteException, IdRepoException,
        SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        return idServices.getAssignedServices(ssoToken, idtype, name,
            mapOfServiceNamesAndOCs, amOrgName, amsdkDN);
    }

    public Map getAttributes1_idrepo(
        String token,
        String type,
        String name,
        Set attrNames,
        String amOrgName,
        String amsdkDN
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        Map res = idServices.getAttributes(ssoToken, idtype, name, attrNames,
            amOrgName, amsdkDN, true);
        if (res != null && res instanceof CaseInsensitiveHashMap) {
            Map res2 = new HashMap();
            Iterator it = res.keySet().iterator();
            while (it.hasNext()) {
                Object attr = it.next();
                Set set = (Set)res.get(attr);
                set = XMLUtils.encodeAttributeSet(set, idRepoDebug);
                res2.put(attr, set);
            }
            res = res2;
        }
        return res;
    }

    public Map getAttributes2_idrepo(
        String token,
        String type,
        String name,
        String amOrgName,
        String amsdkDN
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        Map res = idServices.getAttributes(ssoToken, idtype, name, amOrgName,
            amsdkDN);

        if (res != null && res instanceof CaseInsensitiveHashMap) {
            Map res2 = new HashMap();
            Iterator it = res.keySet().iterator();
            while (it.hasNext()) {
                Object attr = it.next();
                Set set = (Set)res.get(attr);
                set = XMLUtils.encodeAttributeSet(set, idRepoDebug);
                res2.put(attr, set);
            }
            res = res2;
        }
        return res;
    }

    public Map<String, Set<String>> getBinaryAttributes_idrepo(String token, String type, String name,
            Set<String> attrNames, String amOrgName, String amsdkDN)
            throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idType = IdUtils.getType(type);
        Map<String, byte[][]> binaryAttributes =
                idServices.getAttributes(ssoToken, idType, name, attrNames, amOrgName, amsdkDN, false);
        Map<String, Set<String>> ret = new HashMap<String, Set<String>>();
        if (binaryAttributes != null) {
            //shuffle around the data so we can send it across the wire
            for (Map.Entry<String, byte[][]> entry : binaryAttributes.entrySet()) {
                String attrName = entry.getKey();
                byte[][] values = entry.getValue();
                Set<String> stringValues = new HashSet<String>(values.length);
                for (byte[] bytes : values) {
                    stringValues.add(Base64.encode(bytes));
                }
                ret.put(attrName, XMLUtils.encodeAttributeSet(stringValues, idRepoDebug));
            }
        }
        return ret;
    }

    public Set getMembers_idrepo(
        String token,
        String type,
        String name,
        String amOrgName,
        String membersType,
        String amsdkDN
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        Set results = new HashSet();
        IdType idtype = IdUtils.getType(type);
        IdType mtype = IdUtils.getType(membersType);
        Set idSet = idServices.getMembers(ssoToken, idtype, name, amOrgName,
            mtype, amsdkDN);
        if (idSet != null) {
            Iterator it = idSet.iterator();
            while (it.hasNext()) {
                AMIdentity id = (AMIdentity) it.next();
                results.add(IdUtils.getUniversalId(id));
            }
        }
        return results;
    }

    public Set getMemberships_idrepo(
        String token,
        String type,
        String name,
        String membershipType,
        String amOrgName,
        String amsdkDN
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        Set results = new HashSet();
        IdType idtype = IdUtils.getType(type);
        IdType mtype = IdUtils.getType(membershipType);
        Set idSet = idServices.getMemberships(ssoToken, idtype, name, mtype,
            amOrgName, amsdkDN);
        if (idSet != null) {
            Iterator it = idSet.iterator();
            while (it.hasNext()) {
                AMIdentity id = (AMIdentity) it.next();
                results.add(IdUtils.getUniversalId(id));
            }
        }
        return results;
    }

    public Map getServiceAttributes_idrepo(
        String token,
        String type,
        String name,
        String serviceName,
        Set attrNames,
        String amOrgName,
        String amsdkDN
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        return idServices.getServiceAttributes(ssoToken, idtype, name,
            serviceName, attrNames, amOrgName, amsdkDN);
    }

    public Map getBinaryServiceAttributes_idrepo(
        String token, String type,
        String name,
        String serviceName,
        Set attrNames,
        String amOrgName,
        String amsdkDN
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken stoken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        return idServices.getBinaryServiceAttributes(stoken, idtype, name,
                serviceName, attrNames, amOrgName, amsdkDN);
    }

    public Map getServiceAttributesAscending_idrepo(
        String token,
        String type,
        String name,
        String serviceName,
        Set attrNames,
        String amOrgName,
        String amsdkDN
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        return idServices.getServiceAttributesAscending(ssoToken, idtype,
            name, serviceName, attrNames, amOrgName, amsdkDN);
    }

    public Set getSupportedOperations_idrepo(
        String token,
        String type,
        String amOrgName
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        Set opSet = idServices
            .getSupportedOperations(ssoToken, idtype, amOrgName);
        Set resSet = new HashSet();
        if (opSet != null) {
            Iterator it = opSet.iterator();
            while (it.hasNext()) {
                IdOperation thisop = (IdOperation) it.next();
                String opStr = thisop.getName();
                resSet.add(opStr);
            }
        }
        return resSet;
    }

    public Set getSupportedTypes_idrepo(String token, String amOrgName)
        throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        Set typeSet = idServices.getSupportedTypes(ssoToken, amOrgName);
        Set resTypes = new HashSet();
        if (typeSet != null) {
            Iterator it = typeSet.iterator();
            while (it.hasNext()) {
                IdType thistype = (IdType) it.next();
                String typeStr = thistype.getName();
                resTypes.add(typeStr);
            }
        }
        return resTypes;
    }

    public Set getFullyQualifiedNames_idrepo(String token, String type,
        String name, String amOrgName)
        throws RemoteException, IdRepoException, SSOException {
        SSOToken stoken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        Set opSet = idServices.getFullyQualifiedNames(stoken, idtype,
                name, amOrgName);
        Set resSet = null;
        if (opSet != null) {
            // Convert CaseInsensitiveHashSet to HashSet
            resSet = new HashSet();
            Iterator it = opSet.iterator();
            while (it.hasNext()) {
                String opStr = (String) it.next();
                resSet.add(opStr);
            }
        }
        return resSet;
    }

    public boolean isExists_idrepo(
        String token,
        String type,
        String name,
        String amOrgName
    ) throws RemoteException, SSOException, IdRepoException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        return idServices.isExists(ssoToken, idtype, name, amOrgName);

    }

    public boolean isActive_idrepo(
        String token,
        String type,
        String name,
        String amOrgName,
        String amsdkDN
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        return idServices.isActive(ssoToken, idtype, name, amOrgName, amsdkDN);
    }

    public void setActiveStatus_idrepo(
        String token,
        String type,
        String name,
        String amOrgName,
        String amsdkDN,
        boolean active
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        idServices.setActiveStatus(
            ssoToken, idtype, name, amOrgName, amsdkDN, active);

    }

    public void modifyMemberShip_idrepo(
        String token,
        String type,
        String name,
        Set members,
        String membersType,
        int operation,
        String amOrgName
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        IdType mtype = IdUtils.getType(membersType);
        idServices.modifyMemberShip(ssoToken, idtype, name, members, mtype,
            operation, amOrgName);
    }

    public void modifyService_idrepo(
        String token,
        String type,
        String name,
        String serviceName,
        String stype,
        Map attrMap,
        String amOrgName,
        String amsdkDN
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        SchemaType schematype = new SchemaType(stype);
        idServices.modifyService(ssoToken, idtype, name, serviceName,
            schematype, attrMap, amOrgName, amsdkDN);
    }

    public void removeAttributes_idrepo(
        String token,
        String type,
        String name,
        Set attrNames,
        String amOrgName,
        String amsdkDN
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        idServices.removeAttributes(ssoToken, idtype, name, attrNames,
            amOrgName, amsdkDN);
    }

    public Map search1_idrepo(
        String token,
        String type,
        String pattern,
        Map avPairs,
        boolean recursive,
        int maxResults,
        int maxTime,
        Set returnAttrs,
        String amOrgName
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        return search2_idrepo(token, type, pattern, maxTime, maxResults,
            returnAttrs, (returnAttrs == null), 0, avPairs, recursive,
            amOrgName);
    }

    public Map search2_idrepo(
        String token,
        String type,
        String pattern,
        int maxTime,
        int maxResults,
        Set returnAttrs,
        boolean returnAllAttrs,
        int filterOp,
        Map avPairs,
        boolean recursive,
        String amOrgName
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        IdSearchControl ctrl = new IdSearchControl();
        ctrl.setAllReturnAttributes(returnAllAttrs);
        ctrl.setMaxResults(maxResults);
        ctrl.setReturnAttributes(returnAttrs);
        ctrl.setTimeOut(maxTime);
        IdSearchOpModifier modifier = (filterOp == IdRepo.OR_MOD) ?
            IdSearchOpModifier.OR : IdSearchOpModifier.AND;
        ctrl.setSearchModifiers(modifier, avPairs);
        CrestQuery crestQuery = new CrestQuery(pattern);
        return IdSearchResultsToMap(idServices.search(ssoToken, idtype, ctrl, amOrgName, crestQuery));
    }

    public void setAttributes_idrepo(
        String token,
        String type,
        String name,
        Map attributes,
        boolean isAdd,
        String amOrgName,
        String amsdkDN
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        idServices.setAttributes(ssoToken, idtype, name, attributes, isAdd,
            amOrgName, amsdkDN, true);
    }

    public void setAttributes2_idrepo(
        String token,
        String type,
        String name,
        Map attributes,
        boolean isAdd,
        String amOrgName,
        String amsdkDN,
        boolean isString
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        if (!isString) {
            Map<String, byte[][]> binaryAttributes = new HashMap<String, byte[][]>(attributes.size());
            for (Map.Entry<String, Set<String>> entry : ((Map<String, Set<String>>) attributes).entrySet()) {
                Set<String> stringValues = entry.getValue();
                byte[][] values = new byte[stringValues.size()][];
                int counter = 0;
                for (String value : stringValues) {
                    values[counter++] = Base64.decode(value);
                }
                binaryAttributes.put(entry.getKey(), values);
            }
            attributes = binaryAttributes;
        }
        idServices.setAttributes(ssoToken, idtype, name, attributes, isAdd,
            amOrgName, amsdkDN, isString);
    }

    public void changePassword_idrepo(
        String token,
        String type,
        String name,
        String oldPassword,
        String newPassword,
        String amOrgName,
        String amsdkDN
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        idServices.changePassword(ssoToken, idtype, name, oldPassword,
            newPassword, amOrgName, amsdkDN);
    }

    public void unassignService_idrepo(
        String token,
        String type,
        String name,
        String serviceName,
        Map attrMap,
        String amOrgName,
        String amsdkDN
    ) throws RemoteException, IdRepoException, SSOException {
        SSOToken ssoToken = getSSOToken(token);
        IdType idtype = IdUtils.getType(type);
        idServices.unassignService(ssoToken, idtype, name, serviceName,
            attrMap, amOrgName, amsdkDN);

    }

    public void deRegisterNotificationURL_idrepo(String notificationID) throws RemoteException {
        synchronized (idRepoNotificationURLs) {
            URL url = idRepoNotificationURLs.remove(notificationID);
            if (url != null && idRepoDebug.messageEnabled()) {
                idRepoDebug.message("IdRepoJAXRPCObjectImpl.deRegisterNotificationURL_idrepo() - URL "
                        + url + " de-registered for ID " + notificationID);
            }
        }
    }

    /**
     * Returns the notification event XMLs for changed objects in the past N+2 minutes.
     *
     * @param time The number of minutes we should retrieve the changed objects for.
     * @return Returns the notification XMLs for the current minute, for the requested N minute, and also for 2
     * additional minutes.
     * @throws RemoteException If there was an error while collecting changed objects.
     */
    public Set<String> objectsChanged_idrepo(int time) throws RemoteException {
        initializeCacheSize();
        Set<String> answer = collectChangesFromCache(time, idrepoCache);

        if (idRepoDebug.messageEnabled()) {
            idRepoDebug.message("IdRepoJAXRPCObjectImpl.objectsChanged in time: " + time + "+2 minutes:\n" + answer);
        }
        return answer;
    }

    /**
     * Collects the notification XMLs for a given time period from the provided cache.
     *
     * @param time The number of minutes we should retrieve the changed objects for.
     * @param cache The cache we need to collect the notifications from.
     * @return Returns the notification XMLs for the current minute, for the requested N minute, and also for 2
     * additional minutes.
     */
    protected Set<String> collectChangesFromCache(int time, ConcurrentSkipListMap<Long, Set<String>> cache) {
        Set<String> answer = new HashSet<String>();
        if (cacheSize > 0) {

            long cacheIndex = getCacheIndex();

            //Return changes for the previous time + 2 minutes
            for (Set<String> modDNs : idrepoCache.tailMap(cacheIndex - time - 2).values()) {
                //This is safe since modDNs is backed by a ConcurrentHashMap
                answer.addAll(modDNs);
            }
        }
        return answer;
    }

    public String registerNotificationURL_idrepo(String url) throws RemoteException {
        return registerNotificationURL(url, idRepoNotificationURLs);
    }

    protected String registerNotificationURL(String url, Map<String, URL> notificationURLs) {

        // Default value if there are any issues with the registration process.
        String id = "0";
        try {
            // Check URL is not the local server
            if (!isClientOnSameServer(url)) {
                synchronized (notificationURLs) {
                    URL notificationUrl = new URL(url);
                    // Don't add the URL again if we already have it registered
                    boolean alreadyRegistered = false;
                    for (Map.Entry<String, URL> entry : notificationURLs.entrySet()) {
                        if (notificationUrl.toExternalForm().equals(entry.getValue().toExternalForm())) {
                            // This allows us to return the existing entry ID to support clients being able to
                            // de-register the correct entry.
                            id = entry.getKey();
                            alreadyRegistered = true;
                            if (idRepoDebug.messageEnabled()) {
                                idRepoDebug.message("IdRepoJAXRPCObjectImpl.registerNotificationURL() - URL "
                                        + url + " already registered, returning existing ID " + id);
                            }
                            break;
                        }
                    }
                    // If we didn't find the url in our list, add it
                    if (!alreadyRegistered) {
                        String serverID = "";
                        try {
                            serverID = WebtopNaming.getAMServerID();
                        } catch (ServerEntryNotFoundException e) {
                            if (idRepoDebug.messageEnabled()) {
                                idRepoDebug.message("IdRepoJAXRPCObjectImpl.registerNotificationURL - " +
                                        "had a problem getting our serverID ", e);
                            }
                        }
                        // Generate a unique value that includes the serverID to have a better chance of being unique
                        // in a cluster should a de-register request end up on the wrong server.
                        id = SMSUtils.getUniqueID() + "_" + serverID;
                        notificationURLs.put(id, notificationUrl);
                        if (idRepoDebug.messageEnabled()) {
                            idRepoDebug.message("IdRepoJAXRPCObjectImpl.registerNotificationURL - " +
                                    "registered notification URL: " + url + " with ID " + id);
                        }
                    }
                }
            } else {
                // Cannot add this server for notifications
                if (idRepoDebug.warningEnabled()) {
                    idRepoDebug.warning("IdRepoJAXRPCObjectImpl.registerNotificationURL "
                        + "cannot add local server: " + url);
                }
            }
        } catch (MalformedURLException e) {
            if (idRepoDebug.warningEnabled()) {
                idRepoDebug.warning("IdRepoJAXRPCObjectImpl." +
                    "registerNotificationURL invalid URL: " + url, e);
            }
        }
        return id;
    }

     public Map getSpecialIdentities_idrepo(
         String token,
         String type,
         String amOrgName
     ) throws RemoteException, IdRepoException, SSOException {
         SSOToken ssoToken = getSSOToken(token);
         IdType idtype = IdUtils.getType(type);
         IdSearchResults result = idServices.getSpecialIdentities(
             ssoToken, idtype, amOrgName);
         return IdSearchResultsToMap(result);
     }


    // Implementation to process entry changed events
    protected static void processEntryChanged_idrepo(
        String method, String name, int type, Set attrNames) {
        if (idRepoDebug.messageEnabled()) {
            idRepoDebug.message("IdRepoJAXRPCObjectImpl.processEntryChaged "
                + "method processing method: " + method + " name: " + name +
                " type: " + type + " attrName: " + attrNames);
        }
        initializeCacheSize();
        // Return if cache size is 0 or there are no remote clients
        if (cacheSize == 0 && idRepoNotificationURLs.isEmpty()) {
            if (idRepoDebug.messageEnabled()) {
                idRepoDebug.message("IdRepoJAXRPCObjectImpl." +
                    "processEntryChaged No registered notification URLs: " +
                    idRepoNotificationURLs + " and cache size is: " +
                    cacheSize);
            }
            return;
        }

         // Construct the XML document for the event change
        StringBuilder sb = new StringBuilder(100);
        sb.append("<EventNotification><AttributeValuePair>")
            .append("<Attribute name=\"method\" /><Value>").append(method)
            .append("</Value></AttributeValuePair>")
            .append("<AttributeValuePair><Attribute name=\"entityName\" />")
            .append("<Value>").append(XMLUtils.escapeSpecialCharacters(name))
            .append("</Value></AttributeValuePair>");

        if (method.equalsIgnoreCase("objectChanged") ||
            method.equalsIgnoreCase("objectsChanged")) {
            sb.append("<AttributeValuePair><Attribute name=\"eventType\" />")
                .append("<Value>").append(type).append(
                "</Value></AttributeValuePair>");
            if (method.equalsIgnoreCase("objectsChanged")) {
                sb.append("<AttributeValuePair><Attribute ").append(
                    "name=\"attrNames\"/>");
                for (Iterator items = attrNames.iterator(); items.hasNext();) {
                    String attr = (String) items.next();
                    sb.append("<Value>").append(attr).append("</Value>");
                }
                sb.append("</AttributeValuePair>");
            }
        }
        sb.append("</EventNotification>");

        // Update cache for polling by remote clients
        if (cacheSize > 0) {
            Set<String> modDNs = getCachedValues(idrepoCache);

            // Add to cache
            modDNs.add(sb.toString());
            if (idRepoDebug.messageEnabled()) {
                idRepoDebug.message("IdRepoJAXRPCObjectImpl.processing " +
                    "entry change:" + sb.toString());
            }
        }

        // If notification URLs are present, send notifications
        if (idRepoDebug.messageEnabled()) {
            idRepoDebug.message("IdRepoJAXRPCObjectImpl.processEntryChaged =" +
                " notificationURLS " + idRepoNotificationURLs.values());
        }
        NotificationSet ns = null;
        synchronized (idRepoNotificationURLs) {
            for (Map.Entry<String, URL> entry : idRepoNotificationURLs.entrySet()) {
                String id = entry.getKey();
                URL url = entry.getValue();

                // Construct NotificationSet
                if (ns == null) {
                    Notification notification =
                        new Notification(sb.toString());
                    ns = new NotificationSet(IDREPO_SERVICE);
                    ns.addNotification(notification);
                }
                try {
                    PLLServer.send(url, ns);
                    if (idRepoDebug.messageEnabled()) {
                        idRepoDebug.message("IdRepoJAXRPCObjectImpl:" +
                            "sentNotification URL: " + url + " Data: " + ns);
                    }
                } catch (SendNotificationException ne) {
                    if (idRepoDebug.warningEnabled()) {
                        idRepoDebug.warning("IdRepoJAXRPCObjectImpl: failed "
                            + "sending notification to: " + url + "\nRemoving "
                            + "URL from notification list.", ne);
                    }
                    // Remove the URL from Notification List
                    idRepoNotificationURLs.remove(id);
                }
            }
        }
    }

    /**
     * Returns the cached notification XMLs for the current cacheIndex. If there is nothing cached for this minute,
     * this will create a new cache entry and return the corresponding Set.
     *
     * @param cache The cache that stores the notification XMLs per minute.
     * @return A Set that corresponds to the current cacheIndex.
     */
    protected static Set<String> getCachedValues(ConcurrentSkipListMap<Long, Set<String>> cache) {
        // Obtain the cache index
        Long cacheIndex = getCacheIndex();
        Set<String> modDNs = cache.get(cacheIndex);
        if (modDNs == null) {
            modDNs = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
            Set<String> previousValue = cache.putIfAbsent(cacheIndex, modDNs);
            if (previousValue == null) {
                //remove old entries from the map to prevent memory leak
                cache.headMap(cacheIndex - cacheSize, true).clear();
            } else {
                modDNs = previousValue;
            }
        }
        return modDNs;
    }

    protected static long getCacheIndex() {
        return currentTimeMillis() / 60000;
    }

    private Map IdSearchResultsToMap(IdSearchResults res) {
        // TODO ..check if the Map gets properly populated and sent.
        Map answer = new HashMap();
        Map attrMaps = new HashMap();
        Set idStrings = new HashSet();

        Map answer1 = res.getResultAttributes();
        Set ids = res.getSearchResults();
        if (ids != null) {
            Iterator it = ids.iterator();
            while (it.hasNext()) {
                AMIdentity id = (AMIdentity) it.next();
                String idStr = IdUtils.getUniversalId(id);
                idStrings.add(idStr);
                Map attrMap = (Map) answer1.get(id);
                if (attrMap != null) {
                    Map cattrMap = new HashMap();
                    for (Iterator items = attrMap.keySet().iterator();
                        items.hasNext();) {
                        Object item = items.next();
                        Set values = (Set)attrMap.get(item);
                        values = XMLUtils.encodeAttributeSet(values,
                            idRepoDebug);
                        cattrMap.put(item.toString(), values);
                    }
                    attrMaps.put(idStr, cattrMap);
                }
            }
        }
        answer.put(IdRemoteServicesImpl.AMSR_RESULTS, idStrings);
        answer.put(IdRemoteServicesImpl.AMSR_CODE,
            new Integer(res.getErrorCode()));
        answer.put(IdRemoteServicesImpl.AMSR_ATTRS, attrMaps);
        return (answer);
    }

    /**
     * Check if agent token ID is appended to the token string.
     * if yes, we use it as a restriction context. This is meant
     * for cookie hijacking feature where agent appends the agent token ID
     * to the user sso token before sending it over to the server for
     * validation.
     */
    protected SSOToken getSSOToken(String token) throws SSOException {
        // Initalize the class variables
        initialize_idrepo();

        int index = token.indexOf(" ");
        if (tokenManager == null) {
            tokenManager = SSOTokenManager.getInstance();
        }
        if (index == -1) {
            return tokenManager.createSSOToken(token);
        }

        SSOToken stoken = null;
        String agentTokenStr = token.substring(index +1);
        String tokenStr = token.substring(0,index);
        final String ftoken = tokenStr;

        try {
            /*
             * for 7.0 patch-4 agent, IP address maybe send back to server.
             * this is a very simple check for IP Address
             */
            Object context = null;
            if (agentTokenStr.indexOf('.') != -1) {
                try {
                    context = InetAddress.getByName(agentTokenStr);
                } catch (Exception e) {
                    context = tokenManager.createSSOToken(agentTokenStr);
                }
            } else {
                context = tokenManager.createSSOToken(agentTokenStr);
            }
            stoken = (SSOToken)RestrictedTokenContext.doUsing(context,
                new RestrictedTokenAction() {
                    public Object run() throws Exception {
                        return tokenManager.createSSOToken(ftoken);
                    }
            });
       } catch (SSOException e) {
           idRepoDebug.error("IdRepoJAXRPCObjectImpl:getSSOToken", e);
           return tokenManager.createSSOToken(tokenStr);
       } catch (Exception e) {
           idRepoDebug.error("IdRepoJAXRPCObjectImpl:getSSOToken", e);
       }
       return stoken;
   }
}
