/*
 * @(#)PolicyReference.java
 *
 * Copyright 2003-2005 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 org.wso2.balana;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.balana.combine.CombiningAlgorithm;

import org.wso2.balana.ctx.AbstractResult;
import org.wso2.balana.ctx.EvaluationCtx;
import org.wso2.balana.ctx.ResultFactory;
import org.wso2.balana.ctx.xacml2.Result;
import org.wso2.balana.ctx.Status;

import org.wso2.balana.finder.PolicyFinder;
import org.wso2.balana.finder.PolicyFinderResult;

import java.io.OutputStream;
import java.io.PrintStream;

import java.net.URI;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;

/**
 * This class is used as a placeholder for the PolicyIdReference and PolicySetIdReference fields in
 * a PolicySetType. When a reference is used in a policy set, it is telling the PDP to use an
 * external policy in the current policy. Each time the PDP needs to evaluate that policy reference,
 * it asks the policy finder for the policy. Typically the policy finder will have cached the
 * referenced policy, so this isn't too slow.
 * <p>
 * NOTE: all of the accessor methods, the match method, and the evaluate method require this class
 * to ask its <code>PolicyFinder</code> for the referenced policy, which can be a slow operation.
 * Care should be taken, therefore in calling these methods too often. Also note that it's not safe
 * to cache the results of these calls, since the referenced policy may change.
 * 
 * @since 1.0
 * @author Seth Proctor
 */
public class PolicyReference extends AbstractPolicy {

    /**
     * Identifies this as a reference to a <code>Policy</code>
     */
    public static final int POLICY_REFERENCE = 0;

    /**
     * Identifies this as a reference to a <code>PolicySet</code>
     */
    public static final int POLICYSET_REFERENCE = 1;

    // the reference
    private URI reference;

    // the reference type
    private int policyType;

    // and version constraints on this reference
    private VersionConstraints constraints;

    // the finder to use in finding the referenced policy
    private PolicyFinder finder;

    // the meta-data for the parent policy
    private PolicyMetaData parentMetaData;

    // the logger we'll use for all messages
    private static final Log logger = LogFactory.getLog(PolicyReference.class);

    /**
     * Creates a new <code>PolicyReference</code> instance. This has no constraints on version
     * matching. Note that an XACML 1.x reference may not have any constraints.
     * 
     * @param reference the reference to the policy
     * @param policyType one of the two fields in this class
     * @param finder the <code>PolicyFinder</code> used to handle the reference
     * @param parentMetaData the meta-data associated with the containing (parent) policy
     * 
     * @throws IllegalArgumentException if the input policyType isn't valid
     */
    public PolicyReference(URI reference, int policyType, PolicyFinder finder,
            PolicyMetaData parentMetaData) throws IllegalArgumentException {
        this(reference, policyType, new VersionConstraints(null, null, null), finder,
                parentMetaData);
    }

    /**
     * Creates a new <code>PolicyReference</code> instance with version constraints. Note that an
     * XACML 1.x reference may not have any constraints.
     * 
     * @param reference the reference to the policy
     * @param policyType one of the two fields in this class
     * @param constraints any optional constraints on the version of the referenced policy (this is
     *            never null, but it may impose no constraints, and in fact will never impose
     *            constraints when used from a pre-2.0 XACML policy)
     * @param finder the <code>PolicyFinder</code> used to handle the reference
     * @param parentMetaData the meta-data associated with the containing (parent) policy
     * 
     * @throws IllegalArgumentException if the input policyType isn't valid
     */
    public PolicyReference(URI reference, int policyType, VersionConstraints constraints,
            PolicyFinder finder, PolicyMetaData parentMetaData) throws IllegalArgumentException {

        // check if input policyType is a valid value
        if ((policyType != POLICY_REFERENCE) && (policyType != POLICYSET_REFERENCE))
            throw new IllegalArgumentException("Input policyType is not a" + "valid value");

        this.reference = reference;
        this.policyType = policyType;
        this.constraints = constraints;
        this.finder = finder;
        this.parentMetaData = parentMetaData;
    }

    /**
     * Creates an instance of a <code>PolicyReference</code> object based on a DOM node.
     * 
     * @deprecated As of 2.0 you should avoid using this method and should instead use the version
     *             that takes a <code>PolicyMetaData</code> instance. This method will only work for
     *             XACML 1.x policies.
     * 
     * @param root the DOM root of a PolicyIdReference or a PolicySetIdReference XML type
     * @param finder the <code>PolicyFinder</code> used to handle the reference
     * @return an instance of PolicyReference
     * @exception ParsingException if the node is invalid
     */
    public static PolicyReference getInstance(Node root, PolicyFinder finder)
            throws ParsingException {
        return getInstance(root, finder, new PolicyMetaData());
    }

    /**
     * Creates an instance of a <code>PolicyReference</code> object based on a DOM node.
     * 
     * @param root the DOM root of a PolicyIdReference or a PolicySetIdReference XML type
     * @param finder the <code>PolicyFinder</code> used to handle the reference
     * @param metaData the meta-data associated with the containing policy
     * @return an instance of PolicyReference
     * @exception ParsingException if the node is invalid
     */
    public static PolicyReference getInstance(Node root, PolicyFinder finder,
            PolicyMetaData metaData) throws ParsingException {

        URI reference;
        int policyType;

        // see what type of reference we are
        String name = DOMHelper.getLocalName(root);
        if (name.equals("PolicyIdReference")) {
            policyType = POLICY_REFERENCE;
        } else if (name.equals("PolicySetIdReference")) {
            policyType = POLICYSET_REFERENCE;
        } else {
            throw new ParsingException("Unknown reference type: " + name);
        }

        // next get the reference
        try {
            reference = new URI(root.getFirstChild().getNodeValue());
        } catch (Exception e) {
            throw new ParsingException("Invalid URI in Reference", e);
        }

        // now get any constraints
        NamedNodeMap map = root.getAttributes();

        String versionConstraint = null;
        Node versionNode = map.getNamedItem("Version");
        if (versionNode != null)
            versionConstraint = versionNode.getNodeValue();

        String earlyConstraint = null;
        Node earlyNode = map.getNamedItem("EarliestVersion");
        if (earlyNode != null)
            earlyConstraint = earlyNode.getNodeValue();

        String lateConstraint = null;
        Node lateNode = map.getNamedItem("LatestVersion");
        if (lateNode != null)
            lateConstraint = lateNode.getNodeValue();

        VersionConstraints constraints = new VersionConstraints(versionConstraint, earlyConstraint,
                lateConstraint);

        // finally, create the reference
        return new PolicyReference(reference, policyType, constraints, finder, metaData);
    }

    /**
     * Returns the reference identifier used to resolve the policy.
     * 
     * @return the reference <code>URI</code>
     */
    public URI getReference() {
        return reference;
    }

    /**
     * Returns the version constraints associated with this reference. This will never be null,
     * though the constraints may be empty.
     * 
     * @return the version constraints
     */
    public VersionConstraints getConstraints() {
        return constraints;
    }

    /**
     * Returns whether this is a reference to a policy or to a policy set.
     * 
     * @return the reference type, either <code>POLICY_REFERENCE</code> or
     *         <code>POLICYSET_REFERENCE</code>
     */
    public int getReferenceType() {
        return policyType;
    }

    /**
     * Returns the id of this policy. If the policy is invalid or can't be retrieved, then a runtime
     * exception is thrown.
     * 
     * @return the policy id
     * 
     * @throws ProcessingException if the referenced policy can't be retrieved
     */
    public URI getId() {
        return resolvePolicy().getId();
    }

    /**
     * Returns the version of this policy. If the policy is invalid or can't be retrieved, then a
     * runtime exception is thrown.
     * 
     * @return the policy version
     * 
     * @throws ProcessingException if the referenced policy can't be retrieved
     */
    public String getVersion() {
        return resolvePolicy().getVersion();
    }

    /**
     * Returns the combining algorithm used by this policy. If the policy is invalid or can't be
     * retrieved, then a runtime exception is thrown.
     * 
     * @return the combining algorithm
     * 
     * @throws ProcessingException if the referenced policy can't be retrieved
     */
    public CombiningAlgorithm getCombiningAlg() {
        return resolvePolicy().getCombiningAlg();
    }

    /**
     * Returns the given description of this policy or null if there is no description. If the
     * policy is invalid or can't be retrieved, then a runtime exception is thrown.
     * 
     * @return the description or null
     * 
     * @throws ProcessingException if the referenced policy can't be retrieved
     */
    public String getDescription() {
        return resolvePolicy().getDescription();
    }

    /**
     * Returns the target for this policy. If the policy is invalid or can't be retrieved, then a
     * runtime exception is thrown.
     * 
     * @return the policy's target
     * 
     * @throws ProcessingException if the referenced policy can't be retrieved
     */
    public AbstractTarget getTarget() {
        return resolvePolicy().getTarget();
    }

    /**
     * Returns the default version for this policy. If the policy is invalid or can't be retrieved,
     * then a runtime exception is thrown.
     * 
     * @return the policy's default version
     * 
     * @throws ProcessingException if the referenced policy can't be retrieved
     */
    public String getDefaultVersion() {
        return resolvePolicy().getDefaultVersion();
    }

    /**
     * Returns the child policy nodes under this node in the policy tree. If the policy is invalid
     * or can't be retrieved, then a runtime exception is thrown.
     * 
     * @return the <code>List</code> of child policy nodes
     * 
     * @throws ProcessingException if the referenced policy can't be retrieved
     */
    public List getChildren() {
        return resolvePolicy().getChildren();
    }

    /**
     * Returns the child policy nodes and their associated parameters. If the policy is invalid or
     * can't be retrieved, then a runtime exception is thrown.
     * 
     * @return a <code>List</code> of <code>CombinerElement</code>s
     * 
     * @throws ProcessingException if the referenced policy can't be retrieved
     */
    public List getChildElements() {
        return resolvePolicy().getChildElements();
    }

    /**
     * Returns the Set of obligations for this policy, which may be empty if there are no
     * obligations. If the policy is invalid or can't be retrieved, then a runtime exception is
     * thrown.
     * 
     * @return the policy's obligations
     * 
     * @throws ProcessingException if the referenced policy can't be retrieved
     */
    public Set getObligationExpressions() {
        return resolvePolicy().getObligationExpressions();
    }

    /**
     * Returns the meta-data associated with this policy. If the policy is invalid or can't be
     * retrieved, then a runtime exception is thrown. Note that this is the meta-data for the
     * referenced policy, not the meta-data for the parent policy (which is what gets provided to
     * the constructors of this class).
     * 
     * @return the policy's meta-data
     * 
     * @throws ProcessingException if the referenced policy can't be retrieved
     */
    public PolicyMetaData getMetaData() {
        return resolvePolicy().getMetaData();
    }

    /**
     * Given the input context sees whether or not the request matches this policy. This must be
     * called by combining algorithms before they evaluate a policy. This is also used in the
     * initial policy finding operation to determine which top-level policies might apply to the
     * request. If the policy is invalid or can't be retrieved, then a runtime exception is thrown.
     * 
     * @param context the representation of the request
     * 
     * @return the result of trying to match the policy and the request
     */
    public MatchResult match(EvaluationCtx context) {
        try {
            return getTarget().match(context);
        } catch (ProcessingException pe) {
            // this means that we couldn't resolve the policy
            ArrayList code = new ArrayList();
            code.add(Status.STATUS_PROCESSING_ERROR);
            Status status = new Status(code, "couldn't resolve policy ref");
            return new MatchResult(MatchResult.INDETERMINATE, status);
        }
    }

    /**
     * Private helper method that tried to resolve the policy
     */
    private AbstractPolicy resolvePolicy() {
        // see if this reference was setup with a finder
        if (finder == null) {
            if (logger.isWarnEnabled()) {
                logger.warn("PolicyReference with id " + reference.toString()
                        + " was queried but was " + "not configured with a PolicyFinder");
            }

            throw new ProcessingException("couldn't find the policy with " + "a null finder");
        }

        PolicyFinderResult pfr = finder.findPolicy(reference, policyType, constraints,
                parentMetaData);

        if (pfr.notApplicable())
            throw new ProcessingException("couldn't resolve the policy");

        if (pfr.indeterminate())
            throw new ProcessingException("error resolving the policy");

        return pfr.getPolicy();
    }

    /**
     * Tries to evaluate the policy by calling the combining algorithm on the given policies or
     * rules. The <code>match</code> method must always be called first, and must always return
     * MATCH, before this method is called.
     * 
     * @param context the representation of the request
     * 
     * @return the result of evaluation
     */
    public AbstractResult evaluate(EvaluationCtx context) {
        // if there is no finder, then we return NotApplicable
        if (finder == null){
            //return new Result(Result.DECISION_NOT_APPLICABLE, context.getResourceId().encode());
            return ResultFactory.getFactory().getResult(Result.DECISION_NOT_APPLICABLE, context);
        }

        PolicyFinderResult pfr = finder.findPolicy(reference, policyType, constraints,
                parentMetaData);

        // if we found nothing, then we return NotApplicable
        if (pfr.notApplicable()){
            //return new Result(Result.DECISION_NOT_APPLICABLE, context.getResourceId().encode());
            return ResultFactory.getFactory().getResult(Result.DECISION_NOT_APPLICABLE, context);
        }
        // if there was an error, we return that status data
        if (pfr.indeterminate()){
//            return new Result(Result.DECISION_INDETERMINATE, pfr.getStatus(), context
//                    .getResourceId().encode());
            return ResultFactory.getFactory().getResult(Result.DECISION_INDETERMINATE, pfr.getStatus(), context);
        }
        // we must have found a policy
        return pfr.getPolicy().evaluate(context);
    }

    /**
     * Encodes this <code>PolicyReference</code> into its XML form
     *
     * @return <code>String</code>
     */
    public String encode() {
        StringBuilder builder = new StringBuilder();
        encode(builder);
        return builder.toString();
    }

    /**
     * Encodes this <code>PolicyReference</code> into its XML form and writes this out to the provided
     * <code>StringBuilder<code>
     *
     * @param builder string stream into which the XML-encoded data is written
     */
    public void encode(StringBuilder builder) {

        if (policyType == POLICY_REFERENCE) {
            builder.append("<PolicyIdReference").append(encodeConstraints()).append(">").
                            append(reference.toString()).append("</PolicyIdReference>");
        } else {
            builder.append("<PolicySetIdReference").append(encodeConstraints()).append(">").
                    append(reference.toString()).append("</PolicySetIdReference>");
        }
    }

    /**
     * Private helper method that encodes the variable constraints info. Note that if this is a
     * pre-2.0 policy the constraints are always null, so nothing will be added here.
     * @return
     */
    private String encodeConstraints() {
        String str = "";
        VersionConstraints version = getConstraints();

        String v = version.getVersionConstraint();
        if (v != null)
            str += " Version=\"" + v + "\"";

        String e = version.getEarliestConstraint();
        if (e != null)
            str += " EarliestVersion=\"" + e + "\"";

        String l = version.getLatestConstraint();
        if (l != null)
            str += " LatestVersion=\"" + l + "\"";

        return str;
    }

}
