org.apache.openaz.xacml.std.dom.DOMResponse.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.openaz.xacml.std.dom.DOMResponse.java

Source

/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 *
 */

package org.apache.openaz.xacml.std.dom;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.math.BigInteger;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Iterator;

import javax.security.auth.x500.X500Principal;
import javax.xml.XMLConstants;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.openaz.xacml.api.Advice;
import org.apache.openaz.xacml.api.Attribute;
import org.apache.openaz.xacml.api.AttributeAssignment;
import org.apache.openaz.xacml.api.AttributeCategory;
import org.apache.openaz.xacml.api.AttributeValue;
import org.apache.openaz.xacml.api.Decision;
import org.apache.openaz.xacml.api.IdReference;
import org.apache.openaz.xacml.api.Identifier;
import org.apache.openaz.xacml.api.MissingAttributeDetail;
import org.apache.openaz.xacml.api.Obligation;
import org.apache.openaz.xacml.api.Response;
import org.apache.openaz.xacml.api.Result;
import org.apache.openaz.xacml.api.SemanticString;
import org.apache.openaz.xacml.api.Status;
import org.apache.openaz.xacml.api.StatusCode;
import org.apache.openaz.xacml.api.StatusDetail;
import org.apache.openaz.xacml.api.XACML3;
import org.apache.openaz.xacml.std.StdMutableResponse;
import org.apache.openaz.xacml.std.StdResponse;
import org.apache.openaz.xacml.std.StdStatusCode;
import org.apache.openaz.xacml.std.datatypes.ExtendedNamespaceContext;
import org.apache.openaz.xacml.std.datatypes.XPathExpressionWrapper;
import org.apache.openaz.xacml.std.json.JSONStructureException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * DOMResponse is used to convert XML into {@link org.apache.openaz.xacml.api.Response} objects and
 * {@link org.apache.openaz.xacml.api.Response} objects into XML strings. Instances of this class are never
 * created. The {@link org.apache.openaz.xacml.api.Response} objects returned by this class are instances of
 * {@link org.apache.openaz.xacml.std.StdMutableResponse}. {@link org.apache.openaz.xacml.api.Response} objects
 * are generated by loading a file or XML Node tree representing the Request. In normal product operation this
 * is not used to generate new instances because the PDP generates
 * {@link org.apache.openaz.xacml.std.StdResponse} objects internally. Those objects are converted to XML
 * strings for transmission through the RESTful Web Service using the <code>convert</code> method in this
 * class.
 */
public class DOMResponse {
    private static final Log logger = LogFactory.getLog(DOMResponse.class);

    protected DOMResponse() {
    }

    /**
     * Creates a new <code>DOMResponse</code> by parsing the given <code>Node</code> representing a XACML
     * Response element.
     *
     * @param nodeResponse the <code>Node</code> representing the XACML Response element
     * @return a new <code>DOMResponse</code> parsed from the given <code>Node</code>
     * @throws DOMStructureException if the conversion cannot be made
     */
    public static Response newInstance(Node nodeResponse) throws DOMStructureException {
        Element elementResponse = DOMUtil.getElement(nodeResponse);
        boolean bLenient = DOMProperties.isLenient();

        StdMutableResponse mutableResponse = new StdMutableResponse();

        NodeList children = elementResponse.getChildNodes();
        int numChildren;
        boolean sawResult = false;
        if (children != null && (numChildren = children.getLength()) > 0) {
            for (int i = 0; i < numChildren; i++) {
                Node child = children.item(i);
                if (DOMUtil.isElement(child)) {
                    if (DOMUtil.isInNamespace(child, XACML3.XMLNS)
                            && XACML3.ELEMENT_RESULT.equals(child.getLocalName())) {
                        mutableResponse.add(DOMResult.newInstance(child));
                        sawResult = true;
                    } else {
                        if (!bLenient) {
                            throw DOMUtil.newUnexpectedElementException(child, nodeResponse);
                        }
                    }
                }
            }
        }
        if (!sawResult && !bLenient) {
            throw DOMUtil.newMissingElementException(nodeResponse, XACML3.XMLNS, XACML3.ELEMENT_RESULT);
        }

        return new StdResponse(mutableResponse);
    }

    /**
     * Change XACML2 into XACML3
     *
     * @param nodeResponse
     * @return
     * @throws DOMStructureException
     */
    public static boolean repair(Node nodeResponse) throws DOMStructureException {
        Element elementResponse = DOMUtil.getElement(nodeResponse);
        boolean result = false;

        NodeList children = elementResponse.getChildNodes();
        int numChildren;
        boolean sawResult = false;
        if (children != null && (numChildren = children.getLength()) > 0) {
            for (int i = 0; i < numChildren; i++) {
                Node child = children.item(i);
                if (DOMUtil.isElement(child)) {
                    if (DOMUtil.isInNamespace(child, XACML3.XMLNS)
                            && XACML3.ELEMENT_RESULT.equals(child.getLocalName())) {
                        result = DOMResult.repair(child) || result;
                        sawResult = true;
                    } else {
                        logger.warn("Unexpected element " + child.getNodeName());
                        elementResponse.removeChild(child);
                        result = true;
                    }
                }
            }
        }

        if (!sawResult) {
            throw DOMUtil.newMissingElementException(nodeResponse, XACML3.XMLNS, XACML3.ELEMENT_RESULT);
        }
        return result;
    }

    public static Response load(String xmlString) throws DOMStructureException {
        try (InputStream is = new ByteArrayInputStream(xmlString.getBytes("UTF-8"))) {
            return load(is);
        } catch (Exception ex) {
            throw new DOMStructureException("Exception loading String Response: " + ex.getMessage(), ex);
        }
    }

    /**
     * Read a file containing an XML representation of a Response and parse it into a
     * {@link org.apache.openaz.xacml.api.Response} Object. This is used only for testing since Responses in
     * the normal environment are generated by the PDP code.
     *
     * @param fileResponse
     * @return
     * @throws DOMStructureException
     */
    public static Response load(File fileResponse) throws DOMStructureException {
        try (FileInputStream fis = new FileInputStream(fileResponse)) {
            return DOMResponse.load(fis);
        } catch (Exception e) {
            throw new DOMStructureException("File: " + fileResponse.getName() + " " + e.getMessage());
        }
    }

    /**
     * Loads a response from java nio Path object.
     *
     * @param fileResponse
     * @return
     * @throws DOMStructureException
     */
    public static Response load(Path fileResponse) throws DOMStructureException {
        try {
            return DOMResponse.load(Files.newInputStream(fileResponse));
        } catch (Exception e) {
            throw new DOMStructureException(e);
        }
    }

    /**
     * Read a file containing an XML representation of a Response and parse it into a
     * {@link org.apache.openaz.xacml.api.Response} Object. This is used only for testing since Responses in
     * the normal environment are generated by the PDP code.
     *
     * @param fileResponse
     * @return
     * @throws DOMStructureException
     */
    public static Response load(InputStream is) throws DOMStructureException {
        Response request = null;
        try {
            Document document = DOMUtil.loadDocument(is);
            if (document == null) {
                throw new DOMStructureException("Null document returned");
            }

            Node rootNode = DOMUtil.getFirstChildElement(document);
            if (rootNode == null) {
                throw new DOMStructureException("No child in document");
            }

            if (DOMUtil.isInNamespace(rootNode, XACML3.XMLNS)) {
                if (XACML3.ELEMENT_RESPONSE.equals(rootNode.getLocalName())) {
                    request = DOMResponse.newInstance(rootNode);
                    if (request == null) {
                        throw new DOMStructureException("Failed to parse Response");
                    }
                } else {
                    throw DOMUtil.newUnexpectedElementException(rootNode);
                }
            } else {
                throw DOMUtil.newUnexpectedElementException(rootNode);
            }
        } catch (Exception ex) {
            throw new DOMStructureException("Exception loading Response from InputStream: " + ex.getMessage(), ex);
        }
        return request;
    }

    /**
     * Convert the {@link org.apache.openaz.xacml.api.Response} into an XML string. This is used only for
     * debugging. It assumes that pretty-printing is desired.
     *
     * @param response
     * @return
     * @throws Exception
     */
    public static String toString(Response response) throws Exception {
        return toString(response, true);
    }

    /**
     * Convert the {@link org.apache.openaz.xacml.api.Response} into an XML string. This is used only for
     * debugging. The caller chooses whether to pretty-print or not.
     *
     * @param response
     * @param prettyPrint
     * @return
     * @throws Exception
     */
    public static String toString(Response response, boolean prettyPrint) throws Exception {
        String outputString = null;
        try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
            DOMResponse.convert(response, os, prettyPrint);
            outputString = new String(os.toByteArray(), "UTF-8");
        } catch (Exception ex) {
            throw ex;
        }
        return outputString;
    }

    /**
     * Helper - recursively output StatusCode objects as XML.
     *
     * @param sb
     * @param statusCode
     * @param tabCount
     * @param prettyPrint
     */
    private static void outputStatusCode(StringBuilder sb, StatusCode statusCode, int tabCount,
            boolean prettyPrint) {
        String prettyPrintString = "";
        if (prettyPrint) {
            prettyPrintString = "\n";
            for (int i = 0; i < tabCount; i++) {
                prettyPrintString += "\t";
            }
        }

        sb.append(prettyPrintString);
        sb.append("<StatusCode");

        if (statusCode.getStatusCodeValue() != null) {
            sb.append(" Value=\"" + statusCode.getStatusCodeValue().stringValue() + "\"");
        }

        if (statusCode.getChild() == null) {
            // no child code, so finish off the StatusCode element now
            sb.append("/>");
        } else {
            // there is a child, so need to use the two-part notation for this StatusCode
            sb.append(">");
            outputStatusCode(sb, statusCode.getChild(), tabCount + 1, prettyPrint);
            sb.append(prettyPrintString);
            sb.append("</StatusCode>");
        }
    }

    /**
     * Helper: When outputting as XML string, get the value of a Value (within an AttributeValue) object as a
     * String. Most of these objects are SemanticStrings, but some are not and we cannot assume that in the
     * places where we need to generate the output.
     *
     * @param obj
     * @return String
     * @throws JSONStructureException
     * @throws DOMStructureException
     */
    private static String outputValueValue(Object obj) throws DOMStructureException {
        if (obj instanceof String || obj instanceof Boolean || obj instanceof Integer
                || obj instanceof BigInteger) {
            return obj.toString();
        } else if (obj instanceof Double) {
            Double d = (Double) obj;
            if (d == Double.NaN) {
                return "NaN";
            } else if (d == Double.POSITIVE_INFINITY) {
                return "INF";
            } else if (d == Double.NEGATIVE_INFINITY) {
                return "-INF";
            }
            return obj.toString();
        } else if (obj instanceof SemanticString) {
            return ((SemanticString) obj).stringValue();
        } else if (obj instanceof X500Principal || obj instanceof URI) {
            // something is very weird with X500Principal data type. If left on its own the output is a map
            // that includes encoding.
            return obj.toString();
        } else if (obj instanceof XPathExpressionWrapper) {

            XPathExpressionWrapper xw = (XPathExpressionWrapper) obj;

            return xw.getPath();
        } else {
            throw new DOMStructureException("Unhandled data type='" + obj.getClass().getName() + "'");
        }
    }

    /**
     * Helper: When outputting as XML string, extract any Namespace info from an AttributeValue.Value.Value
     * object. This must be done separately from getting the Value as a String because this info is put as an
     * attribute in the surrounding element. Currently only applies to XPathExpressionWrappers.
     *
     * @param valueObject
     * @return
     */
    private static String getNamespaces(Object valueObject) {
        String returnString = "";
        if (!(valueObject instanceof XPathExpressionWrapper)) {
            // value is not XPathExpression, so has no Namespace info in it
            return returnString;
        }
        XPathExpressionWrapper xw = (XPathExpressionWrapper) valueObject;

        ExtendedNamespaceContext namespaceContext = xw.getNamespaceContext();
        if (namespaceContext != null) {
            // get the list of all namespace prefixes
            Iterator<String> prefixIt = namespaceContext.getAllPrefixes();
            while (prefixIt.hasNext()) {
                String prefix = prefixIt.next();
                String namespaceURI = namespaceContext.getNamespaceURI(prefix);
                if (prefix == null || prefix.equals(XMLConstants.DEFAULT_NS_PREFIX)) {
                    returnString += " xmlns=\"" + namespaceURI + "\"";
                } else {
                    returnString += " xmlns:" + prefix + "=\"" + namespaceURI + "\"";
                }
            }

        }
        return returnString;
    }

    /**
     * Convert the {@link org.apache.openaz.xacml.api.Response} object into a string suitable for output in an
     * HTTPResponse. This method generates the output without any pretty-printing. This is the method normally
     * called by the Web Service for generating the output to the PEP through the RESTful interface.
     *
     * @param response
     * @param outputStream
     * @throws java.io.IOException
     * @throws DOMStructureException
     */
    public static void convert(Response response, OutputStream outputStream)
            throws IOException, DOMStructureException {
        convert(response, outputStream, false);
    }

    /**
     * Do the work of converting the {@link org.apache.openaz.xacml.api.Response} object to a string, allowing
     * for pretty-printing if desired.
     *
     * @param response
     * @param outputStream
     * @param prettyPrint
     * @throws java.io.IOException
     * @throws DOMStructureException
     */
    public static void convert(Response response, OutputStream outputStream, boolean prettyPrint)
            throws IOException, DOMStructureException {

        OutputStreamWriter osw = new OutputStreamWriter(outputStream);

        if (response == null) {
            throw new DOMStructureException("No Request in convert");
        }

        if (response.getResults() == null || response.getResults().size() == 0) {
            // must be at least one result
            throw new DOMStructureException("No Result in Response");
        }

        StringBuilder sb = new StringBuilder();

        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");

        if (prettyPrint)
            sb.append("\n");

        // response with attributes
        sb.append("<Response");

        // TODO include all Namespace info
        // Currently this is hard-coded for just the standard XACML namespaces, but ideally should use
        // Namespaces from incoming Request to get non-standard ones.
        sb.append(" xmlns=\"urn:oasis:names:tc:xacml:3.0:core:schema:wd-17\"");
        sb.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
        sb.append(" xsi:schemaLocation=\"urn:oasis:names:tc:xacml:3.0:core:schema:wd-17");
        sb.append(" http://docs.oasis-open.org/xacml/3.0/xacml-core-v3-schema-wd-17.xsd\"");

        // end of <Response>
        sb.append(">");

        // for each Result...
        for (Result result : response.getResults()) {

            if (prettyPrint)
                sb.append("\n\t");

            sb.append("<Result>");

            // Decision
            if (prettyPrint)
                sb.append("\n\t\t");

            if (result.getDecision() == null) {
                throw new DOMStructureException("Result missing Decision");
            }
            sb.append("<Decision>" + result.getDecision().toString() + "</Decision>");

            // Status
            Status status = result.getStatus();
            if (status != null) {
                if (prettyPrint)
                    sb.append("\n\t\t");
                sb.append("<Status>");

                // status code
                StatusCode statusCode = status.getStatusCode();
                Identifier statusCodeId;
                if (statusCode == null) {
                    throw new DOMStructureException("Status must have StatusCode");
                } else {
                    statusCodeId = statusCode.getStatusCodeValue();
                    // if there is a status code, it must agree with the decision
                    // Permit/Deny/NotAllowed must all be OK
                    // Indeterminate must not be OK
                    if (statusCodeId.equals(StdStatusCode.STATUS_CODE_OK.getStatusCodeValue())
                            && !(result.getDecision() == Decision.DENY || result.getDecision() == Decision.PERMIT
                                    || result.getDecision() == Decision.NOTAPPLICABLE)
                            || !statusCodeId.equals(StdStatusCode.STATUS_CODE_OK.getStatusCodeValue())
                                    && !(result.getDecision() == Decision.INDETERMINATE
                                            || result.getDecision() == Decision.INDETERMINATE_DENY
                                            || result.getDecision() == Decision.INDETERMINATE_DENYPERMIT
                                            || result.getDecision() == Decision.INDETERMINATE_PERMIT)) {
                        throw new DOMStructureException("StatusCode '" + statusCodeId.stringValue()
                                + "' does not match Decision '" + result.getDecision().toString());
                    }

                    outputStatusCode(sb, statusCode, 3, prettyPrint);
                }

                // status message
                if (status.getStatusMessage() != null) {

                    if (prettyPrint)
                        sb.append("\n\t\t\t");
                    sb.append("<StatusMessage>" + status.getStatusMessage() + "</StatusMessage>");

                }

                // status detail
                StatusDetail statusDetail = status.getStatusDetail();
                if (statusDetail != null) {
                    // cross-check that rules defined in XACML Core spec section 5.5.7 re: when StatusDetail
                    // may/may-not be included have been followed
                    if (status.isOk()) {
                        throw new DOMStructureException(
                                "Status '" + statusCodeId.stringValue() + "' must not return StatusDetail");
                    } else if (statusCodeId.stringValue().equals(XACML3.ID_STATUS_MISSING_ATTRIBUTE.stringValue())
                            && status.getStatusDetail().getMissingAttributeDetails() == null) {
                        throw new DOMStructureException("Status '" + statusCodeId.stringValue()
                                + "' has StatusDetail without MissingAttributeDetail");
                    } else if (statusCodeId.stringValue().equals(XACML3.ID_STATUS_SYNTAX_ERROR.stringValue())) {
                        throw new DOMStructureException(
                                "Status '" + statusCodeId.stringValue() + "' must not return StatusDetail");
                    } else if (statusCodeId.stringValue().equals(XACML3.ID_STATUS_PROCESSING_ERROR.stringValue())) {
                        throw new DOMStructureException(
                                "Status '" + statusCodeId.stringValue() + "' must not return StatusDetail");
                    }

                    // if included, StatusDetail is handled differently for each type of detail message and
                    // the contents are formatted into escaped XML rather than objects

                    if (result.getStatus().getStatusDetail().getMissingAttributeDetails() != null) {
                        if (prettyPrint)
                            sb.append("\n\t\t\t");
                        sb.append("<StatusDetail>");

                        for (MissingAttributeDetail mad : statusDetail.getMissingAttributeDetails()) {
                            if (mad.getAttributeId() == null || mad.getCategory() == null
                                    || mad.getDataTypeId() == null) {
                                throw new DOMStructureException(
                                        "MissingAttributeDetail is missing required AttributeId, Category or DataTypeId");
                            }
                            if (prettyPrint)
                                sb.append("\n\t\t\t\t");
                            sb.append("<MissingAttributeDetail");
                            sb.append(" Category=\"" + mad.getCategory().stringValue() + "\"");
                            sb.append(" AttributeId=\"" + mad.getAttributeId().stringValue() + "\"");
                            sb.append(" DataTypeId=\"" + mad.getDataTypeId().stringValue() + "\"");
                            if (mad.getIssuer() != null) {
                                sb.append(" Issuer=\"" + mad.getIssuer() + "\"");
                            }
                            sb.append(">");
                            if (mad.getAttributeValues() != null) {
                                for (AttributeValue<?> value : mad.getAttributeValues()) {
                                    if (prettyPrint) {
                                        sb.append("\n\t\t\t\t\t");
                                    }
                                    sb.append("<AttributeValue" + getNamespaces(value.getValue()) + ">"
                                            + outputValueValue(value.getValue()) + "</AttributeValue>");
                                }
                            }
                            if (prettyPrint) {
                                sb.append("\n\t\t\t\t");
                            }
                            sb.append("</MissingAttributeDetail>");
                        }

                        if (prettyPrint) {
                            sb.append("\n\t\t\t");
                        }
                        sb.append("</StatusDetail>");
                    }
                }

                if (prettyPrint)
                    sb.append("\n\t\t");
                sb.append("</Status>");
            }

            // Obligations
            if (result.getObligations() != null && result.getObligations().size() > 0) {
                if (prettyPrint)
                    sb.append("\n\t\t");
                sb.append("<Obligations>");

                for (Obligation obligation : result.getObligations()) {
                    if (obligation.getId() == null) {
                        throw new DOMStructureException("Obligation must have ObligationId");
                    }
                    if (prettyPrint)
                        sb.append("\n\t\t\t");
                    sb.append("<Obligation ObligationId=\"" + obligation.getId().stringValue() + "\">");

                    for (AttributeAssignment aa : obligation.getAttributeAssignments()) {
                        if (prettyPrint)
                            sb.append("\n\t\t\t\t");
                        sb.append("<AttributeAssignment");

                        if (aa.getAttributeId() == null) {
                            throw new DOMStructureException("Obligation AttributeAssignment must have AttributeId");
                        }
                        sb.append(" AttributeId=\"" + aa.getAttributeId().stringValue() + "\"");
                        if (aa.getDataTypeId() == null || aa.getAttributeValue() == null
                                || aa.getAttributeValue().getValue() == null) {
                            throw new DOMStructureException("Obligation AttributeAssignment '"
                                    + aa.getAttributeId().stringValue() + "' must have DataType and Value");
                        }
                        sb.append(" DataType=\"" + aa.getDataTypeId().stringValue() + "\""
                                + getNamespaces(aa.getAttributeValue().getValue()) + ">");
                        sb.append(outputValueValue(aa.getAttributeValue().getValue()));

                        sb.append("</AttributeAssignment>");
                    }

                    if (prettyPrint)
                        sb.append("\n\t\t\t");
                    sb.append("</Obligation>");
                }

                if (prettyPrint)
                    sb.append("\n\t\t");
                sb.append("</Obligations>");
            }

            // AssociatedAdvice
            if (result.getAssociatedAdvice() != null && result.getAssociatedAdvice().size() > 0) {
                if (prettyPrint)
                    sb.append("\n\t\t");
                sb.append("<AssociatedAdvice>");

                for (Advice advice : result.getAssociatedAdvice()) {
                    if (advice.getId() == null) {
                        throw new DOMStructureException("Advice must have AdviceId");
                    }
                    if (prettyPrint)
                        sb.append("\n\t\t\t");
                    sb.append("<Advice AdviceId=\"" + advice.getId().stringValue() + "\">");

                    for (AttributeAssignment aa : advice.getAttributeAssignments()) {
                        if (prettyPrint)
                            sb.append("\n\t\t\t\t");
                        sb.append("<AttributeAssignment");

                        if (aa.getAttributeId() == null) {
                            throw new DOMStructureException("Advice AttributeAssignment must have AttributeId");
                        }
                        sb.append(" AttributeId=\"" + aa.getAttributeId().stringValue() + "\"");
                        if (aa.getDataTypeId() == null || aa.getAttributeValue() == null
                                || aa.getAttributeValue().getValue() == null) {
                            throw new DOMStructureException("Advice AttributeAssignment '"
                                    + aa.getAttributeId().stringValue() + "' must have DataType and Value");
                        }
                        sb.append(" DataType=\"" + aa.getDataTypeId().stringValue() + "\""
                                + getNamespaces(aa.getAttributeValue().getValue()) + ">");
                        sb.append(outputValueValue(aa.getAttributeValue().getValue()));

                        sb.append("</AttributeAssignment>");
                    }

                    if (prettyPrint)
                        sb.append("\n\t\t\t");
                    sb.append("</Advice>");
                }

                if (prettyPrint)
                    sb.append("\n\t\t");
                sb.append("</AssociatedAdvice>");
            }

            // Attributes
            if (result.getAttributes() != null && result.getAttributes().size() > 0) {
                // this may include attributes with IncludeInResult=false!

                for (AttributeCategory category : result.getAttributes()) {
                    if (prettyPrint)
                        sb.append("\n\t\t");
                    if (category.getCategory() == null) {
                        throw new DOMStructureException("Attributes must have Category");
                    }
                    sb.append("<Attributes Category=\"" + category.getCategory().stringValue() + "\">");

                    for (Attribute attr : category.getAttributes()) {
                        if (!attr.getIncludeInResults()) {
                            // skip this one - do not include in results
                            continue;
                        }
                        if (prettyPrint)
                            sb.append("\n\t\t\t");
                        sb.append("<Attribute IncludeInResult=\"" + attr.getIncludeInResults() + "\"");
                        if (attr.getAttributeId() == null) {
                            throw new DOMStructureException("Attribute inf Category '"
                                    + category.getCategory().stringValue() + "' must have AttributeId");
                        }
                        sb.append(" AttributeId=\"" + attr.getAttributeId().stringValue() + "\"");
                        if (attr.getIssuer() == null) {
                            sb.append(">");
                        } else {
                            sb.append(" Issuer=\"" + attr.getIssuer() + "\">");
                        }

                        if (attr.getValues().size() == 0) {
                            throw new DOMStructureException(
                                    "Attribute '" + attr.getAttributeId() + "' must have at least one value");
                        }
                        for (AttributeValue<?> value : attr.getValues()) {
                            if (value.getDataTypeId() == null || value.getValue() == null) {
                                throw new DOMStructureException("Attribute '" + attr.getAttributeId()
                                        + "' has AttributeValue missing either DataType or Value");
                            }
                            if (prettyPrint)
                                sb.append("\n\t\t\t\t");
                            sb.append("<AttributeValue DataType=\"" + value.getDataTypeId().stringValue() + "\"");
                            if (value.getXPathCategory() != null) {
                                sb.append(" XPathCategory=\"" + value.getXPathCategory().stringValue() + "\"");
                            }
                            sb.append(">");

                            sb.append(outputValueValue(value.getValue()));

                            sb.append("</AttributeValue>");
                        }

                        if (prettyPrint)
                            sb.append("\n\t\t\t");
                        sb.append("</Attribute>");
                    }

                    if (prettyPrint)
                        sb.append("\n\t\t");
                    sb.append("</Attributes>");
                }

            }

            // PolicyIdentifierList
            Collection<IdReference> policyIds = result.getPolicyIdentifiers();
            Collection<IdReference> policySetIds = result.getPolicySetIdentifiers();
            if (policyIds != null && policyIds.size() > 0 || policySetIds != null && policySetIds.size() > 0) {
                if (prettyPrint)
                    sb.append("\n\t\t\t");
                sb.append("<PolicyIdentifierList>");

                // individual Ids
                for (IdReference idReference : policyIds) {
                    if (idReference == null) {
                        throw new DOMStructureException("PolicyIdentifiers has null IdReference");
                    }
                    if (prettyPrint)
                        sb.append("\n\t\t\t\t");
                    sb.append("<PolicyIdReference");
                    if (idReference.getVersion() != null) {
                        sb.append(" Version=\"" + idReference.getVersion().stringValue() + "\">");
                    } else {
                        sb.append(">");
                    }
                    sb.append(idReference.getId().stringValue());
                    sb.append("</PolicyIdReference>");
                }
                // Set Ids
                for (IdReference idReference : policySetIds) {
                    if (idReference == null) {
                        throw new DOMStructureException("PolicySetIdentifiers has null IdReference");
                    }
                    if (prettyPrint)
                        sb.append("\n\t\t\t\t");
                    sb.append("<PolicySetIdReference");
                    if (idReference.getVersion() != null) {
                        sb.append(" Version=\"" + idReference.getVersion().stringValue() + "\">");
                    } else {
                        sb.append(">");
                    }
                    sb.append(idReference.getId().stringValue());
                    sb.append("</PolicySetIdReference>");
                }

                if (prettyPrint)
                    sb.append("\n\t\t\t");
                sb.append("</PolicyIdentifierList>");
            }

            // end of Result
            if (prettyPrint)
                sb.append("\n\t");
            sb.append("</Result>");
        }

        if (prettyPrint)
            sb.append("\n");

        sb.append("</Response>");

        // all done

        osw.write(sb.toString());

        // force output
        osw.flush();

    }

    /**
     * Unit test program to load an XML file containing a XACML Response document.
     *
     * @param args the list of Response files to load and parse
     */
    public static void main(String[] args) {
        if (args.length > 0) {
            for (String xmlFileName : args) {
                File fileXml = new File(xmlFileName);
                if (!fileXml.exists()) {
                    System.err.println("Input file \"" + fileXml.getAbsolutePath() + "\" does not exist.");
                    continue;
                } else if (!fileXml.canRead()) {
                    System.err
                            .println("Permission denied reading input file \"" + fileXml.getAbsolutePath() + "\"");
                    continue;
                }
                System.out.println(fileXml.getAbsolutePath() + ":");
                try {
                    Document documentResponse = DOMUtil.loadDocument(fileXml);
                    assert documentResponse != null;

                    NodeList children = documentResponse.getChildNodes();
                    if (children == null || children.getLength() == 0) {
                        System.err.println("No Responses found in \"" + fileXml.getAbsolutePath() + "\"");
                        continue;
                    } else if (children.getLength() > 1) {
                        System.err.println("Multiple Responses found in \"" + fileXml.getAbsolutePath() + "\"");
                    }
                    Node nodeResponse = children.item(0);
                    if (!nodeResponse.getLocalName().equals(XACML3.ELEMENT_RESPONSE)) {
                        System.err.println("\"" + fileXml.getAbsolutePath() + "\" is not a Response");
                        continue;
                    }

                    Response domResponse = DOMResponse.newInstance(nodeResponse);
                    System.out.println(domResponse.toString());
                    System.out.println();
                } catch (Exception ex) {
                    ex.printStackTrace(System.err);
                }
            }
        }
    }

}