/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2006 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: ResourceResultCache.java,v 1.21 2010/01/21 22:18:01 dillidorai Exp $
 *
 * Portions Copyrighted 2015-2016 ForgeRock AS.
 */

package com.sun.identity.policy.client;

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

import com.iplanet.am.util.Cache;
import com.iplanet.am.util.SystemProperties;
import com.iplanet.dpro.session.Session;
import com.iplanet.dpro.session.SessionException;
import com.iplanet.dpro.session.SessionID;
import com.iplanet.services.comm.client.AlreadyRegisteredException;
import com.iplanet.services.comm.client.PLLClient;
import com.iplanet.services.comm.client.SendRequestException;
import com.iplanet.services.comm.share.Request;
import com.iplanet.services.comm.share.RequestSet;
import com.iplanet.services.comm.share.Response;
import com.iplanet.services.naming.URLNotFoundException;
import com.iplanet.services.naming.WebtopNaming;
import com.iplanet.sso.SSOException;
import com.iplanet.sso.SSOToken;
import com.iplanet.sso.SSOTokenEvent;
import com.iplanet.sso.SSOTokenListener;
import com.iplanet.sso.SSOTokenListenersUnsupportedException;
import com.iplanet.sso.SSOTokenManager;
import com.sun.identity.common.HttpURLConnectionManager;
import com.sun.identity.idm.AMIdentity;
import com.sun.identity.idm.IdUtils;
import com.sun.identity.policy.ActionDecision;
import com.sun.identity.policy.PolicyDecision;
import com.sun.identity.policy.PolicyException;
import com.sun.identity.policy.PolicyUtils;
import com.sun.identity.policy.ResBundleUtils;
import com.sun.identity.policy.ResourceMatch;
import com.sun.identity.policy.ResourceResult;
import com.sun.identity.policy.interfaces.ResourceName;
import com.sun.identity.policy.remote.AdvicesHandleableByAMRequest;
import com.sun.identity.policy.remote.AdvicesHandleableByAMResponse;
import com.sun.identity.policy.remote.PolicyChangeNotification;
import com.sun.identity.policy.remote.PolicyEvaluationException;
import com.sun.identity.policy.remote.PolicyListenerRequest;
import com.sun.identity.policy.remote.PolicyNotification;
import com.sun.identity.policy.remote.PolicyRequest;
import com.sun.identity.policy.remote.PolicyResponse;
import com.sun.identity.policy.remote.PolicyService;
import com.sun.identity.policy.remote.RemoveListenerRequest;
import com.sun.identity.policy.remote.ResourceResultRequest;
import com.sun.identity.shared.Constants;
import com.sun.identity.shared.JSONUtils;
import com.sun.identity.shared.debug.Debug;
import com.sun.identity.shared.encode.Hash;
import org.forgerock.openam.session.SessionCache;
import org.forgerock.openam.session.SessionCookies;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

/**
 * Singleton class that implements client side policy decision cache.
 * Handles communication with policy service acting 
 * as a proxy to policy service.  In effect, this is a caching proxy.
 */
class ResourceResultCache implements SSOTokenListener {

    //service>resource>tokenID>scope>result
    private static ResourceResultCache resourceResultCache;

    private PolicyProperties policyProperties;
    private Set remotePolicyListeners 
            = Collections.synchronizedSet(new HashSet(10));

    //serviceName -> resourceName -> sessionId -> scope -> result
    private Map resultCache = new HashMap(10); 

    private PolicyNotificationHandler notificationHandler;
    private Set tokenRegistry = 
        Collections.synchronizedSet(new HashSet(10000));
    private int        cacheTtl;
    private Set        advicesHandleableByAM; 

    private static Debug debug = PolicyEvaluator.debug;

    private static final String POLICY_SERVICE_ID_FOR_NAMING = "policy";

    private static final String POLICY_SERVICE = "policyservice";
    private static final String REST_POLICY_SERVICE = "ws/1/entitlement/entitlement";
    private static final String REST_POLICY_SERVICE_LISTENER = "ws/1/entitlement/listener";
    private static final String REST_LISTENER_NOTIFICATION_URL = "url";
    
    private static final String IPLANET_AM_WEB_AGENT_SERVICE = "iPlanetAMWebAgentService";

    private static final String REST_QUERY_REALM = "realm";
    private static final String REST_QUERY_APPLICATION = "application";
    private static final String REST_QUERY_SUBJECT = "subject";
    private static final String REST_QUERY_RESOURCE = "resource";
    private static final String REST_QUERY_RESOURCES = "resources";
    private static final String REST_QUERY_ACTION = "actionName";
    private static final String REST_QUERY_ENV = "env";

    private static final String JSON_RESOURCE_NAME = "resourceName";
    private static final String JSON_ACTIONS_VALUES = "actionsValues";
    private static final String JSON_ADVICES = "advices";
    private static final String JSON_ATTRIBUTES = "attributes";

    private static final String GET_RESPONSE_ATTRIBUTES 
            = "Get_Response_Attributes";

    private static final SessionCache sessionCache = SessionCache.getInstance();
    private static final SessionCookies sessionCookies = SessionCookies.getInstance();

    private static long requestID = 0;
    private static String REQUEST_ID_LOCK = "REQUEST_ID_LOCK";
    private static String SECRET_MASK = "*********";

    /**
     * Constructs the singleton instance of <code>ResourceResultCache</code>
     * 
     * @param policyProperties object that provides access to configuration
     *        properties such as policy service URL, notification URL etc.
     *        This is nice wrapper over
     *        <code>com.iplanet.am.util.SystemProperties</code>
     */
    private ResourceResultCache(PolicyProperties policyProperties) 
            throws PolicyException {
        this.policyProperties = policyProperties;
        notificationHandler = new PolicyNotificationHandler(this);
        cacheTtl = policyProperties.getCacheTtl();

        if (policyProperties.notificationEnabled()){
            //register notification handler with PLLClient
            registerHandlerWithPLLClient(notificationHandler);
            if (debug.messageEnabled()) {
                debug.message( "RsourceResultCache():"
                        + "added policyNotificationHandler "
                        + "with PLLClient");
            }
        }

        if (debug.messageEnabled()) {
            debug.message( "RsourceResultCache():"
                    + "Singleton Instance Created");
        }
    }

    /**
     * Returns reference to the singleton instance of 
     * <code>ResourceResultCache</code>
     * 
     * @param policyProperties object that provides access to configuration
     *        properties such as policy service URL, notification URL etc.
     *        This is nice wrapper over
     *        <code>com.iplanet.am.util.SystemProperties</code>
     *
     * @return reference to the singleton instance of 
     *         <code>ResourceResultCache</code>
     */
    synchronized static ResourceResultCache getInstance(
            PolicyProperties policyProperties) throws PolicyException {
        if (resourceResultCache == null) {
            resourceResultCache = new ResourceResultCache(policyProperties);
        }  else {
            resourceResultCache.policyProperties = policyProperties;
            resourceResultCache.cacheTtl = policyProperties.getCacheTtl();
        }
        return resourceResultCache;
    } 

    /**
     * Returns reference to the singleton instance of 
     * <code>ResourceResultCache</code>
     * 
     * @return reference to the singleton instance of 
     *         <code>ResourceResultCache</code>
     */
    private synchronized static ResourceResultCache getInstance() {
        if ( (resourceResultCache == null) 
                && debug.warningEnabled()) {
            debug.warning("ResourceResultCache.getInstance():"
                    + "ResourceResultCache has not been created:"
                    + "returning null");
           
        }
        return resourceResultCache;
    } 
    


    /**
     * Returns policy decision
     * @param appToken application sso token to identify the client to policy
     * service
     * @param serviceName name of service for which to get policy decision
     * @param token session token of user for whom to get policy decision
     * @param resourceName resource name for which to get policy decision
     * @param actionNames action names for which to get policy decision
     * @param env environment map to use to get policy decision
     * @param retryCount try this many times before giving up if received policy
     * decision is found to have expired
     * @return policy decision
     * @throws PolicyException if can not get policy decision
     * @throws SSOException if user session token is not valid
     * @throws InvalidAppSSOTokenException if application session token 
     * is not valid
     */ 
    PolicyDecision getPolicyDecision(SSOToken appToken, String serviceName,
            SSOToken token, String resourceName, Set actionNames,
            Map env, int retryCount) 
            throws InvalidAppSSOTokenException, 
            PolicyException, SSOException {
        int count = 0;
        boolean validTtl = false;
        PolicyDecision pd = getPolicyDecision(appToken, serviceName, 
                token, resourceName, actionNames, 
                env, true); //use cache
        if (pd.getTimeToLive() > currentTimeMillis()) {
            validTtl = true;
        }
        while (!validTtl && (count < retryCount)) {
            count++;
            if (debug.messageEnabled()) {
                debug.message("ResourceResultCache.getPolicyDecision():"
                        + "Received expired decision, "
                        + "Getting decision again, repeat attempt=" 
                        + count);
            }
            pd = getPolicyDecision(appToken, serviceName, 
                    token, resourceName, actionNames, 
                    env, false);  //do not use cache
            if (pd.getTimeToLive() > currentTimeMillis()) {
                validTtl = true;
                break;
            }
        }
        if (!validTtl) {
            if (debug.warningEnabled()) {
                debug.warning("ResourceResultCache.getPolicyDecision():"
                        + "Received expired decision from server");
            }
            Object[] args = {resourceName};
            throw new PolicyEvaluationException(ResBundleUtils.rbName,
                "received_expired_decision", args, null);
        }

        if (actionNames != null) {
            PolicyDecision pd1 = new PolicyDecision();
            Iterator nameIter = actionNames.iterator();
            while (nameIter.hasNext()) {
                String actionName = (String)nameIter.next();
                Map actionDecisions = pd.getActionDecisions();
                ActionDecision ad =
                        (ActionDecision)actionDecisions.get(actionName);
                if (ad != null) {
                    pd1.addActionDecision(ad);
                }
            }
            Map mergedReponseAttrsMap = new HashMap();
            PolicyUtils.appendMapToMap(pd.getResponseAttributes(), 
                    mergedReponseAttrsMap);
            pd1.setResponseAttributes(mergedReponseAttrsMap);
            pd = pd1;
        } else {
            pd = (PolicyDecision)pd.clone();
        }

        return pd;
    }

    /**
     * Returns policy decision
     * @param appToken application sso token to identify the client to policy
     * service
     *
     * @param serviceName name of service for which to get policy decision
     * @param token session token of user for whom to get policy decision
     * @param resourceName resource name for which to get policy decision
     * @param actionNames action names for which to get policy decision
     * @param env environment map to use to get policy decision
     *
     * @param useCache flag indicating whether to return a locally cached 
     * policy decision.  Locally cached decision is returned only if the 
     * value is <code>true</code>. Otherwise, policy decision is fetched 
     * from policy service and returned.
     *
     * @return policy decision
     * @throws PolicyException if can not get policy decision
     * @throws SSOException if session token is not valid
     */ 
    private PolicyDecision getPolicyDecision(SSOToken appToken, 
            String serviceName, SSOToken token, String resourceName, 
            Set actionNames, Map env, boolean useCache) 
            throws InvalidAppSSOTokenException, 
            PolicyException, SSOException {

        String cacheMode = policyProperties.getCacheMode();
        String rootResourceName = resourceName;
        if (PolicyProperties.SUBTREE.equals(cacheMode)) {
            rootResourceName = getRootResourceName(resourceName, serviceName);
            if (debug.messageEnabled()) {
                debug.message("ResourceResultCache.getPolicyDecision():"
                        + "resourceName=" + resourceName
                        + ":cacheMode=" + cacheMode
                        + ":would get resource results for root resource="
                        + rootResourceName);
            }
        }

        Set resourceResults = getResourceResults(appToken, serviceName, 
                token, rootResourceName, actionNames, env, cacheMode, useCache);
        ResourceName resourceComparator =
                (ResourceName)policyProperties.getResourceComparator(
                serviceName);
        PolicyDecision pd = getPolicyDecisionFromResourceResults(
                resourceResults, resourceName, resourceComparator, serviceName);
        if (debug.messageEnabled()) {
            debug.message("ResourceResultCache.getPolicyDecision():"
                    + ":serviceName=" + serviceName 
                    + ":token=" + token.getPrincipal().getName() 
                    + ":resourceName=" + resourceName 
                    + ":actionNames=" + actionNames + ":env" 
                    + ":cacehMode=" + cacheMode
                    + ":useCache=" + useCache
                    + ":returning policyDecision:" + pd);
        }
        return pd;
    }

    /**
     * Returns a set of <code>ResourceResult</code> objects
     * @param appToken application sso token to identify the client to policy
     * service
     *
     * @param serviceName name of service for which to get resource results
     * @param token session token of user for whom to get  resource results
     * @param resourceName resource name for which to get  resource results
     * @param actionNames action names for which to get  resource results
     * @param env environment map to use to get resource results
     * @param scope the scope to be used while getting resource results
     * @return a set of <code>ResourceResult</code> objects
     *
     * @throws PolicyException if can not get 
     * @throws SSOException if session token is not valid
     * @throws InvalidAppSSOTokenException if application session token 
     * is not valid
     */ 
    private Set getResourceResults(SSOToken appToken, String serviceName,
            SSOToken token, String resourceName, Set actionNames,
            Map env, String scope) 
            throws InvalidAppSSOTokenException, 
            PolicyException, SSOException {
        return getResourceResults(appToken, serviceName, 
                token, resourceName, actionNames, env, scope,
                true); //useCache
    }

    /**
     * Returns a set of <code>ResourceResult</code> objects
     * @param appToken application sso token to identify the client to policy
     * service
     *
     * @param serviceName name of service for which to get resource results
     * @param token session token of user for whom to get resource results
     * @param resourceName resource name for which to get resource results
     * @param actionNames action names for which to get resource results
     * @param env environment map to use to get resource results
     * @param scope the scope to be used while getting resource results
     * @param useCache flag indicating whether to return  locally cached 
     * resource results.  Locally cached resource results are 
     * returned only if the value is <code>true</code>
     *
     * @return a set of <code>ResourceResult</code>
     *
     * @throws PolicyException if can not get resource results
     * @throws SSOException if session token is not valid
     * @throws InvalidAppSSOTokenException if application session token 
     * is not valid
     */ 
    private Set getResourceResults(SSOToken appToken, String serviceName,
            SSOToken token, String resourceName, Set actionNames,
            Map env, String scope, boolean useCache) 
            throws InvalidAppSSOTokenException, 
            PolicyException, SSOException {
        SSOTokenManager.getInstance().validateToken(token);
        String cacheMode = policyProperties.getCacheMode();
        Set resourceResults = null;
        if (debug.messageEnabled()) {
            debug.message("ResourceResultCache.getResourceResults():"
                    + ":serviceName=" + serviceName 
                    + ":token=" + token.getPrincipal().getName() 
                    + ":resourceName=" + resourceName 
                    + ":actionNames=" + actionNames + ":env" 
                    + ":useCache=" + useCache
                    + ":useRESTProtocol()=" + policyProperties.useRESTProtocol()
                    + ":entering ");
        }

        Map resourceTokenIDsMap = null;
        // resultCache -> serviceName -> resourceName -> sessionId -> scope -> result
        synchronized(resultCache) {
            // resourceName -> sessionId -> scope -> result
            resourceTokenIDsMap = (Map)resultCache.get(serviceName);
            if (resourceTokenIDsMap == null) {
                // changed to fix 4295 Policy cache causes frequent 
                // full gc or out of memory issues
                resourceTokenIDsMap 
                        = new Cache(policyProperties.getResultsCacheResourceCap());
                resultCache.put(serviceName, resourceTokenIDsMap);
            }
        }

        Map tokenIDScopesMap = null;
        // resourceTokenIDsMap -> resourceName -> sessionId -> scope -> result
        synchronized(resourceTokenIDsMap) {
            // sessionId -> scope -> result
            tokenIDScopesMap = (Map)resourceTokenIDsMap.get(resourceName);
            if (tokenIDScopesMap == null) {
                // changed to fix 4295 Policy cache causes frequent full 
                // gc or out of memory issues
                tokenIDScopesMap  
                        = new Cache(policyProperties.getResultsCacheSessionCap());
                resourceTokenIDsMap.put(resourceName, tokenIDScopesMap);
            }
        }

        Map scopeResultsMap= null;
        String tokenID =  token.getTokenID().toString();
        // tokenIDScopesMap -> sessionId -> scope -> result
        synchronized(tokenIDScopesMap) {
            scopeResultsMap = (Map)tokenIDScopesMap.get(tokenID);
            if (scopeResultsMap == null) {
                scopeResultsMap = new HashMap();
                tokenIDScopesMap.put(tokenID, scopeResultsMap);
                if (!tokenRegistry.contains(tokenID)) {
                    try {
                        token.addSSOTokenListener(this);
                        tokenRegistry.add(tokenID);
                    } catch (SSOTokenListenersUnsupportedException ex) {
                        // Catching exception to avoid adding tokenID to tokenRegistry
                        // scopeResultsMap will be populated and returned to the caller but not cached
                        debug.message("ResourceResultCache.getResourceResults(): could not add sso listener: {}", ex.getMessage());
                    }
                }
            }
        }

        Object[] results = null;
        boolean fetchResultsFromServer = false;
        // scopeResultsMap -> scope -> result
        synchronized(scopeResultsMap) {
            results = (Object[])scopeResultsMap.get(scope);
            if ( results == null) {

                //array elements:resourceResults, env, ttl, actionNames
                results = new Object[4];
                scopeResultsMap.put(scope, results);
            }

            if ( !useCache ) {
                if (debug.messageEnabled()) {
                    debug.message("ResourceResultCache.getResourceResults():"
                            + "would contact server since useCache is false");
                }
                fetchResultsFromServer = true;
            } else if (results[0] == null) {
                if (debug.messageEnabled()) {
                    debug.message("ResourceResultCache.getResourceResults():"
                            + "would contact server "
                            + " since results not in cache");
                }
                fetchResultsFromServer = true;
            } else if ((env == null) && (results[1] != null)) {
                if (debug.messageEnabled()) {
                    debug.message("ResourceResultCache.getResourceResults():"
                            + "would contact server since env does not match");
                }
                fetchResultsFromServer = true;
            } else if ((env != null) && !env.equals(results[1])) { 
                if (debug.messageEnabled()) {
                    debug.message("ResourceResultCache.getResourceResults():"
                            + "would contact server since env does not Match");
                }
                fetchResultsFromServer = true;
            } else if (((Long) results[2]).longValue()
                    < currentTimeMillis()) {
                if (debug.messageEnabled()) {
                    debug.message("ResourceResultCache.getResourceResults():"
                            + "would contact server since results ttl has "
                            + " expired");
                }
                fetchResultsFromServer = true;
            } else if ((actionNames == null) && (results[3] != null)) {
                if (debug.messageEnabled()) {
                    debug.message("ResourceResultCache.getResourceResults():"
                            + "would contact server since action names do not "
                            + " match");
                }
                fetchResultsFromServer = true;
            } else if ((actionNames != null) &&  (results[3] == null)) {
                if (debug.messageEnabled()) {
                    debug.message("ResourceResultCache.getResourceResults():"
                            + "would contact server since action names do not "
                            + " Match");
                }
                fetchResultsFromServer = true;
            } else if ((results[3] !=null) &&!((Set)results[3]).containsAll(
                    actionNames))  {
                if (debug.messageEnabled()) {
                    debug.message("ResourceResultCache.getResourceResults():"
                            + "would contact server since cached action names "
                            + " do not cover request action names");
                }
                fetchResultsFromServer = true;
            } else if (resourceResultsHasAdvices((Set)(results[0]))
                    && PolicyProperties.SELF.equals(cacheMode)) { 
                //get from server if there were advices in the cached decision
                //we do this only if cacheMode is self
                fetchResultsFromServer = true;

            }

        }

        // changed to fix 4205 Policy client code has bottleneck when processing notificati 
        // FIXME: remove the check for service name with the some fix on server
        if (fetchResultsFromServer) {
            if(policyProperties.useRESTProtocol() 
                    && IPLANET_AM_WEB_AGENT_SERVICE.equalsIgnoreCase(serviceName)) {
                resourceResults = getRESTResultsFromServer(appToken, 
                        serviceName, token, resourceName, scope, 
                        actionNames, env);
            } else {
                resourceResults = getResultsFromServer(appToken, 
                        serviceName, token, resourceName, scope, 
                        actionNames, env);
            }
            results[0] = resourceResults;

            if (env != null) {
                env = PolicyUtils.cloneMap(env);
            }
            results[1] = env;

            results[2] 
                    = new Long(currentTimeMillis() + cacheTtl);

            if (actionNames != null) {
                Set actionNames1 = actionNames;
                actionNames = new HashSet();
                actionNames.addAll(actionNames1);
            }
            results[3] = actionNames;
        } else {
            if (debug.messageEnabled()) {
                debug.message("ResourceResultCache.getResourceResults():"
                        + "would not contact server, "
                        + " would use results from  cache ");
            }
        }


        resourceResults = (Set)(results[0]);
        if (debug.messageEnabled()) {
            debug.message("ResourceResultCache.getResourceResults("
                    + serviceName + ","
                    + token.getPrincipal().getName() + "," 
                    + resourceName + ","
                    + actionNames + ",env)" 
                    + ": returning resourceResults");
        }
        return resourceResults;
    }

    private Set getRESTResultsFromServer(SSOToken appToken, String serviceName,
            SSOToken token, String resourceName, String scope, 
            Set actionNames, Map env) 
            throws InvalidAppSSOTokenException, SSOException,
            PolicyException {
        Set<ResourceResult> resourceResults = null;
        try {        
            AMIdentity userIdentity  = IdUtils.getIdentity(token);
            String restUrl = getRESTPolicyServiceURL(token, scope);
            String queryString = buildEntitlementRequestQueryString(
                "/", serviceName, token, resourceName, actionNames, env);
            restUrl = restUrl + "?" + queryString;
            if (debug.messageEnabled()) {
                debug.message("ResourceResultCache.getRESTResultsFromServer():"
                        + ":serviceName=" + serviceName 
                        + ":token=" + token.getPrincipal().getName() 
                        + ":resourceName=" + resourceName 
                        + ":scope=" + scope 
                        + ":actionNames=" + actionNames + ":env" 
                        + ":restUrl=" + restUrl
                        + ":entering");
            }
            String jsonString = getResourceContent(appToken, token, restUrl);
            if (debug.messageEnabled()) {
                debug.message("ResourceResultCache.getRESTResultsFromServer():"
                        + ":server response jsonString=" + jsonString); 
            }
            resourceResults = jsonResourceContentToResourceResults(
                jsonString, serviceName);
        } catch (InvalidAppSSOTokenException e) {
            throw e;
        } catch (Exception e) {
            String[] args = {e.getMessage()};
            throw new PolicyEvaluationException(
                    ResBundleUtils.rbName,
                    "rest_policy_request_exception",
                    args, e);
        }
        if (debug.messageEnabled()) {
            debug.message("ResourceResultCache.getRESTResultsFromServer():"
                    + "returning");
        }
        return resourceResults;
    }

    /**
     * Returns a set of <code>ResourceResult</code> objects from server.
     * Fresh resource results 
     * are fetched from policy server and returned.
     * @param appToken application sso token to identify the client to policy
     * service
     *
     * @param serviceName name of service for which to get resource results
     * @param token session token of user for whom to get resource results
     * @param resourceName resource name for which to get resource results
     * @param scope the scope to be used while getting resource results
     * @param actionNames action names for which to get resource results
     * @param env environment map to use to get resource results
     *
     * @return a set of <code>ResourceResult</code> objects
     *
     * @throws PolicyException if can not get resource results
     * @throws SSOException if session token is not valid
     * @throws InvalidAppSSOTokenException if application session token 
     * is not valid
     */ 
    private Set getResultsFromServer(SSOToken appToken, String serviceName,
            SSOToken token, String resourceName, String scope, 
            Set actionNames, Map env) 
            throws InvalidAppSSOTokenException, SSOException,
            PolicyException {
        Set resourceResults = null;
        Response response = null;
        try {        
            URL policyServiceUrl = getPolicyServiceURL(token);
            if (debug.messageEnabled()) {
                debug.message("ResourceResultCache.getResultsFromServer():"
                        + ":serviceName=" + serviceName 
                        + ":token=" + token.getPrincipal().getName() 
                        + ":resourceName=" + resourceName 
                        + ":scope=" + scope 
                        + ":actionNames=" + actionNames + ":env" 
                        + ":policyServiceURL=" + policyServiceUrl
                        + ":entering");
            }
            ResourceResultRequest rrRequest = new ResourceResultRequest();
            rrRequest.setServiceName(serviceName);
            rrRequest.setResourceName(resourceName);
            rrRequest.setResourceScope(scope);
            rrRequest.setUserSSOToken(token.getTokenID().toString());

            Set responseAttributes = null;
            if (env != null) {
                rrRequest.setEnvParms(env);
                responseAttributes = getResponseAttributes(env);
                if (debug.messageEnabled()) {
                    debug.message("ResourceResultCache.getResultsFromServer():"
                            + "responseAttributes to get="
                            + responseAttributes);
                }
                if (responseAttributes != null) {
                    rrRequest.setResponseAttributes(responseAttributes);
                }
            }

            PolicyRequest policyRequest = new PolicyRequest();
            policyRequest.setAppSSOToken(appToken.getTokenID().toString());
            policyRequest.setMethodID(
                    PolicyRequest.POLICY_REQUEST_GET_RESOURCE_RESULTS);
            policyRequest.setRequestId(newRequestID());
            policyRequest.setResourceResultRequest(rrRequest);

            PolicyService ps = sendPLLRequest(policyServiceUrl, policyRequest);
            if (ps != null) {
                PolicyResponse pr = ps.getPolicyResponse();
                String exceptionMessage = pr.getExceptionMsg();
                if (exceptionMessage != null) {
                    if(exceptionMessage.indexOf(
                                PolicyResponse.APP_SSO_TOKEN_INVALID) >= 0) {
                        if (debug.warningEnabled()) {
                            debug.warning("ResourceResultCache."
                                + "getResultsFromServer():"
                                + " response exception " + exceptionMessage);
                            debug.warning("ResourceResultCache."
                                + "getResultsFromServer():"
                                + " appSSOToken is invalid");
                            debug.warning("ResourceResultCache."
                                + "throwing InvalidAppSSOTokenException");
                        }

                        String[] args = {exceptionMessage};
                        throw new InvalidAppSSOTokenException(
                                ResBundleUtils.rbName,
                                "server_reported_invalid_app_sso_token", 
                                args, null);
                    } else {
                        debug.warning("ResourceResultCache."
                                + "getResultsFromServer():"
                                + "response exception message=" 
                                + exceptionMessage);
                        String[] args = {exceptionMessage};
                        throw new PolicyEvaluationException(
                                ResBundleUtils.rbName,
                                "server_reported_exception", 
                                args, null);
                    }
                } else {
                    resourceResults = pr.getResourceResults();
                }
            }
        } catch (SendRequestException sre) {
            String[] args = {sre.getMessage()};
            throw new PolicyEvaluationException(
                    ResBundleUtils.rbName,
                    "pll_send_request_exception",
                    args, sre);
        }
        if (debug.messageEnabled()) {
            debug.message("ResourceResultCache.getResultsFromServer():"
                    + "returning");
        }
        return resourceResults;
    }

    /**
     * Returns policy decision computed from a set of
     * <code>ResourceResult</code> objects
     *
     * @param resourceResults resource results used to compute policy decision
     * @param resourceName resource name for which to get policy decision
     * @param resourceComparator <code>ResourceName</code>, resource
     * comparison algorithm used to compare resources
     *
     * @return computed policy decision
     *
     * @throws PolicyException if can not get policy decision
     */ 
    private PolicyDecision getPolicyDecisionFromResourceResults(
            Set resourceResults, String resourceName, 
            ResourceName resourceComparator,
            String serviceName) throws PolicyException {

        PolicyDecision pd = new PolicyDecision();
        Iterator resultsIter = resourceResults.iterator();
        boolean processed = false;
        while (!processed && resultsIter.hasNext()) {
            ResourceResult resourceResult 
                    = (ResourceResult)resultsIter.next();
            processed = mergePolicyDecisions(pd, resourceResult,
                    resourceName, resourceComparator, serviceName);
        }
        return pd;
    }

    /**
     * Merges policy decisions applicable to a resource 
     * from a <code>ResourceResult</code> object.
     *
     * @param pd a collector for merged policy decision
     * @param resourceResult <code>ResourceResult</code> from which
     * to find applicable policy decisions
     * @param resourceName resource name for which to get policy decision
     * @param resourceComparator <code>ResourceName</code>, resource
     * comparison algorithm used to compare resources
     *
     * @param serviceName service name
     *
     * @return a flag indicating whether more <code>ResourceResult</code>
     * objects need to be visited to to compute the policy decision.
     * <code>true</code> is returned if no more <code>ResourceResult</code>
     * objects need to be visited
     * 
     *
     * a <code>ResourceResult</code> object.
     *
     * @throws PolicyException if can not get policy decision
     */ 
    private boolean mergePolicyDecisions(PolicyDecision pd, 
            ResourceResult resourceResult, String resourceName,
            ResourceName resourceComparator, String serviceName) 
            throws PolicyException {
        boolean processed = false;
        if (debug.messageEnabled()) {
            debug.message("ResourceResultCache.mergePolicyDecisions():"
                    + "resourceName=" + resourceName
                    + ":resourceResultResourceName="
                    + resourceResult.getResourceName());
        }

        ResourceMatch  result = resourceComparator.compare(resourceName,
            resourceResult.getResourceName(), true); //wild card compare
        if (result.equals(ResourceMatch.EXACT_MATCH)) {
            resetPolicyDecision(resourceResult.getPolicyDecision(), pd,
                    serviceName);
            processed = true;
        } else if (result.equals(ResourceMatch.WILDCARD_MATCH)) {
            mergePolicyDecisions(resourceResult.getPolicyDecision(), pd,
                    serviceName);
            if (pd.getTimeToLive() < currentTimeMillis()) {
                processed = true;
            }
            if (!processed) {
                Set resourceResults = resourceResult.getResourceResults();
                Iterator resultsIter = resourceResults.iterator();
                while (!processed && resultsIter.hasNext()) {
                    ResourceResult subResult 
                            = (ResourceResult)resultsIter.next();
                    processed = mergePolicyDecisions(pd, subResult,
                            resourceName, resourceComparator, serviceName);
                }
            }
        } else if (result.equals(ResourceMatch.SUPER_RESOURCE_MATCH)) {
                Set resourceResults = resourceResult.getResourceResults();
                Iterator resultsIter = resourceResults.iterator();
                while (!processed && resultsIter.hasNext()) {
                    ResourceResult subResult 
                            = (ResourceResult)resultsIter.next();
                    processed = mergePolicyDecisions(pd, subResult,
                            resourceName, resourceComparator, serviceName);
                }
        } // else NO_MATCH or SUBRESOURCE_MATCH nothing to do
        return processed;
    }


    /**
     * Merges two policy decisions
     * @param pd1 policy decision to be merged
     * @param pd2 policy decision to be merged into
     * @param serviceName service name
     * @return merged policy decision
     */
    private PolicyDecision mergePolicyDecisions(PolicyDecision pd1, 
            PolicyDecision pd2, String serviceName) { //pd2 is collector
      Map actionDecisions1 = pd1.getActionDecisions();
      Set actions = new HashSet();
      actions.addAll(actionDecisions1.keySet());
      Iterator iter = actions.iterator();
      while ( iter.hasNext() ) {
          String action = (String) iter.next();
          ActionDecision ad1 = (ActionDecision) actionDecisions1.get(action);
          pd2.addActionDecision(ad1, 
                policyProperties.getTrueValue(serviceName, action), 
                policyProperties.getFalseValue(serviceName, action));
      }
      Map mergedReponseAttrsMap = new HashMap();
      PolicyUtils.appendMapToMap(pd1.getResponseAttributes(), 
            mergedReponseAttrsMap);
      PolicyUtils.appendMapToMap(pd2.getResponseAttributes(), 
            mergedReponseAttrsMap);
      pd2.setResponseAttributes(mergedReponseAttrsMap);
      return pd2;
    }

    /**
     * Merges two policy decisions
     * @param pd1 policy decision to be merged
     * @param pd2 policy decision to be merged into. Action decisions 
     * present in the policy decision are cleared before merging
     * @param serviceName service name
     * @return merged policy decision
     */
    private PolicyDecision resetPolicyDecision(PolicyDecision pd1, 
            PolicyDecision pd2, String serviceName) { //pd2 is collector
      Map actionDecisions1 = pd1.getActionDecisions();
      Map actionDecisions2 = pd2.getActionDecisions();
      actionDecisions2.clear();
      Set actions = new HashSet();
      actions.addAll(actionDecisions1.keySet());
      Iterator iter = actions.iterator();
      while ( iter.hasNext() ) {
          String action = (String) iter.next();
          ActionDecision ad1 = (ActionDecision) actionDecisions1.get(action);
          pd2.addActionDecision(ad1, 
                policyProperties.getTrueValue(serviceName, action), 
                policyProperties.getFalseValue(serviceName, action));
      }
      Map mergedReponseAttrsMap = new HashMap();
      PolicyUtils.appendMapToMap(pd1.getResponseAttributes(), 
            mergedReponseAttrsMap);
      PolicyUtils.appendMapToMap(pd2.getResponseAttributes(), 
            mergedReponseAttrsMap);
      pd2.setResponseAttributes(mergedReponseAttrsMap);
      return pd2;
    }

    /**
     * Registers a listener with policy service to recieve
     * notifications on policy changes
     * @param appToken session token identifying the client
     * @param serviceName service name
     * @param notificationURL end point on the client that listens for
     * notifications
     */
    void addRemotePolicyListener(SSOToken appToken, 
            String serviceName, String notificationURL) {
        addRemotePolicyListener(appToken, serviceName, notificationURL, 
                false);
    }

    /**
     * Registers a listener with policy service to recieve
     * notifications on policy changes
     * @param appToken session token identifying the client
     * @param serviceName service name
     * @param notificationURL end point on the client that listens for
     * notifications
     *
     * @param reRegister flag indicating whether to register listener
     *  even if it was already registered. <code>true</code> indicates
     * to register listener again even if it was previously registered
     */
    boolean addRemotePolicyListener(SSOToken appToken, 
            String serviceName, String notificationURL, 
            boolean reRegister) {
        boolean  status = false;
        if (debug.messageEnabled()) {
            debug.message("ResourceResultCache.addRemotePolicyListener():"
                    + "serviceName=" + serviceName
                    + ":notificationURL=" + notificationURL);
        } 

        if (remotePolicyListeners.contains(serviceName)
                    && !reRegister) {
            if (debug.messageEnabled()) {
                debug.message("ResourceResultCache.addRemotePolicyListener():"
                        + "serviceName=" + serviceName
                        + ":notificationURL=" + notificationURL
                        + ":is already registered");
            } 
            return status;
        } //else do the following

        URL policyServiceURL = null;
        if (appToken != null) {
            try {
                policyServiceURL 
                        = getPolicyServiceURL(appToken);
            } catch (PolicyException pe) {
                debug.error("ResourceResultCache.addRemotePolicyListener():"
                        + "Can not add policy listner", pe);
            }
        }

        if ((appToken != null) && (policyServiceURL != null)) {
            PolicyListenerRequest listenerReq = new PolicyListenerRequest();
            listenerReq.setServiceName(serviceName);
            listenerReq.setNotificationURL(notificationURL);

            PolicyRequest policyReq = new PolicyRequest();
            policyReq.setAppSSOToken(appToken.getTokenID().toString());
            policyReq.setMethodID(
                    PolicyRequest.POLICY_REQUEST_ADD_POLICY_LISTENER);
            policyReq.setPolicyListenerRequest(listenerReq);

            try {
                PolicyService ps = sendPLLRequest(policyServiceURL, policyReq);
                if (ps != null) {
                    if (debug.messageEnabled()) {
                        debug.message("ResourceResultCache."
                                + "addRemotePolicyListener():"
                                + "result=" + ps.toXMLString());
                    } 
                    PolicyResponse psres = ps.getPolicyResponse();
                    if (psres.getMethodID() 
                            == PolicyResponse.POLICY_ADD_LISTENER_RESPONSE) {
                        status = true;
                        remotePolicyListeners.add(serviceName);
                        if (debug.messageEnabled()) {
                            debug.message("ResourceResultCache."
                                    + "addRemotePolicyListener():"
                                    + "serviceName=" + serviceName
                                    + ":notificationURL=" + notificationURL
                                    + ":policyServiceURL=" + policyServiceURL
                                    + ":add succeeded");
                        } 
                    } 
                } else {
                    debug.error("ResourceResultCache.addRemotePolicyListener():"
                            + " no result");
                } 
            } catch (Exception e) {
                debug.error("ResourceResultCache.addRemotePolicyListener():",e);
            } 
        } 
        return status;
    } 

    /**
     * Removes a listener registered with policy service to recieve
     * notifications on policy changes
     * @param appToken session token identifying the client
     * @param serviceName service name
     * @param notificationURL end point on the client that listens for
     * notifications
     */
     public boolean removeRemotePolicyListener(SSOToken appToken, 
            String serviceName, String notificationURL) {
        boolean status = false;
        URL policyServiceURL = null;
        remotePolicyListeners.remove(notificationURL);
        if (appToken != null) {
            try {
                policyServiceURL = getPolicyServiceURL(appToken);
            } catch (PolicyException pe) {
                debug.error("ResourceResultCache.removeRemotePolicyListener():"
                        + "Can not remove policy listner:", pe);
            }
        }

        if ((appToken != null) && (policyServiceURL != null)) {
            RemoveListenerRequest rmReq = new RemoveListenerRequest();

            rmReq.setServiceName(serviceName);
            rmReq.setNotificationURL(notificationURL);

            PolicyRequest policyReq = new PolicyRequest();
            policyReq.setAppSSOToken(appToken.getTokenID().toString());
            policyReq.setMethodID(
                    PolicyRequest.POLICY_REQUEST_REMOVE_POLICY_LISTENER);
            policyReq.setRemoveListenerRequest(rmReq);

            try {
                PolicyService ps = sendPLLRequest(policyServiceURL, policyReq);
                if (ps != null) {
                    if (debug.messageEnabled()) {
                        debug.message("ResourceResultCache."
                                + "removeRemotePolicyListener():"
                                + "result=" + ps.toXMLString());
                    } 
                    PolicyResponse psres = ps.getPolicyResponse();
                    if (psres.getMethodID() 
                            == PolicyResponse.POLICY_REMOVE_LISTENER_RESPONSE) {
                        status = true;
                    } 
                } else {
                    debug.message("ResourceResultCache."
                            + "removeRemotePolicyListener():"
                            + "no result");
                } 
            } catch (Exception e) {
                debug.error("ResourceResultCache.removeRemotePolicyListener():",
                    e);
            } 
        } 
        return status;
    }

    /**
     * Processes policy notifications forwarded from listener end 
     * point of policy client
     * @param pn policy notification
     */
    static void processPolicyNotification(PolicyNotification pn) 
            throws PolicyEvaluationException {
        if (pn != null) {
            if (debug.messageEnabled()) {
                debug.message("ResourceResultCache:processPolicyNotification():"
                        + pn);
            }
            ResourceResultCache cache = ResourceResultCache.getInstance();
            PolicyChangeNotification pcn = pn.getPolicyChangeNotification();
            String serviceName = pcn.getServiceName();
            if (serviceName != null) {
                if (cache.remotePolicyListeners.contains(serviceName)) {
                    Set affectedResourceNames = pcn.getResourceNames();
                    if (debug.messageEnabled()) {
                        debug.message("ResourceResultCache:"
                                + "processPolicyNotification():" 
                                + "serviceName=" + serviceName
                                + ":affectedResourceNames=" 
                                + affectedResourceNames
                                + ":clearing cache for affected "
                                + "resource names");
                    }
                    clearCacheForResourceNames(serviceName, 
                            affectedResourceNames);
                } else {
                    if (debug.messageEnabled()) {
                        debug.message("ResourceResultCache:"
                                + "processPolicyNotification():" 
                                + "serviceName not registered"
                                + ":no resource names cleared from cache");
                    }
                }
            } else {
                if (debug.messageEnabled()) {
                    debug.message("ResourceResultCache:"
                            + "processPolicyNotification():" 
                            + "serviceName is null"
                            + ":no resource names cleared from cache");
                }
            }
        } else {
            debug.error("ResourceResultCache.processPolicyNotification()" 
            + "PolicyNotification is null");
        } 
    } 

    /** 
     * Registers policy notification handler with <code>PLLClient</code>
     * @param handler policy notification handler
     */
    private void registerHandlerWithPLLClient(
            PolicyNotificationHandler handler) {
        try {
            PLLClient.addNotificationHandler(POLICY_SERVICE_ID_FOR_NAMING, 
                handler);
            if (debug.messageEnabled()) {
                debug.message("ResourceResultCache."
                        + "registerHandlerWithPLLClient():"
                        + "registered notification handler");
            }
        } catch (AlreadyRegisteredException ae) {
            if (debug.warningEnabled()) {
                debug.message("ResourceResultCache."
                        + "registerHandlerWithPLLClient():"
                        + "AlreadyRegisteredException", ae);
            }
        }
    }

   /**
    * Returns policy service URL based on session token
    * @param token session token of user
    * @return policy service URL based on session token
    * @throws PolicyException if can not get policy service URL
    */
    static URL getPolicyServiceURL(SSOToken token) throws
            PolicyException {
        URL policyServiceURL = null;
        try {
            String ssoTokenID = token.getTokenID().toString();
            SessionID sid = new SessionID(ssoTokenID);
            Session session = sessionCache.getSession(sid);
            URL sessionServiceURL = session.getSessionServiceURL();
            String protocol =  sessionServiceURL.getProtocol();
            String host = sessionServiceURL.getHost();
            int port = sessionServiceURL.getPort();
            String uri = sessionServiceURL.getPath();

            String portString = null;
            if ( port == -1) {
                portString = "";
            } else {
                portString = Integer.toString(port);
            }
            policyServiceURL = WebtopNaming.getServiceURL(
                POLICY_SERVICE_ID_FOR_NAMING,
                protocol, host, portString, uri);
        } catch (SessionException se) {
            debug.error("ResourceResultCache.getPolicyServiceURL():"
                    + "Can not find policy service URL", se);
            throw new PolicyEvaluationException(
                    ResBundleUtils.rbName,
                    "policy_service_url_not_found",
                    null, se);
        } catch (URLNotFoundException ue) {
            debug.error("ResourceResultCache.getPolicyServiceURL():"
                    + "Can not find policy service URL", ue);
            throw new PolicyEvaluationException(
                    ResBundleUtils.rbName,
                    "policy_service_url_not_found",
                    null, ue);
        }
        return policyServiceURL;
    }

    /**
     * Processes session token change ntofication
     * @param tokenEvent session token change notification event
     */
    public void ssoTokenChanged(SSOTokenEvent tokenEvent) {
        String tokenID = tokenEvent.getToken().getTokenID().toString();
        if (debug.messageEnabled()) {
            debug.message("ResourceResultCache.ssoTokenChanged():"
                + "for tokenID=" + SECRET_MASK); //mask tokenID
        }

        try {
            synchronized(resultCache) {
                Set services = (Set)resultCache.keySet();
                Iterator serviceIter = services.iterator();
                while (serviceIter.hasNext()) {
                    String serviceName = (String)serviceIter.next();
                    Map resourceTokenIDsMap = (Map)resultCache.get(serviceName);
                    synchronized(resourceTokenIDsMap) {
                        Set resources = (Set)resourceTokenIDsMap.keySet();
                        Iterator resourceIter = resources.iterator();
                        while (resourceIter.hasNext()) {
                            String resource = (String)resourceIter.next();
                            Map tokenIDScopesMap 
                                    = (Map)resourceTokenIDsMap.get(resource);
                            if (tokenIDScopesMap != null) {
                                tokenIDScopesMap.remove(tokenID);
                            }
                            
                            boolean tokenPresent = tokenRegistry.remove(tokenID);
                            if ( (tokenPresent == false) &&
                                (debug.messageEnabled()) ) {
                                debug.message("ResourceResultCache. tokenID= "
                                    + SECRET_MASK
                                    + " not found in Token Registry.");
                            }

                            if (debug.messageEnabled()) {
                                debug.message("ResourceResultCache."
                                    + "ssoTokenChanged():"
                                    + "removing cache results for "
                                    + "tokenID=" + SECRET_MASK //mask tokenID
                                    + ":serviceName=" + serviceName 
                                    + ":resource=" + resource);
                            }
                        }
                    }
                }
            }
        } catch (Throwable t) {
            if (debug.warningEnabled()) {
                debug.warning("ResourceResultCache.ssoTokenChanged():"
                    + "Exception caught", t);
            }
        }
    }

    /**
     * Clears cached decisions for a set of resources
     * @param serviceName service name
     * @param affectedResourceNames affected resource names
     */
    private static void clearCacheForResourceNames(String serviceName, 
            Set affectedResourceNames) {
        if ((affectedResourceNames == null) 
                || affectedResourceNames.isEmpty()) {
            return;
        }  

        Map resourceTokenIDsMap 
                = (Map)(resourceResultCache.resultCache).get(serviceName);
        if ((resourceTokenIDsMap == null)
                || resourceTokenIDsMap.isEmpty()) {
            return;
        } 

        ResourceName resourceComparator 
                = resourceResultCache.policyProperties
                .getResourceComparator(serviceName);
        Iterator arIter = affectedResourceNames.iterator();
        while (arIter.hasNext()) {
            String affectedRN = (String)arIter.next();
            if (debug.messageEnabled()) {
                debug.message("ResourceResultCache."
                        + "clearCacheForResourceNames():"
                        + "affectedResourceName=" + affectedRN);
            }
            synchronized (resourceTokenIDsMap) {
                Set cachedResourceNames = resourceTokenIDsMap.keySet();
                Iterator crIter = cachedResourceNames.iterator();
                while (crIter.hasNext()) {
                    String cachedRN = (String)crIter.next();
                    if (debug.messageEnabled()) {
                        debug.message("ResourceResultCache."
                                + "clearCacheForResourceNames():"
                                + "affectedResourceName=" + affectedRN
                                + ":cachedResourceName=" + cachedRN);
                    }
                    if (affectedRN.equals(cachedRN)) {
                        crIter.remove();
                        if (debug.messageEnabled()) {
                            debug.message("ResourceResultCache."
                                    + "clearCacheForResourceNames():"
                                    + "cleared cached results for "
                                    + "resourceName=" + cachedRN
                                    + ":affectedResourceName=" + affectedRN
                                    + ":match=SAME RESOURCE NAME");
                        }
                    } else {
                        ResourceMatch rm 
                                = resourceComparator.compare( cachedRN, 
                                affectedRN, true); //wildcard compare
                        if (rm.equals(ResourceMatch.EXACT_MATCH)) {
                            crIter.remove();
                            if (debug.messageEnabled()) {
                                debug.message("ResourceResultCache."
                                        + "clearCacheForResourceNames():"
                                        + "cleared cached results for "
                                        + "resourceName=" + cachedRN
                                        + ":affectedResourceName=" + affectedRN
                                        + ":match=EXACT_MATCH");
                            }
                        } else if (rm.equals(ResourceMatch.WILDCARD_MATCH)) {
                            crIter.remove();
                            if (debug.messageEnabled()) {
                                debug.message("ResourceResultCache."
                                        + "clearCacheForResourceNames():"
                                        + "cleared cached results for "
                                        + "resourceName=" + cachedRN
                                        + ":affectedResourceName=" + affectedRN
                                        + ":match=WILD_CARD_MATCH");
                            }
                        } else if (rm.equals(
                                ResourceMatch.SUB_RESOURCE_MATCH)) {
                            crIter.remove();
                            if (debug.messageEnabled()) {
                                debug.message("ResourceResultCache."
                                        + "clearCacheForResourceNames():"
                                        + "cleared cached results for "
                                        + "resourceName=" + cachedRN
                                        + ":affectedResourceName=" + affectedRN
                                        + ":match=SUB_RESOURCE_MACTH");
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Returns response attribute names specified in environment map
     * @param env environment map
     */
    private Set getResponseAttributes(Map env) {
        Set responseAttributes = null;
        if ( env != null) {
            responseAttributes = (Set) env.get(GET_RESPONSE_ATTRIBUTES);
        }
        return responseAttributes;
    }
    
    /** 
     * Returns a new request ID. Used in identifying request messages
     * sent to policy service
     * @return a new request ID. Used in identifying request messages
     * sent to policy service
     */
    private String newRequestID() {
        String requestIDString = null;
        synchronized(REQUEST_ID_LOCK) {
            requestIDString = String.valueOf(requestID++);
        }
        return requestIDString;
    }

    /**
     * Returns root resource name
     * @param resource resource name from which to compute root resource name
     * @param serviceName service name
     * @return root resource name computed from resource name
     */
    private String getRootResourceName(String resource, String serviceName) {
        ResourceName resourceComparator 
                = policyProperties.getResourceComparator(serviceName);
        String rootResource = "";
        if ((resource != null) && (resource.length() != 0)) {
            String[] resources = resourceComparator.split(resource);
            rootResource = resources[0];
            int index = resource.indexOf(rootResource);
            if ( index > 0 ) {
                rootResource = resource.substring(0, index) + rootResource;
            }
        } 
        return rootResource;
    }

    /** 
     * Returns names of policy advices that could be handled by OpenAM
     * Enterprise if PEP redirects user agent to OpenAM.
     *
     * @param appToken application sso token that would be used while
     *        communicating to OpenAM
     * @param refetchFromServer indicates whether to get the values fresh 
     *      from OpenAM or return the values from local cache.
     *      If the server reports app sso token is invalid, a new app sso
     *      token is created and one more call is made to the server.
     * @return names of policy advices that could be handled by OpenAM
     * @throws InvalidAppSSOTokenException if the server reported that the
     *         app sso token provided was invalid
     * @throws PolicyEvaluationException if the server reported any other error
     * @throws PolicyException if there are problems in getting the advice 
     *          names
     * @throws SSOException if the appToken is detected to be invalid
     *         at the client
     */
    Set getAdvicesHandleableByAM(SSOToken appToken, boolean refetchFromServer) 
            throws InvalidAppSSOTokenException, PolicyException, SSOException {
        if (debug.messageEnabled()) {
            debug.message("ResourceResultCache.getAdvicesHandleableByAM():"
                    + ":entering");
        } 

        if ( (advicesHandleableByAM != null) && !refetchFromServer ) {
            if (debug.messageEnabled()) {
                debug.message("ResourceResultCache.getAdvicesHandleableByAM():"
                        + ":returning cached advices"
                        + advicesHandleableByAM);
            } 
            return advicesHandleableByAM;
        }

        URL policyServiceURL = null;
        if (appToken != null) {
            try {
                policyServiceURL 
                        = getPolicyServiceURL(appToken);
            } catch (PolicyException pe) {
                debug.error("ResourceResultCache.getAdvicesHandleableByAM():",
                        pe);
                throw pe;
            }
        }

        if ((appToken != null) && (policyServiceURL != null)) {
            PolicyRequest policyReq = new PolicyRequest();
            policyReq.setAppSSOToken(appToken.getTokenID().toString());
            policyReq.setAdvicesHandleableByAMRequest(new
                    AdvicesHandleableByAMRequest());
            policyReq.setMethodID(
                PolicyRequest.POLICY_REQUEST_ADVICES_HANDLEABLE_BY_AM_REQUEST);

            try {
                PolicyService ps = sendPLLRequest(policyServiceURL, policyReq);
        	if (ps != null) {
                    if (debug.messageEnabled()) {
                        debug.message("ResourceResultCache."
                                + "getAdvicesHandleableByAM():"
                                + "result=" + ps.toXMLString());
                    } 
                    PolicyResponse psres = ps.getPolicyResponse();

                    String exceptionMessage = psres.getExceptionMsg();
                    if (exceptionMessage != null) {
                        if(exceptionMessage.indexOf(ResBundleUtils.getString(
                                "app_sso_token_invalid")) >= 0) {
                            if (debug.warningEnabled()) {
                                debug.warning("ResourceResultCache."
                                    + "getAdvicesHandleableByAM():"
                                    + " response exception " 
                                    + exceptionMessage);
                                debug.warning("ResourceResultCache."
                                    + "AdvicesHandleableByAM():"
                                    + " appSSOToken is invalid");
                                debug.warning("ResourceResultCache."
                                    + "throwing InvalidAppSSOTokenException");
                            }

                            String[] args = {exceptionMessage};
                            throw new InvalidAppSSOTokenException(
                                    ResBundleUtils.rbName,
                                    "server_reported_invalid_app_sso_token", 
                                    args, null);
                        } else {
                            if (debug.warningEnabled()) {
                                debug.warning("ResourceResultCache."
                                        + "AdvicesHandleableByAM():"
                                        + "response exception message=" 
                                        + exceptionMessage);
                            }
                            String[] args = {exceptionMessage};
                            throw new PolicyEvaluationException(
                                    ResBundleUtils.rbName,
                                    "server_reported_exception", 
                                    args, null);
                        }
                    }

                    if (psres.getMethodID() == PolicyResponse.
                        POLICY_ADVICES_HANDLEABLE_BY_AM_RESPONSE) 
                    {
                        AdvicesHandleableByAMResponse  
                            advicesHandleableByAMResponse 
                                = psres.getAdvicesHandleableByAMResponse();
                        if (debug.messageEnabled()) {
                            debug.message("ResourceResultCache."
                                    + "getAdvicesHandleableByAM():"
                                    + advicesHandleableByAMResponse);
                        } 
                        if (advicesHandleableByAMResponse != null) {
                            advicesHandleableByAM = 
                                advicesHandleableByAMResponse.
                                getAdvicesHandleableByAM();
                        }
                    } 
                } else {
                    debug.error("ResourceResultCache.getAdvicesHandleableByAM()"
                            +":no result");
                } 
            } catch (SendRequestException e) {
                debug.error("ResourceResultCache.getAdvicesHandleableByAM():",
                        e);
                throw new PolicyException(e);
            } 
        } 
        if (advicesHandleableByAM == null) {
            advicesHandleableByAM = Collections.EMPTY_SET;
        }
        if (debug.messageEnabled()) {
            debug.message("ResourceResultCache.getAdvicesHandleableByAM():"
                    + ":returning advicesHandleableByAM"
                    + advicesHandleableByAM);
        } 
        return advicesHandleableByAM;
    }

    /**
     * Clears cached policy decisions
     * @param serviceName service name for which cached decisions
     * would be cleared
     */
    void clearCachedDecisionsForService(String serviceName) {
        if (debug.messageEnabled()) {
            debug.message("ResourceResultCache."
                    + "clearCachedDecisionsForService():"
                    + "serviceName=" + serviceName);
        } 
        synchronized(resultCache) {
            resultCache.remove(serviceName);
        }
    }

    /**
     * Return a PolicyService object based on the XML document received
     * from remote Policy Server. This is in response to a request that we
     * send to the Policy server.
     * @param policyServiceUrl The URL of the Policy Service
     * @param preq The SessionRequest XML document
     * @return PolicyService 
     * @exception SendRequestException is thrown if there was an error in
     * sending the XML document or PolicyException if there are any parsing
     * errors.     
     */
    public static PolicyService sendPLLRequest(URL policyServiceUrl,
                           PolicyRequest preq) throws SendRequestException,
                           PolicyException{
        String lbcookie = null;
        try {
            lbcookie = getLBCookie(preq);
        } catch (Exception e) {
            throw new SendRequestException(e);
        }
        
        PolicyService policyService = new PolicyService();
        policyService.setMethodID(PolicyService.POLICY_REQUEST_ID);
        policyService.setPolicyRequest(preq); 
        String xmlString = policyService.toXMLString();

        Request request = new Request(xmlString);
        RequestSet requestSet 
                = new RequestSet(PolicyService.POLICY_SERVICE);
        
        requestSet.addRequest(request);
        if (debug.messageEnabled()) {
            debug.message("ResourceResultCache.sendPLLRequest:"
                    + "sending PLL request to URL=" + policyServiceUrl 
                    + ":\nPLL message=" + xmlString);
        }
        Vector responses = PLLClient.send(policyServiceUrl, lbcookie, requestSet);
        Response response = (Response) responses.elementAt(0);
        PolicyService ps = PolicyService.parseXML(response.getContent());
        if (debug.messageEnabled()) {
            debug.message("ResourceResultCache.sendPLLRequest:"
                                + "result=" + ps.toXMLString());
        }
        return ps;
        
     }
    
    /**
     * Returns lbcookie value for the Session
     * @param  preq policy request
     * @return lbcookie name and value pair
     * @throws Exception if session in request is invalid
     */
    public static String getLBCookie(PolicyRequest preq) throws Exception{
       String lbcookie;
       ResourceResultRequest rrReq = preq.getResourceResultRequest();
       if(rrReq !=null ) {
           lbcookie = sessionCookies.getLBCookie(rrReq.getUserSSOToken());
       } else {
           lbcookie = sessionCookies.getLBCookie(preq.getAppSSOToken());
       }
       return lbcookie;
    }
    

    private boolean resourceResultsHasAdvices(Set resourceResults) {
        boolean hasAdvices = false;
        if (resourceResults != null) {
            Iterator rrIter = resourceResults.iterator();
            while (rrIter.hasNext()) {
                ResourceResult rr = (ResourceResult)rrIter.next();
                if (rr.hasAdvices()) {
                    hasAdvices =true;
                    break;
                }
            }
        }
        return hasAdvices;
    }
    
    private String getRESTPolicyServiceURL(SSOToken token, String scope) 
            throws SSOException, PolicyException {
        URL policyServiceURL = getPolicyServiceURL(token);
        String restUrl = policyServiceURL.toString();
        restUrl = restUrl.replace(POLICY_SERVICE, REST_POLICY_SERVICE);
        if (PolicyProperties.SUBTREE.equals(scope)) {
            restUrl = restUrl + "s";
        }
        if (debug.messageEnabled()) {
            debug.message("ResourceResultCache.getRESTPolicyServiceURL():"
                + "restPolicyServiceUrl=" + restUrl);
        }
        return restUrl;
    }

    private static Set<String> mapActionBooleanToString(String serviceName, String actionName, 
            Set actValues) {
        Set values = null;
        if (actValues != null) {
            values = new HashSet<String>();
            values.addAll(actValues);
            if (values.remove("true")) {
                values.add("allow");
            }
            if (values.remove("false")) {
                values.add("deny");
            }
        } 
        return values;
    }

    String getResourceContent(SSOToken appToken, SSOToken userToken, String url)
        throws PolicyException {
        StringBuilder sb = new StringBuilder();
        HttpURLConnection conn = null;
        BufferedReader reader = null;
        try {
            conn = HttpURLConnectionManager.getConnection(new URL(url));
            conn.setDoOutput(true);
            conn.setUseCaches(false);
            conn.setRequestMethod("GET");
            conn.setInstanceFollowRedirects(false);
            setCookieAndHeader(conn, appToken, userToken);
            conn.connect();

            reader = new BufferedReader(
                new InputStreamReader(conn.getInputStream(), "UTF-8"));
            int len;
            char[] buf = new char[1024];
            while ((len = reader.read(buf, 0, buf.length)) != -1) {
                sb.append(buf, 0, len);
            }
            int responseCode = conn.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_MOVED_TEMP) { // got a 302
                        if (debug.warningEnabled()) {
                            debug.warning("ResourceResultCache.getResourceContent():"
                                    + "got 302 redirect");
                            debug.warning("ResourceResultCache.getResourceContent():"
                                    + "throwing InvalidAppSSOTokenException");
                        }

                        String[] args = {conn.getResponseMessage()};
                        throw new InvalidAppSSOTokenException(
                                ResBundleUtils.rbName,
                                "rest_call_to_server_caused_302",
                                args, null);
            
            } else if (responseCode != HttpURLConnection.HTTP_OK) {
                if (debug.warningEnabled()) {
                    debug.warning(
                        "ResourceResultCache.getResourceContent():" +
                        "REST call failed with HTTP response code:" +
                        responseCode);
                }
                throw new PolicyException(
                    "Entitlement REST call failed with error code:" +
                    responseCode);
            }
        } catch (UnsupportedEncodingException uee) {
            // should not happen
            debug.error("ResourceResultCache.getResourceContent():"
                    + "UnsupportedEncodingException:" + uee.getMessage());
        } catch (IOException ie) {
            debug.error("IOException:" + ie);
            throw new PolicyException(ResBundleUtils.rbName,
                    "rest_call_failed_with_io_exception", null, ie);
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
                if (conn != null) {
                    conn.disconnect();
                }
                
            } catch (Exception e) {
                // ignore
            }
        }
        return sb.toString();
    }

    private void setCookieAndHeader(
        HttpURLConnection conn,
        SSOToken appToken,
        SSOToken userToken
    ) throws UnsupportedEncodingException {
        String cookieValue = appToken.getTokenID().toString();
        if (Boolean.parseBoolean(
            SystemProperties.get(Constants.AM_COOKIE_ENCODE, "false"))) {
            cookieValue = URLEncoder.encode(cookieValue, "UTF-8");
        }
        String cookieName = SystemProperties.get(Constants.AM_COOKIE_NAME,
            "iPlanetDirectoryPro");
        conn.setRequestProperty("Cookie", cookieName + "=" + cookieValue);

        String userTokenId = userToken.getTokenID().toString();
        String userTokenIdHeader = "ssotoken:" + userTokenId;
        conn.setRequestProperty("X-Query-Parameters", userTokenIdHeader);
    }

    Set<ResourceResult> jsonResourceContentToResourceResults(
            String jsonResourceContent, String serviceName) 
            throws JSONException, PolicyException {
        Set<ResourceResult> resourceResults = null;
        JSONObject jsonObject = null;
        try {
            jsonObject = new JSONObject(jsonResourceContent);
        } catch(JSONException e) {
            debug.error("ResourceResultCache.jsonResourceContentToResourceResults():"
                    + "json parsing error of response: " + jsonResourceContent);
            throw new PolicyEvaluationException(
                    ResBundleUtils.rbName,
                    "error_rest_reponse", 
                    null, null);
        }
        int statusCode = jsonObject.optInt("statusCode");
        if (statusCode != 200) {
            debug.error("ResourceResultCache.jsonResourceContentToResourceResults():"
                    + "statusCode=" + statusCode + ", error response");
            throw new PolicyEvaluationException(
                    ResBundleUtils.rbName,
                    "error_rest_reponse", 
                    null, null);
        }

        jsonObject = jsonObject.optJSONObject("body");
        if (jsonObject == null) {
            debug.error("ResourceResultCache.jsonResourceContentToResourceResults():"
                    + "does not have decisions object");
            throw new PolicyEvaluationException(
                    ResBundleUtils.rbName,
                    "error_rest_reponse", 
                    null, null);
        }
        JSONArray jsonArray = jsonObject.optJSONArray("results");
        if (jsonArray != null) {
                ResourceName resourceComparator =
                        (ResourceName)policyProperties.getResourceComparator(
                        serviceName);
                ResourceResult virtualResourceResult = new ResourceResult(
                    ResourceResult.VIRTUAL_ROOT,
                    new PolicyDecision());
                int arrayLen = jsonArray.length();
                for (int i = 0; i < arrayLen; i++) {
                    JSONObject jo = jsonArray.optJSONObject(i);
                    if (jo != null) {
                        ResourceResult rr = jsonEntitlementToResourceResult(jo, 
                                serviceName);
                        virtualResourceResult.addResourceResult(rr, resourceComparator);
                    }
                }
                resourceResults = virtualResourceResult.getResourceResults();
        } else {
            String resourceName = jsonObject.optString("resourceName");
            if (resourceName != null) {
                ResourceResult resourceResult 
                        = jsonEntitlementToResourceResult(jsonObject, serviceName);
                resourceResults = new HashSet<ResourceResult>();
                resourceResults.add(resourceResult);
            } else {
                debug.error("ResourceResultCache.jsonResourceContentToResourceResults():"
                        + "does not have results or resourceName object");
                throw new PolicyEvaluationException(
                        ResBundleUtils.rbName,
                        "error_rest_reponse", 
                        null, null);
            }
        }

        return resourceResults;
    }

    ResourceResult jsonEntitlementToResourceResult(JSONObject jsonEntitlement, 
            String serviceName) throws JSONException {
        String resultResourceName = jsonEntitlement.optString(JSON_RESOURCE_NAME); 
        Map<String, Set<String>> actionsValues = JSONUtils.getMapStringSetString(
                jsonEntitlement, JSON_ACTIONS_VALUES);
        Map<String, Set<String>> advices = JSONUtils.getMapStringSetString(
                jsonEntitlement, JSON_ADVICES);
        Map<String, Set<String>> attributes = JSONUtils.getMapStringSetString(
                jsonEntitlement, JSON_ATTRIBUTES);
        Set<String> actNames = (actionsValues != null) 
                ? actionsValues.keySet() : null;
        PolicyDecision pd = new PolicyDecision();
        if (actNames != null) {
            for (String actName : actNames) {
                Set<String> actValues = actionsValues.get(actName);
                actValues  = mapActionBooleanToString(serviceName, actName, actValues);
                ActionDecision ad = new ActionDecision(actName, actValues);
                ad.setAdvices(advices);
                pd.addActionDecision(ad);
            }
        }
        pd.setResponseDecisions(attributes);
        ResourceResult resourceResult = new ResourceResult(
                resultResourceName,
                pd);
        return resourceResult;
    }


    /**
     * Registers a REST listener with policy service to recieve
     * notifications on policy changes
     * @param appToken session token identifying the client
     * @param serviceName service name
     * @param notificationURL end point on the client that listens for
     * notifications
     */
    void addRESTRemotePolicyListener(SSOToken appToken, 
            String serviceName, String notificationURL) {
        addRESTRemotePolicyListener(appToken, serviceName, notificationURL, 
                false);
    }

    /**
     * Registers a REST listener with policy service to recieve
     * notifications on policy changes
     * @param appToken session token identifying the client
     * @param serviceName service name
     * @param notificationURL end point on the client that listens for
     * notifications
     *
     * @param reRegister flag indicating whether to register listener
     *  even if it was already registered. <code>true</code> indicates
     * to register listener again even if it was previously registered
     */
    boolean addRESTRemotePolicyListener(SSOToken appToken, 
            String serviceName, String notificationURL, 
            boolean reRegister) {
        boolean  status = false;
        if (debug.messageEnabled()) {
            debug.message("ResourceResultCache.addRESTRemotePolicyListener():"
                    + "serviceName=" + serviceName
                    + ":notificationURL=" + notificationURL);
        } 

        if (remotePolicyListeners.contains(serviceName)
                    && !reRegister) {
            if (debug.messageEnabled()) {
                debug.message("ResourceResultCache.addRESTRemotePolicyListener():"
                        + "serviceName=" + serviceName
                        + ":notificationURL=" + notificationURL
                        + ":is already registered");
            } 
            return status;
        } //else do the following

        if (appToken != null) {
            try {
                String policyServiceListenerURL = null;
                policyServiceListenerURL 
                        = getRESTPolicyServiceListenerURL(appToken);
                String rootURL = getRootURL(notificationURL);
                if (debug.messageEnabled()) {
                    debug.message("ResourceResultCache."
                            + "addRESTRemotePolicyListener():"
                            + "serviceName=" + serviceName
                            + ":notificationURL=" + notificationURL
                            + ":rootURL=" + rootURL
                            + ":policyServiceListenerURL=" + policyServiceListenerURL
                            );
                } 
                Set<String> resourceNames = new HashSet<String>();
                resourceNames.add(rootURL);
                String queryString = buildRegisterListenerQueryString(
                    appToken, serviceName, resourceNames);
                queryString += "&url=" +
                    URLEncoder.encode(notificationURL, "UTF-8");
                String resourceContent = postForm(appToken,
                    policyServiceListenerURL, queryString);
                // FIXME: what do we check in the content?
                // FIXME: check the response, detect error conditions?
                if (debug.messageEnabled()) {
                    debug.message("ResourceResultCache."
                            + "addRESTRemotePolicyListener():"
                            + ":resourceContent=" + resourceContent
                            );
                } 
                status = true;
                remotePolicyListeners.add(serviceName);
            } catch (UnsupportedEncodingException e) {
                debug.error("ResourceResultCache.addRESTRemotePolicyListener():"
                        + "Can not add policy listner", e);
            } catch (SSOException se) {
                debug.error("ResourceResultCache.addRESTRemotePolicyListener():"
                        + "Can not add policy listner", se);
            } catch (PolicyException pe) {
                debug.error("ResourceResultCache.addRESTRemotePolicyListener():"
                        + "Can not add policy listner", pe);
            }
        } else {
            // log a debug message: not registering listener
            if (debug.messageEnabled()) {
                debug.message("ResourceResultCache.addRESTRemotePolicyListener():"
                        + "not adding listener, app sso token is null");
            }
        }

        return status;
    } 

    /**
     * Removes a REST listener registered with policy service to recieve
     * notifications on policy changes
     * @param appToken session token identifying the client
     * @param serviceName service name
     * @param notificationURL end point on the client that listens for
     * notifications
     */
     public boolean removeRESTRemotePolicyListener(SSOToken appToken, 
            String serviceName, String notificationURL) {
        boolean status = false;
        URL policyServiceURL = null;
        remotePolicyListeners.remove(notificationURL);
        if (appToken != null) {
            try {
                policyServiceURL = getPolicyServiceURL(appToken);
            } catch (PolicyException pe) {
                debug.error("ResourceResultCache.removeRemotePolicyListener():"
                        + "Can not remove policy listner:", pe);
            }
        }

        if (appToken != null) {
            try {
                String policyServiceListenerURL = null;
                policyServiceListenerURL 
                        = getRESTPolicyServiceListenerURL(appToken);
                if (debug.messageEnabled()) {
                    debug.message("ResourceResultCache."
                            + "removeRESTRemotePolicyListener():"
                            + "serviceName=" + serviceName
                            + ":notificationURL=" + notificationURL
                            + ":policyServiceListenerURL=" + policyServiceListenerURL
                            );
                } 
                StringBuilder sb = new StringBuilder();
                sb.append(policyServiceListenerURL).append("/");
                sb.append(URLEncoder.encode(notificationURL, "UTF-8"));
                Set<String> resourceNames = null;
                sb.append("?");
                sb.append(buildRegisterListenerQueryString(
                    appToken, serviceName, resourceNames));
                String restUrl = sb.toString();
                String resourceContent = deleteRESTResourceContent(
                    appToken, restUrl);
                // FIXME: what do we check in the content
                if (debug.messageEnabled()) {
                    debug.message("ResourceResultCache."
                            + "removeRESTRemotePolicyListener():"
                            + ":resourceContent=" + resourceContent
                            );
                } 
                remotePolicyListeners.remove(notificationURL);
            } catch (UnsupportedEncodingException e) {
                debug.error("ResourceResultCache.addRESTRemotePolicyListener():"
                        + "Can not add policy listner", e);
            } catch (SSOException se) {
                debug.error("ResourceResultCache.addRESTRemotePolicyListener():"
                        + "Can not add policy listner", se);
            } catch (PolicyException pe) {
                debug.error("ResourceResultCache.removeRESTRemotePolicyListener():"
                        + "Can not remove policy listner", pe);
            }
        } else {
            // log a debug message: not removing listener
            // log a debug message: not registering listener
            if (debug.messageEnabled()) {
                debug.message("ResourceResultCache.removeRESTRemotePolicyListener():"
                        + "not removing listener, app sso token is null");
            }
        }

        return status;
    }

    /**
     * Processes REST policy notifications forwarded from listener end 
     * point of policy client
     * @param pn REST policy notification
     */
    static void processRESTPolicyNotification(String pn) //pn has to be JSON string
            throws PolicyEvaluationException {
        // samplePn = "{realm: "/", privilgeName: "p1", resources: ["r1", "r2"]}";
        if (pn != null) {
            if (debug.messageEnabled()) {
                debug.message("ResourceResultCache:processRESTPolicyNotification(), jsonString:"
                        + pn);
            }
            ResourceResultCache cache = ResourceResultCache.getInstance();
            // FIXME after servre side is fixed to provide serviceName in notification
            String serviceName = "iPlanetAMWebAgentService"; 
            Set<String> affectedResourceNames = null;  
            try {
                JSONObject jo = new JSONObject(pn);
                JSONArray jsonArray = jo.optJSONArray("resources");
                if (jsonArray != null) {
                    int arrayLen = jsonArray.length();
                    for (int i = 0; i < arrayLen; i++) {
                        String  resName = jsonArray.optString(i);
                        if (affectedResourceNames == null) {
                            affectedResourceNames = new HashSet<String>();
                        }
                        affectedResourceNames.add(resName);
                    }
                }
            } catch (JSONException je) {
                debug.error("ResourceResultCache.processRESTPolicyNotification():"
                    + "pn=" + pn);
                throw new PolicyEvaluationException("notification_not_valid_json");
            }
            if (serviceName != null && affectedResourceNames != null) {
                if (cache.remotePolicyListeners.contains(serviceName)) {
                    if (debug.messageEnabled()) {
                        debug.message("ResourceResultCache:"
                                + "processRESTPolicyNotification():" 
                                + "serviceName=" + serviceName
                                + ":affectedResourceNames=" 
                                + affectedResourceNames
                                + ":clearing cache for affected "
                                + "resource names");
                    }
                    clearCacheForResourceNames(serviceName, 
                            affectedResourceNames);
                } else {
                    if (debug.messageEnabled()) {
                        debug.message("ResourceResultCache:"
                                + "processRESTPolicyNotification():" 
                                + "serviceName not registered"
                                + ":no resource names cleared from cache");
                    }
                }
            } else {
                if (debug.messageEnabled()) {
                    debug.message("ResourceResultCache:"
                            + "processRESTPolicyNotification():" 
                            + "serviceName or affectedResourceNames is null"
                            + ":no resource names cleared from cache");
                }
            }
        } else {
            debug.error("ResourceResultCache.processRESTPolicyNotification()" 
                    + "PolicyNotification is null");
        } 
    } 

    private String getRESTPolicyServiceListenerURL(SSOToken token) 
            throws SSOException, PolicyException {
        String restUrl = null;
        URL policyServiceURL = getPolicyServiceURL(token);
        restUrl = policyServiceURL.toString();
        restUrl = restUrl.replace(POLICY_SERVICE, REST_POLICY_SERVICE_LISTENER);
        if (debug.messageEnabled()) {
            debug.message("ResourceResultCache.getRESTPolicyServiceURL():"
                + "restPolicyServiceListenerUrl=" + restUrl);
        }
        return restUrl;
    }
    
    String postForm(SSOToken appToken, String url, String formContent)
        throws PolicyException {
        if (debug.messageEnabled()) {
            debug.message("ResourceResultCache."
                    + "postForm():"
                    + "url=" + url
                    + ", formContent=" + formContent);
        }
        StringBuilder sb = new StringBuilder();
        HttpURLConnection conn = null;
        OutputStream out = null;
        BufferedReader reader = null;
        try {
            conn = HttpURLConnectionManager.getConnection(new URL(url));
            conn.setDoInput(true);
            conn.setDoOutput(true);
            conn.setUseCaches(false);
            setCookieAndHeader(conn, appToken, appToken);
            conn.setRequestMethod("POST");
            conn.setRequestProperty("Content-Type", 
                    "application/x-www-form-urlencoded");
            conn.setRequestProperty("Content-Length", 
                    Integer.toString(formContent.length()));

            conn.connect();

            out =   conn.getOutputStream();
            out.write(formContent.getBytes("UTF-8"));
            out.write("\r\n".getBytes("UTF-8"));
            out.flush();
            out.close();

            reader =  new BufferedReader(
                    new InputStreamReader(
                    conn.getInputStream(), "UTF-8"));
            int len;
            char[] buf = new char[1024];
            while ((len = reader.read(buf, 0, buf.length)) != -1) {
                sb.append(buf, 0, len);
            }
            int responseCode = conn.getResponseCode();
            // any 200 series response code is success
            if (responseCode < 200 || responseCode > 299) {
                    if (debug.warningEnabled()) {
                        debug.warning("ResourceResultCache."
                                + "postForm():"
                                + "REST call failed with HTTP response code:" 
                                + responseCode);
                    }
                    throw new PolicyException(
                            "Entitlement REST call failed with error code:" 
                            + responseCode);
            }
        } catch (UnsupportedEncodingException uee) {
            // should not happen
            debug.error("ResourceResultCache.postFormParams():"
                    + "UnsupportedEncodingException:" + uee.getMessage());
        } catch (IOException ie) {
            debug.error("ResourceResultCache.postForm():IOException:" 
                    + ie.getMessage(), ie);
            throw new PolicyException(ResBundleUtils.rbName,
                    "rest_call_failed_with_io_exception", null, ie);
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
                if (conn != null) {
                    conn.disconnect();
                }
                
            } catch (Exception e) {
                // ignore
            }
        }
        return sb.toString();
    }

    private String deleteRESTResourceContent(SSOToken appToken, String url)
        throws PolicyException {
        StringBuilder sb = new StringBuilder();
        HttpURLConnection conn = null;
        BufferedReader reader = null;
        try {
            conn = HttpURLConnectionManager.getConnection(new URL(url));
            conn.setDoOutput(true);
            conn.setUseCaches(false);
            setCookieAndHeader(conn, appToken, appToken);
            conn.setRequestMethod("DELETE");
            conn.connect();

            reader = new BufferedReader(new InputStreamReader(
                conn.getInputStream(), "UTF-8"));
            int len;
            char[] buf = new char[1024];
            while ((len = reader.read(buf, 0, buf.length)) != -1) {
                sb.append(buf, 0, len);
            }
            int responseCode = conn.getResponseCode();
            if (responseCode != conn.HTTP_OK) {
                    if (debug.warningEnabled()) {
                        debug.warning("ResourceResultCache."
                                + "deleteRESTResourceContent():"
                                + "REST call failed with HTTP response code:" 
                                + responseCode);
                    }
                    throw new PolicyException(
                            "Entitlement REST call failed with error code:" 
                             + responseCode);
            }
        } catch (UnsupportedEncodingException uee) {
            // should not happen
            debug.error("ResourceResultCache.deleteRESTResourceContent():"
                    + "UnsupportedEncodingException:" + uee.getMessage());
        } catch (IOException ie) {
            debug.error("IOException:" + ie);
            throw new PolicyException(ResBundleUtils.rbName,
                    "rest_call_failed_with_io_exception", null, ie);
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
                if (conn != null) {
                    conn.disconnect();
                }
                
            } catch (Exception e) {
                // ignore
            }
        }
        return sb.toString();
    }

    static String buildRegisterListenerQueryString(
            SSOToken appToken,
            String serviceName, // called application in entitlement
            Set<String> resourceNames) throws PolicyException {
        StringBuilder sb = new StringBuilder();
        try {
            if (appToken == null) {
                if (debug.warningEnabled()) {
                    debug.warning("ResourceResultCache.builRegisterListenerdQueryString():"
                            + "admin is null");
                }
                throw new PolicyException(ResBundleUtils.rbName,
                        "admin_can_not_be_null", null, null); 
            } else {
                String tokenId = appToken.getTokenID().toString();
                String hashedTokenId = Hash.hash(tokenId);
                sb.append(REST_QUERY_SUBJECT).append("=");
                sb.append(URLEncoder.encode(hashedTokenId, "UTF-8"));
            }

            if ((serviceName == null) || (serviceName.length() == 0)) {
                if (debug.warningEnabled()) {
                    debug.warning("ResourceResultCache.builRegisterListenerdQueryString():"
                            + "serviceName can not be null");
                }
                throw new PolicyException(ResBundleUtils.rbName,
                        "service_name_can_not_be_null", null, null);
            } else {
                if (sb.length() > 0) {
                    sb.append("&");
                }
                sb.append(REST_QUERY_APPLICATION).append("=");
                sb.append(URLEncoder.encode(serviceName, "UTF-8"));
            }

            if ((resourceNames == null) || resourceNames.isEmpty()) {
                if (debug.warningEnabled()) {
                    debug.warning("ResourceResultCache.builRegisterListenerdQueryString():"
                            + "resoureNames is null or empty");
                }
            } else {
                for (String resourceName : resourceNames) {
                    if (sb.length() > 0) {
                        sb.append("&");
                    }
                    sb.append(REST_QUERY_RESOURCES).append("=");
                    sb.append(URLEncoder.encode(resourceName, "UTF-8"));
                }
            }
        } catch (UnsupportedEncodingException use) {
            // should not happen
            debug.error("ResourceResultCache.buildRegisterListenerQueryString():" 
                    + use.getMessage());
        }
        return sb.toString();
    }

    static String buildEntitlementRequestQueryString(
            String realm, 
            String serviceName, 
            SSOToken userToken,
            String resource,
            Set actionNames,
            Map envMap) throws PolicyException {
        StringBuilder sb = new StringBuilder();
        try {
            realm = (realm == null || (realm.trim().length() == 0)) ? "/"
                    : realm;
            realm = URLEncoder.encode(realm, "UTF-8");
            sb.append(REST_QUERY_REALM).append("=");
            sb.append(realm);

            if ((serviceName == null) || (serviceName.length() == 0)) {
                if (debug.warningEnabled()) {
                    debug.warning("ResourceResultCache."
                            + "buildEntitlementRequestQueryString():"
                            + "serviceName can not be null");
                }
                throw new PolicyException(ResBundleUtils.rbName,
                        "service_name_can_not_be_null", null, null);
            } else {
                sb.append("&").append(REST_QUERY_APPLICATION).append("=");
                sb.append(URLEncoder.encode(serviceName, "UTF-8"));
            }

            if (userToken == null) {
                if (debug.warningEnabled()) {
                    debug.warning("ResourceResultCache."
                            + "buildEntitlementRequestQueryString():"
                            + "subject can not be null");
                }
                throw new PolicyException(ResBundleUtils.rbName,
                        "subject_can_not_be_null", null, null);
            } else {
                String userTokenId = userToken.getTokenID().toString();
                String hashedUserTokenId = Hash.hash(userTokenId);
                sb.append("&").append(REST_QUERY_SUBJECT).append("=");
                sb.append(URLEncoder.encode(hashedUserTokenId, "UTF-8"));
            }
            if ((resource == null) || (resource.trim().length() == 0)) {
                if (debug.warningEnabled()) {
                    debug.warning("ResourceResultCache."
                            + "buildEntitlementRequestQueryString():"
                            + "resource can not be null");
                }
                throw new PolicyException(ResBundleUtils.rbName,
                        "resource_can_not_be_null", null, null);
            } else {
                sb.append("&").append(REST_QUERY_RESOURCE).append("=");
                sb.append(URLEncoder.encode(resource, "UTF-8"));
            }
            if ((actionNames != null) && !actionNames.isEmpty()) {
                for (Object actObj: actionNames) {
                    sb.append("&").append(REST_QUERY_ACTION).append("=");
                    sb.append(URLEncoder.encode(actObj.toString(), "UTF-8"));
                }
            }
            if ((envMap != null) && !envMap.isEmpty()) {
                String encodedEq = URLEncoder.encode("=", "UTF-8");
                Set keys = envMap.keySet();
                for (Object keyOb : keys) {
                    Set values = (Set)envMap.get(keyOb);
                    String key = URLEncoder.encode(keyOb.toString(), "UTF-8");
                    if ((values != null) && !values.isEmpty()) {
                        for (Object valueOb : values) {
                            sb.append("&").append(REST_QUERY_ENV).append("=");
                            sb.append(key);
                            sb.append(encodedEq);
                            sb.append(URLEncoder.encode(valueOb.toString(), "UTF-8"));
                        }
                    }
                }
            }
        } catch (UnsupportedEncodingException use) {
            // should not happen
            debug.error("ResourceResultCache.buildEntitlementRequestQueryString():" 
                    + use.getMessage());
        }
        return sb.toString();
    }

    String getRootURL(String url) {
        if (url == null) {
            return null;
        }
        int dsi = url.indexOf("//");
        if (dsi == -1) {
            return url;
        }
        int si = url.indexOf("/", dsi + 3);
        if (si == -1) {
            return url;
        }
        return (url.substring(0, si));
    }

    
}

