/*
 * @(#)BasicEvaluationCtx.java
 *
 * Copyright 2004-2006 Sun Microsystems, Inc. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   1. Redistribution of source code must retain the above copyright notice,
 *      this list of conditions and the following disclaimer.
 * 
 *   2. Redistribution in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *
 * Neither the name of Sun Microsystems, Inc. or the names of contributors may
 * be used to endorse or promote products derived from this software without
 * specific prior written permission.
 * 
 * This software is provided "AS IS," without a warranty of any kind. ALL
 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING
 * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
 * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN")
 * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE
 * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST
 * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,
 * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY
 * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE,
 * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 *
 * You acknowledge that this software is not designed or intended for use in
 * the design, construction, operation or maintenance of any nuclear facility.
 */

package com.sun.xacml;

import com.sun.xacml.attr.AttributeValue;
import com.sun.xacml.attr.BagAttribute;
import com.sun.xacml.attr.DateAttribute;
import com.sun.xacml.attr.DateTimeAttribute;
import com.sun.xacml.attr.DecisionAttribute;
import com.sun.xacml.attr.StringAttribute;
import com.sun.xacml.attr.TimeAttribute;

import com.sun.xacml.cond.EvaluationResult;

import com.sun.xacml.ctx.Attribute;
import com.sun.xacml.ctx.RequestCtx;
import com.sun.xacml.ctx.RequestElement;
import com.sun.xacml.ctx.Result;

import com.sun.xacml.finder.AttributeFinder;
import com.sun.xacml.finder.RevocationFinder;
import com.sun.xacml.reduction.ReductionGraph;

import java.net.URI;

import java.util.ArrayList;
import java.util.Date;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

import org.apache.log4j.Logger;

import org.w3c.dom.Node;


/**
 * A basic implementation of <code>EvaluationCtx</code> that is created from
 * an XACML Request and falls back on an AttributeFinder if a requested
 * value isn't available in the Request.
 * <p>
 * Note that this class can do some optional caching for current date, time,
 * and dateTime values (defined by a boolean flag to the constructors). The
 * XACML specification requires that these values always be available, but it
 * does not specify whether or not they must remain constant over the course
 * of an evaluation if the values are being generated by the PDP (if the
 * values are provided in the Request, then obviously they will remain
 * constant). The default behavior is for these environment values to be
 * cached, so that (for example) the current time remains constant over the
 * course of an evaluation.
 *
 * @since 1.2
 * @author Seth Proctor
 * @author Ludwig Seitz
 */
public class BasicEvaluationCtx implements EvaluationCtx, Cloneable
{
    /**
     * the finder to use if a value isn't in the request
     */
    private AttributeFinder aFinder;

    /**
     * flag that indicates if the attribute finder is deactivated.
     */
    private boolean afinderActive = true;

    /**
     * the revocation finder to use
     */
    private RevocationFinder rFinder;

    /**
     * flag that indicates if the revocation finder is deactivated.
     */
    private boolean rfinderActive = true;
    /**
     * the DOM root the original RequestContext document.
     */
    private Node requestRoot;

    /**
     * The request data. A map of <code>Set</code>s of 
     * <code>RequestElement</code>s keyed by the attribute category 
     * <code>String</code>s.
     */
    private HashMap<URI, Set<RequestElement>> requestElements = null;

    /**
     * the cached delegation decision value
     */
    private int delegationDecision;

    /**
     * the delegation depth value
     */
    private int delegationDepth;

    /**
     * the resource id
     */
    private AttributeValue resourceId;

    /**
     * the <code>Set</code> of <code>RequestElements</code> containing
     * only the attributes that should be included in the result.
     */
    private Set<RequestElement> includedAttributes;

    /**
     *  the resource scope
     */
    private int scope;

    /**
     * the cached current date, which we may or may
     * not be using depending on how this object was constructed
     */
    private DateAttribute currentDate;

    /**
     * the cached current time, which we may or may
     * not be using depending on how this object was constructed
     */
    private TimeAttribute currentTime;

    /**
     * the cached current dateTime, which we may or may
     * not be using depending on how this object was constructed
     */
    private DateTimeAttribute currentDateTime;

    /**
     * a flag indicating that we should use the cached date/time values.
     */
    private boolean useCachedEnvValues;

    /**
     * a stack of parent <code>PolicySet</code>s.
     */
    private Stack<PolicyTreeElement> parentPolicySets;

    /**
     * a stack of <code>ReductionGraph</code>s.
     */
    private Stack<ReductionGraph> reductionGraphs;

    /**
     * the set of deactivated policies
     */
    private Set<URI> inactivePolicyIds;

    /**
     * the logger we'll use for all messages
     */
    private static final Logger logger =
        Logger.getLogger(BasicEvaluationCtx.class.getName());

    /**
     * Constructs a new <code>BasicEvaluationCtx</code> based on the given
     * request. The resulting context will cache current date, time, and
     * dateTime values so they remain constant for this evaluation.
     *
     * @param request the request
     *
     * @throws ParsingException if a required attribute is missing, or if there
     *                          are any problems dealing with the request data
     */
    public BasicEvaluationCtx(RequestCtx request) 
    throws ParsingException {
        this(request, null, null, true);
    }

    /**
     * Constructs a new <code>BasicEvaluationCtx</code> based on the given
     * request.
     *
     * @param request the request
     * @param cacheEnvValues whether or not to cache the current time, date,
     *                       and dateTime so they are constant for the scope
     *                       of this evaluation
     *
     * @throws ParsingException if a required attribute is missing, or if there
     *                          are any problems dealing with the request data
     */
    public BasicEvaluationCtx(RequestCtx request, boolean cacheEnvValues)
    throws ParsingException {
        this(request, null, null, cacheEnvValues);
    }

    /**
     * Constructs a new <code>BasicEvaluationCtx</code> based on the given
     * request, and supports looking outside the original request for attribute
     * values using the <code>AttributeFinder</code>. The resulting context
     * will cache current date, time, and dateTime values so they remain
     * constant for this evaluation.
     *
     * @param request the request
     * @param aFinder an <code>AttributeFinder</code> to use in looking for
     *               attributes that aren't in the request
     * @param rFinder an <code>RevocationFinder</code> to use in looking for
     *               revocations
     *
     * @throws ParsingException if a required attribute is missing, or if there
     *                          are any problems dealing with the request data
     */
    public BasicEvaluationCtx(RequestCtx request, AttributeFinder aFinder,
            RevocationFinder rFinder)
    throws ParsingException {
        this(request, aFinder, rFinder, true);
    }

    /**
     * Constructs a new <code>BasicEvaluationCtx</code> based on the given
     * request, and supports looking outside the original request for attribute
     * values using the <code>AttributeFinder</code>.
     *
     * @param request the request
     * @param aFinder an <code>AttributeFinder</code> to use in looking for
     * @param rFinder an <code>RevocationFinder</code> to use in looking for
     *               revocations
     *             attributes that aren't in the request           
     * @param cacheEnvValues whether or not to cache the current time, date,
     *                       and dateTime so they are constant for the scope
     *                       of this evaluation
     *
     * @throws ParsingException if a required attribute is missing, or if there
     *                          are any problems dealing with the request data
     */
    public BasicEvaluationCtx(RequestCtx request, AttributeFinder aFinder,
            RevocationFinder rFinder,
            boolean cacheEnvValues) 
    throws ParsingException {
        // keep track of the attribute finder
        this.aFinder = aFinder;
        // keep track of the revocation finder
        this.rFinder = rFinder;

        // remember the root of the DOM tree for XPath queries
        this.requestRoot = request.getDocumentRoot();

        // initialize the cached date/time values so it's clear we haven't
        // retrieved them yet
        this.useCachedEnvValues = cacheEnvValues;
        this.currentDate = null;
        this.currentTime = null;
        this.currentDateTime = null;

        // parse the request elements into a map
        // and collect the IncludeInResult attributes
        this.requestElements = new HashMap<URI, Set<RequestElement>>();
        this.includedAttributes = new HashSet<RequestElement>();
        Iterator<RequestElement> iter = request.getRequestElements().iterator();
        while (iter.hasNext()){
            RequestElement re = iter.next();
            URI category = re.getCategory();
            if (!re.getIncludeInResultSet().isEmpty()) {
                this.includedAttributes.add(new RequestElement(
                        re.getCategory(), re.getIncludeInResultSet()));
            }
            if (this.requestElements.containsKey(category)) {
                this.scope = Constants.SCOPE_MULTIPE_ELEMENTS;
                Set<RequestElement> set = this.requestElements.get(category);
                set.add(re);
            } else {
                Set<RequestElement> set = new HashSet<RequestElement>();
                set.add(re);
                this.requestElements.put(category, set);
            }     
        }

        //Compatibility code for XACML 2.0 requestId handling
        if (request.getXACMLVersion() < Constants.XACML_VERSION_3_0
                && !this.includedAttributes.isEmpty()) {
            throw new ParsingException("Can't have included attributes" 
                    + " in XACML < v 3.0");
        } 

        //Compatibility code for old Resource handling.
        if (this.requestElements.containsKey(Constants.RESOURCE_CAT)) {
            Iterator<RequestElement> reIt = this.requestElements.get(
                    Constants.RESOURCE_CAT).iterator();
            while(reIt.hasNext()) {
                RequestElement re = (RequestElement)reIt.next();
                checkResource(re.getAttributes());
            }
        }
        
        //this will only be set in administrative requests.
        this.delegationDecision = Result.INVALID_DECISION;
        this.delegationDepth = 0;

        this.parentPolicySets = new Stack<PolicyTreeElement>();
        this.reductionGraphs = new Stack<ReductionGraph>();

        // this is empty when the evaluation context is first created
        // it is used during the reduction of untrusted policies
        this.inactivePolicyIds = new HashSet<URI>();
    }

    /**
     * The clone method
     * 
     * @return  a copy of this object.
     * 
     */
    public Object clone() {
        try {
            BasicEvaluationCtx clone = (BasicEvaluationCtx)super.clone();
            if (this.aFinder != null) {                
                clone.aFinder = (AttributeFinder)this.aFinder.clone();
            }
            clone.afinderActive = this.afinderActive;
            if(this.rFinder != null) {
                clone.rFinder = (RevocationFinder)this.rFinder.clone();
            }
            clone.rfinderActive = this.rfinderActive;
            if(this.requestRoot != null) {
                clone.requestRoot = this.requestRoot.cloneNode(true);
            } else {
                clone.requestRoot = null;
            }

            // deep copy of the requestElements
            clone.requestElements = new HashMap<URI, Set<RequestElement>>();
            Iterator<URI> iter = this.requestElements.keySet().iterator();
            while(iter.hasNext()) {
                URI key = URI.create(iter.next().toString());
                Set<RequestElement> res = new HashSet<RequestElement>();
                Iterator<RequestElement> iter2 = this.requestElements.get(key).iterator();
                while(iter2.hasNext()) {
                    res.add((RequestElement)iter2.next().clone());
                }
                clone.requestElements.put(key, res);
            }

            clone.delegationDecision = this.delegationDecision;
            clone.delegationDepth = this.delegationDepth;
            clone.resourceId = this.resourceId;

            //deep copy of the included attributes
            clone.includedAttributes = new HashSet<RequestElement>();
            Iterator<RequestElement> iter3 = this.includedAttributes.iterator();
            while(iter3.hasNext()) {
                RequestElement element = iter3.next();
                clone.includedAttributes.add( (RequestElement) element.clone());
            }
            synchronized (clone) {
                clone.scope = this.scope;
                clone.currentDate = this.currentDate;
                clone.currentTime = this.currentTime;
                clone.currentDateTime = this.currentDateTime;
                clone.useCachedEnvValues = this.useCachedEnvValues;
            }
            //clone.parentPolicySets = (Stack<PolicyTreeElement>) this.parentPolicySets.clone(); // 
            clone.parentPolicySets = new Stack<PolicyTreeElement>();
            Collections.copy(parentPolicySets, this.parentPolicySets);
            //clone.reductionGraphs = (Stack<ReductionGraph>) this.reductionGraphs.clone();
            clone.reductionGraphs = new Stack<ReductionGraph>();
            Collections.copy(reductionGraphs, this.reductionGraphs);
            clone.inactivePolicyIds = new HashSet<URI>(this.inactivePolicyIds);
            return clone;
        } catch (CloneNotSupportedException e) {//This should never happen
            throw new RuntimeException("Couldn't clone BasicEvaluationCtx");
        }  
    }

    /**
     * Create a context for an administrative request from this context.
     * 
     * @param decision  The decision code corresponding to those in the
     *                   <code>Result</code> class.
     * @param delegate  The delegate in this request (a set containing a 
     *                    single <code>RequestElement</code>).
     *                     
     * @return  An administrative context for this context.                      
     */
    public EvaluationCtx createAdminCtx(int decision, Set<RequestElement> delegate) {
        if (decision == Result.INVALID_DECISION 
                || decision == Result.DECISION_INDETERMINATE
                || decision == Result.DECISION_NOT_APPLICABLE) {
            throw new ProcessingException("Invalid decision value for an" 
                    + " administrative request");
        } 

        //First copy the old context.
        BasicEvaluationCtx adminCtx = null;
        adminCtx = (BasicEvaluationCtx)this.clone();

        //Now set the new values ...
        adminCtx.delegationDecision = decision;
        adminCtx.delegationDepth = this.delegationDepth + 1;

        //... and modify the requestElements accordingly
        HashMap<URI, Set<RequestElement>> newREmap = new HashMap<URI, Set<RequestElement>>();

        //get out the delegation relevant information
        // and remove the old
        adminCtx.requestElements.remove(Constants.DELEGATION_INFO);
        adminCtx.requestElements.remove(Constants.DELEGATE);

        //modify the rest so category * becomes category 
        // urn:oasis:names:tc:xacml:3.0:attribute-category:delegated:*
        Iterator<Map.Entry<URI, Set<RequestElement>>> iter = adminCtx.requestElements.entrySet().iterator();
        while(iter.hasNext()) {
        	Map.Entry<URI, Set<RequestElement>> entry = iter.next();
            Set<RequestElement> requestElementCategory = entry.getValue();
            Iterator<RequestElement> iter2 = requestElementCategory.iterator();
            HashSet<RequestElement> newREset = new HashSet<RequestElement>();
            URI category = null;
            while(iter2.hasNext()) {
                RequestElement oldRe = iter2.next();
                category = oldRe.getCategory();
                if (category.toString().startsWith(Constants.DELEGATED)) {
                    newREset.add(oldRe);
                } else { // add the delegated part to the category
                    category = URI.create(Constants.DELEGATED 
                            + category.toString());
                    // this URI must be valid, since the old category was
                    RequestElement newRe = new RequestElement(category,
                            oldRe.getAttributes());
                    newREset.add(newRe);
                }
            }
            newREmap.put(category, newREset);
        }

        //put in the new delegate
        newREmap.put(Constants.DELEGATE, delegate);

        //put in the new delegation info

        // then create the correct DelegationInfo category
        DecisionAttribute delegationDecision = new DecisionAttribute(decision);
        Set<Attribute> delegationInfoAttrs = new HashSet<Attribute>();
        delegationInfoAttrs.add(delegationDecision);
        RequestElement delegationInfo = new RequestElement(
                Constants.DELEGATION_INFO, delegationInfoAttrs);
        Set<RequestElement> delegationInfoSet = new HashSet<RequestElement>();
        delegationInfoSet.add(delegationInfo);
        // and put it into the new request elements map
        newREmap.put(Constants.DELEGATION_INFO, delegationInfoSet);

        //now we are done, copy the newREmap to the requestElements
        adminCtx.requestElements = newREmap;

        return adminCtx;
    }

    /**
     * Creates a copy of this context with disabled attribute finder.
     * 
     * @return  A copy of this context with disabled attribute finder.
     */
    public EvaluationCtx copyWithoutAttributeFinder() {
        BasicEvaluationCtx ctx = (BasicEvaluationCtx)this.clone();
        ctx.aFinder = null;
        ctx.afinderActive = false;
        return ctx;
    }

    /**
     * This basically does the same thing that the other types need
     * to do, except that we also look for a resource-id attribute, not
     * because we're going to use, but only to make sure that it's not
     * double, and for the optional scope attribute, to see what the scope
     * of the attribute is.
     * 
     * @param resourceAttr  A <code>Map</code> keyed by attribute id
     *                      <code>URI</code>s, containing <code>Set</code>s
     *                      of <code>Attribute</code>s. 
     * @param xacmlVersion  The XACML version number.
     *                      
     * @throws ParsingException 
     * 
     */
    private void checkResource(Map<URI, Set<Attribute>> resourceAttr) 
    throws ParsingException {

        // make sure there's only one value for resource-id
        Set<Attribute> rSet =  resourceAttr.get(Constants.RESOURCE_ID);  
        if (rSet != null) {
            if (rSet.size() > 1) {
                System.err.println("Resource may contain "
                        + "only one resource-id Attribute");
                throw new ParsingException("too many resource-id attrs");
            }
            // keep track of the resource-id attribute
            this.resourceId = ((Attribute)(rSet.iterator().next()))
            .getValue();
        }


        // see if a resource-scope attribute was included
        if (resourceAttr.containsKey(Constants.RESOURCE_SCOPE)) {
            Set<Attribute> set = resourceAttr.get(Constants.RESOURCE_SCOPE);

            // make sure there's only one value for resource-scope
            if (set.size() > 1) {
                System.err.println("Resource may contain "
                        + "only one resource-scope Attribute");
                throw new ParsingException("too many resource-scope attrs");
            }

            Attribute attr = (Attribute)(set.iterator().next());
            AttributeValue attrValue = attr.getValue();

            // scope must be a string, so throw an exception otherwise
            if (! attrValue.getType().toString().
                    equals(StringAttribute.identifier)) {
                throw new ParsingException("scope attr must be a string");
            }

            String value = ((StringAttribute)attrValue).getValue();

            if (value.equals("Immediate")) {
                this.scope = Constants.SCOPE_IMMEDIATE;
            } else if (value.equals("Children")) {
                this.scope = Constants.SCOPE_CHILDREN;
            } else if (value.equals("Descendants")) {
                this.scope = Constants.SCOPE_DESCENDANTS;
            } else if (value.equals("XPath-expression")) {
                this.scope = Constants.SCOPE_XPATH_EXPRESSION;
            } else if (value.equals("EntireHierarchy")) {
                this.scope = Constants.SCOPE_ENTIRE_HIERARCHY;
            } else {
                System.err.println("Unknown scope type: " + value);
                throw new ParsingException("invalid scope type: " + value);
            }
        } else {
            // by default, the scope is always Immediate
            this.scope = Constants.SCOPE_IMMEDIATE;
        }
    }

    /**
     * Returns the DOM root of the original RequestType XML document.
     *
     * @return the DOM root node
     */
    public Node getRequestRoot() {
        return this.requestRoot;
    }

    /**
     * Returns the resource scope of the request, which will be one of the
     * three fields denoting Immediate, Children, or Descendants.
     *
     * @return the scope of the resource in the request
     */
    public int getScope() {
        return this.scope;
    }

    /**
     * Returns the resource named in the request as resource-id.
     *
     * @return the resource
     */
    public AttributeValue getResourceId() {
        return this.resourceId;
    }

    /**
     * @return The <code>Set</code> of <code>RequestElement</code>s
     *         representing the included attributes.
     */
    public Set<RequestElement> getIncludedAttributes() {
        return this.includedAttributes;
    }

    /**
     * Changes the value of the resource-id attribute in this context. This
     * is useful when you have multiple resources (ie, a scope other than
     * IMMEDIATE), and you need to keep changing only the resource-id to
     * evaluate the different effective requests.
     *
     * @param resourceId the new resource-id value
     */
    public void setResourceId(AttributeValue resourceId) {
        this.resourceId = resourceId;

        // there will always be exactly one value for this attribute
        Iterator<RequestElement> rIt = this.requestElements.get(Constants.RESOURCE_CAT).iterator();
        while (rIt.hasNext()) {
            RequestElement re = rIt.next();
            Map<URI, Set<Attribute>> attrMap = re.getAttributes();
            Set<Attribute> resId = attrMap.get(Constants.RESOURCE_ID);
            Attribute attr = (Attribute)(resId.iterator().next());
            // remove the old value...
            resId.remove(attr);
            Attribute newAttr = new Attribute(attr.getId(), attr.getIssuer(),
                    resourceId,
                    attr.getVersion(), 
                    attr.includeInResult());
            // if it was marked to be included in the result remove this too
            if (attr.includeInResult()) {
                Iterator<RequestElement> iter = this.includedAttributes.iterator();
                while (iter.hasNext()) {
                    RequestElement ire = iter.next();
                    if (ire.getCategory().equals(Constants.RESOURCE_CAT)) {
                        this.includedAttributes.remove(ire);
                        Map<URI, Set<Attribute>> newAttrs = new HashMap<URI, Set<Attribute>>();
                        newAttrs.putAll(ire.getAttributes());
                        newAttrs.remove(Constants.RESOURCE_ID);
                        Set<Attribute> newResourceId = new HashSet<Attribute>();
                        newResourceId.add(newAttr);
                        newAttrs.put(Constants.RESOURCE_ID, newResourceId);
                        this.includedAttributes.add(new RequestElement(
                                ire.getCategory(), newAttrs));
                    }
                }
            }
            // ...and insert the new value
            resId.add(newAttr);
        }
    }

    /**
     * Returns the value for the current time. The current time, current
     * date, and current dateTime are consistent, so that they all
     * represent the same moment. If this is the first time that one
     * of these three values has been requested, and caching is enabled,
     * then the three values will be resolved and stored.
     * <p>
     * Note that the value supplied here applies only to dynamically
     * resolved values, not those supplied in the Request. In other words,
     * this always returns a dynamically resolved value local to the PDP,
     * even if a different value was supplied in the Request. This is
     * handled correctly when the value is requested by its identifier.
     *
     * @return the current time
     */
    public synchronized TimeAttribute getCurrentTime() {
        long millis = dateTimeHelper();
        if (this.useCachedEnvValues) {
            return this.currentTime;
        }
        return new TimeAttribute(new Date(millis));
    }

    /**
     * Returns the value for the current date. The current time, current
     * date, and current dateTime are consistent, so that they all
     * represent the same moment. If this is the first time that one
     * of these three values has been requested, and caching is enabled,
     * then the three values will be resolved and stored.
     * <p>
     * Note that the value supplied here applies only to dynamically
     * resolved values, not those supplied in the Request. In other words,
     * this always returns a dynamically resolved value local to the PDP,
     * even if a different value was supplied in the Request. This is
     * handled correctly when the value is requested by its identifier.
     *
     * @return the current date
     */
    public synchronized DateAttribute getCurrentDate() {
        long millis = dateTimeHelper();

        if (this.useCachedEnvValues) {
            return this.currentDate;
        }
        return new DateAttribute(new Date(millis));
    }

    /**
     * Returns the value for the current dateTime. The current time, current
     * date, and current dateTime are consistent, so that they all
     * represent the same moment. If this is the first time that one
     * of these three values has been requested, and caching is enabled,
     * then the three values will be resolved and stored.
     * <p>
     * Note that the value supplied here applies only to dynamically
     * resolved values, not those supplied in the Request. In other words,
     * this always returns a dynamically resolved value local to the PDP,
     * even if a different value was supplied in the Request. This is
     * handled correctly when the value is requested by its identifier.
     *
     * @return the current dateTime
     */
    public synchronized DateTimeAttribute getCurrentDateTime() {
        long millis = dateTimeHelper();

        if (this.useCachedEnvValues) {
            return this.currentDateTime;
        }
        return new DateTimeAttribute(new Date(millis));
    }

    /**
     * Private helper that figures out if we need to resolve new values,
     * and returns either the current moment (if we're not caching) or
     * -1 (if we are caching)
     * 
     * @return returns the current moment or -1. 
     */
    private long dateTimeHelper() {
        // if we already have current values, then we can stop (note this
        // always means that we're caching)
        if (this.currentTime != null) {
            return -1;
        }

        // get the current moment
        Date time = new Date();
        long millis = time.getTime();

        // if we're not caching then we just return the current moment
        if (! this.useCachedEnvValues) {
            return millis;
        }
        // we're caching, so resolve all three values, making sure
        // to use clean copies of the date object since it may be
        // modified when creating the attributes
        this.currentTime = new TimeAttribute(time);
        this.currentDate = new DateAttribute(new Date(millis));
        this.currentDateTime = new DateTimeAttribute(new Date(millis));

        return -1;
    }

    /**
     * Return available attribute values of the selected category.
     * 
     * @param category the category the attribute value(s) must be in
     * @param type the type of the attribute value(s) to find
     * @param id the id of the attribute value(s) to find
     * @param issuer the issuer of the attribute value(s) to find or null
     *                
     * @return a result containing a bag either empty because no values were
     * found or containing at least one value, or status associated with an
     * Indeterminate result
     */
    public EvaluationResult getAttribute(URI category, URI type, URI id, 
            URI issuer) {
        Set<RequestElement> elements = this.requestElements.get(category);
        if (elements == null) {
            elements = RequestElement.EMPTY_SET;
        }
        return getGenericAttributes(category, type, id, issuer, 
                elements, false);
    }

    /**
     * Helper function for the resource, action, environment and delegate 
     * methods to get an attribute.
     *
     * @param category  the category the attribute value(s) must be in
     * @param type the type of the attribute value(s) to find
     * @param id  the id of the attribute value(s) to find
     * @param issuer  the issuer of the attribute value(s) to find or null
     * @param elements a set of <code>RequestElement</code>s to search for
     *                the requested attribute.
     * @param local  get only local attributes, don't call the 
     *               AttributeFinder.
     *
     * @return a result containing a bag either empty because no values were
     * found or containing at least one value, or status associated with an
     * Indeterminate result.
     * 
     */
    private EvaluationResult getGenericAttributes(URI category, URI type, 
            URI id, URI issuer, Set<RequestElement> elements, boolean local) {

        Iterator<RequestElement> iter = elements.iterator();
        Set<Attribute> attrSet = new HashSet<Attribute>();
        while (iter.hasNext()) {
            RequestElement element = iter.next();
            // try to find the id
            Map<URI, Set<Attribute>> map = element.getAttributes();
            if (map.containsKey(id)) {
                attrSet.addAll(map.get(id));
            }
        }

        if (attrSet.isEmpty()) {
            if (local) {
                return new EvaluationResult(BagAttribute.createEmptyBag(type));
            }
            // the request didn't have an attribute with that id, so we should
            // try asking the attribute finder
            return callHelper(category, type, id, issuer, elements);
        }

        // now go through each, considering each Attribute object
        List<AttributeValue> attributes = new ArrayList<AttributeValue>();
        Iterator<Attribute> it = attrSet.iterator();

        while (it.hasNext()) {
            Attribute attr = it.next();

            // make sure the type and issuer are correct
            if ((attr.getValue().getType().equals(type)) &&
                    ((issuer == null) ||
                            ((attr.getIssuer() != null) &&
                                    (attr.getIssuer().equals(issuer.toString()))))) {

                // if we got here, then we found a match, so we want to pull
                // out the values and put them in out list
                attributes.add(attr.getValue());
            }
        }

        // see if we found any acceptable attributes
        if (attributes.size() == 0) {
            if (local) {
                return new EvaluationResult(BagAttribute.createEmptyBag(type));
            }
            // we failed to find any that matched the type/issuer, or all the
            // Attribute types were empty...so ask the finder
//            if (logger.isLoggable(Level.FINE)) {
//                logger.fine("Attribute not in request: " + id.toString() +
//                " ... querying AttributeFinder");
//            }
            if (logger.isDebugEnabled()) {
                logger.debug("Attribute not in request: " + id.toString() + " category " + category + 
                " ... querying AttributeFinder");
            }

            return callHelper(category, type, id, issuer, elements);
        }

        // if we got here, then we found at least one useful AttributeValue
        return new EvaluationResult(new BagAttribute(type, attributes));
    }

    /**
     * Protected helper that calls the finder if it's non-null, or else returns
     * an empty bag. (Proctected so it can be overridden by subclasses).
     * 
     * @param category  the category the attribute value(s) must be in or null
     * @param type the type of the attribute value(s) to find or null 
     * @param id  the id of the attribute value(s) to find or null
     * @param issuer  the issuer of the attribute value(s) to find or null
     * @param elements a set of <code>RequestElement</code>s to search for the
     *                requested attribute.
     *
     * @return  an <code>EvaluationResult</code> containing the requested
     *          attribute or an empty bag. 
     */
    protected EvaluationResult callHelper(URI category, URI type, URI id, 
            URI issuer, Set<RequestElement> elements) {
        if (this.aFinder != null) {
            return this.aFinder.findAttribute(category, type, id, issuer,
                    this);
        }
        if (this.afinderActive) {
            logger.warn("Context tried to invoke AttributeFinder but was " 
                    + "not configured with one");
        }
        return new EvaluationResult(BagAttribute.createEmptyBag(type));
    }

    /**
     * Returns the attribute value(s) retrieved using the given XPath
     * expression.
     *
     * @param contextPath the XPath expression to search
     * @param namespaceNode the DOM node defining namespace mappings to use,
     *                      or null if mappings come from the context root
     * @param type the type of the attribute value(s) to find
     * @param xpathVersion the version of XPath to use
     *
     * @return a result containing a bag either empty because no values were
     * found or containing at least one value, or status associated with an
     * Indeterminate result
     */
    public EvaluationResult getAttribute(String contextPath,
            Node namespaceNode, URI type,
            String xpathVersion) {
        if (this.aFinder != null) {
            return this.aFinder.findAttribute(contextPath, namespaceNode, 
                    type, this, xpathVersion);
        }
        logger.warn("Context tried to invoke AttributeFinder but was " +
        "not configured with one");

        return new EvaluationResult(BagAttribute.createEmptyBag(type));
    }
    
    /**
     * Get the decision.
     * 
     * @return The <code>int</code> value of the decision according to
     *         the <code>Result</code> class. 
     */
    public int getDecision() {
        return this.delegationDecision;
    }

    /**
     * Get the delegation depth.
     * 
     *  @return  The <code>int</code> value specifying the number of nodes
     *           in the reduction graph until now (not including this one).
     */
    public int getDelegationDepth() {
        return this.delegationDepth;
    }

    /**
     * Get a whole category.
     * 
     * @param category  The name of the category. If the category does
     *                  not exist return an empty set
     * 
     * @return The <code>Set</code> of <code>RequestElement</code>s with
     *         the matching category.
     */
    public Set<RequestElement> getCategory(URI category) {
        if (this.requestElements.containsKey(category)) {
            return new HashSet<RequestElement>(this.requestElements.get(category));
        }
        return RequestElement.EMPTY_SET;
    }

    /**
     * @return  the <code>Set</code> of <code>RequestElements</code>
     *          containing the attributes that should be included 
     *          in the result.
     */
    public Set<RequestElement> getIncludedAttrs() {
        return this.includedAttributes;
    }

    /**
     * @return  the <code>Map</code> of <code>RequestElements</code>
     *          defining this request.
     */
    public Map<URI, Set<RequestElement>> getRequestElements() {
        return Collections.unmodifiableMap(this.requestElements);
    }

    /**
     * Save the parent <code>PolicySet</code> in this evaluation context 
     * for doing reduction of delegated policies if that becomes necessary.
     * 
     * @param pps  the parent policy set
     */
    public void saveParentPolicySet(AbstractPolicy pps) {
        if (pps instanceof PolicySet) {
            this.parentPolicySets.push(pps);           
        } else if (pps instanceof PolicyReference) {
            PolicyReference pr = (PolicyReference)pps;
            if (pr.getReferenceType() != PolicyReference.POLICYSET_REFERENCE) {
                //this should never happen
                throw new RuntimeException("Tried to save a Reference "
                        + "to a Policy as a PolicySet");
            }
            this.parentPolicySets.push(pps);
        } else {
            throw new RuntimeException("Tried to save " 
                    + this.getClass().getName() + " as a PolicySet");
        }
    }


    /**
     * Create a reduction graph for the current parent PolicySet.
     *
     */
    public void createReductionGraph() {
        this.reductionGraphs.push(new ReductionGraph(getParentPolicySet()));
    }


    /**
     * @return  The current reduction graph or null if there is none.
     */
    public ReductionGraph getReductionGraph() {
        if (this.reductionGraphs != null && !this.reductionGraphs.empty()) {
            return (ReductionGraph)this.reductionGraphs.peek();
        } 
        return null;
    }

    /**
     * Remove the current <code>ReductionGraph</code> from the stack. 
     */
    public void popReductionGraph() {
        if (!this.reductionGraphs.isEmpty()) {
            this.reductionGraphs.pop();
        } 
    }

    /**
     * Returns the root <code>PolicySet</code> for this evaluation context.
     *  If there is none, return null.
     *
     * @return  the root policy set or null.
     */
    public AbstractPolicy getParentPolicySet() {
        if (!this.parentPolicySets.empty()) {
            return (AbstractPolicy) this.parentPolicySets.peek();
        } 
        return null;
    }

    /**
     * Remove the current parent <code>PolicySet</code> from the stack
     * of parent policy sets. 
     */
    public void popParentPolicySet() {
        if (!this.parentPolicySets.isEmpty()) {
            this.parentPolicySets.pop();
        } 
    }

    /**
     * Checks whether a <code>Policy</code> or <code>PolicySet</code> 
     * supports a revocation of a specific Policy of PolicySet 
     * in this context.
     * 
     * @param supporting  The policy or policy set that could support 
     *                    a revocation.
     * @param candidate  The id of the policy or policy set that is candidate
     *                   for revocation.
     * 
     * @return  true if the policy/policy set supports a revocation, 
     *          false otherwise.
     */
    public boolean supportsRevocation(AbstractPolicy supporting, 
            URI candidate) {
        if (this.rFinder != null && this.rfinderActive) {
            //deactivate revocation finder to avoid infinite loops
            // of policies revoking themselves
            this.rfinderActive = false;
            boolean result = this.rFinder.supportsRevocation(supporting,
                    candidate, this);
            this.rfinderActive = true;
            return result;
        }
        // since there is no revocation finder, no policy can be
        // revoked.
        logger.warn("Context tried to invoke RevocationFinder but was " 
                + "not configured with one");
        return false;
    }

    /**
     * Add new inactive PolicyId to the Map
     * @param policyId  the id of the new inactive policy 
     */
    public void addInactivePolicyId(URI policyId) {
        this.inactivePolicyIds.add(policyId);
    }

    /**
     * Return an unmodifiable <code>Set</code> of <code>URI</code>s of
     * inactive policies
     * @return  the inactive policies
     */
    public Set<URI> getInactivePolicyIds() {
        return Collections.unmodifiableSet(
                new HashSet<URI>(this.inactivePolicyIds)); 
    }

    /**
     * Signal a new event to this EvaluationCtx.
     * BasicEvaluationCtx does nothing with this signal.
     * 
     * @param element  The new event.
     */
    public void newEvent(Object element) {
        //BasicEvaluationCtx does nothing with this signal.
    }

    /**
     * Signal that an event has finished and pass the result.
     * BasicEvaluationCtx does nothing with this signal.
     * 
     * @param result  The result of the finished event.
     */
    public void closeCurrentEvent(Result result) {
        //BasicEvaluationCtx does nothing with this signal.
    } 

    /**
     * Signal that an event has finished and pass the result.
     * BasicEvaluationCtx does nothing with this signal.
     * 
     * @param result  The result of the finished event.
     */
    public void closeCurrentEvent(MatchResult result) {
        //BasicEvaluationCtx does nothing with this signal.
    } 

    /**
     * Signal that an event has finished and pass the result
     * which is a <code>EvaluationResult</code>
     * BasicEvaluationCtx does nothing with this signal.
     * 
     * @param result  The result of the finished event.
     */
    public void closeCurrentEvent(EvaluationResult result) {
        //BasicEvaluationCtx does nothing with this signal.
    }

    /**
     * Signal that an event has finished with a <code>String</code> message.
     * BasicEvaluationCtx does nothing with this signal.
     * 
     * @param message  The message.
     */
    public void closeCurrentEvent(String message) {
        //BasicEvaluationCtx does nothing with this signal.
    }

    /**
     * Signal that an event has finished with no result.
     * BasicEvaluationCtx does nothing with this signal.
     */
    public void closeCurrentEvent() {
        //BasicEvaluationCtx does nothing with this signal.
    }
}