org.apache.openaz.xacml.std.json.JSONRequest.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.openaz.xacml.std.json.JSONRequest.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.json;

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.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigInteger;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.security.auth.x500.X500Principal;
import javax.xml.XMLConstants;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.openaz.xacml.api.Attribute;
import org.apache.openaz.xacml.api.AttributeValue;
import org.apache.openaz.xacml.api.DataType;
import org.apache.openaz.xacml.api.DataTypeFactory;
import org.apache.openaz.xacml.api.Identifier;
import org.apache.openaz.xacml.api.Request;
import org.apache.openaz.xacml.api.RequestAttributes;
import org.apache.openaz.xacml.api.RequestAttributesReference;
import org.apache.openaz.xacml.api.RequestReference;
import org.apache.openaz.xacml.api.SemanticString;
import org.apache.openaz.xacml.api.XACML3;
import org.apache.openaz.xacml.std.IdentifierImpl;
import org.apache.openaz.xacml.std.StdAttribute;
import org.apache.openaz.xacml.std.StdAttributeValue;
import org.apache.openaz.xacml.std.StdMutableRequest;
import org.apache.openaz.xacml.std.StdMutableRequestAttributes;
import org.apache.openaz.xacml.std.StdMutableRequestReference;
import org.apache.openaz.xacml.std.StdRequest;
import org.apache.openaz.xacml.std.StdRequestAttributesReference;
import org.apache.openaz.xacml.std.StdRequestDefaults;
import org.apache.openaz.xacml.std.datatypes.DataTypes;
import org.apache.openaz.xacml.std.datatypes.ExtendedNamespaceContext;
import org.apache.openaz.xacml.std.datatypes.StringNamespaceContext;
import org.apache.openaz.xacml.std.datatypes.XPathExpressionWrapper;
import org.apache.openaz.xacml.std.dom.DOMUtil;
import org.apache.openaz.xacml.util.FactoryException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonLocation;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

/**
 * JSONRequest is used to convert JSON into {@link org.apache.openaz.xacml.api.Request} objects. Instances are
 * only generated by loading a file, string, or InputStream representing the Request.
 */
public class JSONRequest {
    private static final Log logger = LogFactory.getLog(JSONRequest.class);

    /*
     * Map of Data Type Identifiers used to map shorthand notation for DataTypes into the full Identifer. This
     * is loaded the first time a Request is processed. Loading is done using Reflection. The map contains
     * keys for both the short form and the long form of each DataType. For example both of the following are
     * in the table: http://www.w3.org/2001/XMLSchema#base64Binary =
     * http://www.w3.org/2001/XMLSchema#base64Binary base64Binary =
     * http://www.w3.org/2001/XMLSchema#base64Binary (Note difference in structure and usage from
     * JSONResponse.)
     */
    private static Map<String, Identifier> shorthandMap = null;

    /*
     * To check the individual data attributes for being the correct type, we need an instance of the
     * DataTypeFactory
     */
    private static DataTypeFactory dataTypeFactory = null;

    /*
     * Prevent creation of instances - this class contains only static methods that return other object types.
     */
    protected JSONRequest() {
    }

    //
    // HELPER METHODS used in Parsing
    //

    /**
     * Allow both JSON boolean and Strings containing JSON booleans. If value is null, assume it is optional
     * and just return null. If Boolean, return same value. If String, note in log that it was string and
     * return converted value. Otherwise throw exception.
     *
     * @param value
     * @param location
     * @return
     * @throws JSONStructureException
     */
    private static Boolean makeBoolean(Object value, String location) throws JSONStructureException {
        if (value == null || value instanceof Boolean) {
            return (Boolean) value;
        }
        try {
            Boolean b = DataTypes.DT_BOOLEAN.convert(value);
            logger.warn(location + " has string containing boolean, should be unquoted boolean value");
            return b;
        } catch (Exception e) {
            throw new JSONStructureException(location + " must be Boolean");
        }
    }

    /**
     * Check the given map for all components having been removed (i.e. everything in the map was known and
     * used). If anything remains, throw an exception based on the component and the keys left in the map
     */
    private static void checkUnknown(String component, Map<?, ?> map) throws JSONStructureException {
        if (map.size() == 0) {
            return;
        }

        String keys = null;
        Iterator<?> it = map.keySet().iterator();
        while (it.hasNext()) {
            if (keys == null) {
                keys = "'" + it.next().toString() + "'";
            } else {
                keys += ", '" + it.next().toString() + "'";
            }
        }

        String message = component + " contains unknown element" + ((map.size() == 1) ? " " : "s ") + keys;
        throw new JSONStructureException(message);
    }

    /**
     * Convert a JSON representation of an XPathExpression into the internal objects. XPathExpression is the
     * only DataType that has a complex multi-part description. It includes - XPathCategory - required - XPath
     * - required - Namespaces - optional; a list of complex structures
     *
     * @param valueMap
     * @return
     * @throws JSONStructureException
     */
    private static AttributeValue<Object> convertXPathExpressionMapToAttributeValue(Map<?, ?> valueMap)
            throws JSONStructureException {
        // get required elements
        Object xpathCategoryObject = valueMap.remove("XPathCategory");
        Object xpathObject = valueMap.remove("XPath");
        if (!(xpathCategoryObject instanceof String) || !(xpathObject instanceof String)) {
            throw new JSONStructureException("XpathCategory and XPath must both be strings");
        }
        String xpathCategoryString = (String) xpathCategoryObject;
        String xpathString = (String) xpathObject;
        if (xpathCategoryString == null || xpathCategoryString.length() == 0 || xpathString == null
                || xpathString.length() == 0) {
            throw new JSONStructureException("XPathCategory or XPath missing or 0-length");
        }

        Identifier xpathCategoryId = new IdentifierImpl(xpathCategoryString);

        // get the Namespaces, if any.
        // Use StringNamespaceContext because we need to use the add functions to incrementally add the
        // namespaces
        StringNamespaceContext namespaceContext = null;

        Object namespacesObject = valueMap.remove("Namespaces");
        if (namespacesObject != null) {
            if (!(namespacesObject instanceof List)) {
                throw new JSONStructureException("Namespaces must be an array");
            }
            List<?> namespacesList = (List<?>) namespacesObject;

            if (namespacesList.size() > 0) {
                // create a NamespaceContext object to hold the namespaces
                namespaceContext = new StringNamespaceContext();

                for (Object n : namespacesList) {
                    if (!(n instanceof Map)) {
                        throw new JSONStructureException("Namespace within Namespaces array must be object");
                    }
                    Map<?, ?> namespaceMap = (Map<?, ?>) n;

                    Object namespaceObject = namespaceMap.remove("Namespace");
                    if (namespaceObject == null || !(namespaceObject instanceof String)) {
                        throw new JSONStructureException(
                                "Namespace object within Namespaces array must contain Namespace string member");
                    }

                    Object prefixObject = namespaceMap.remove("Prefix");
                    if (prefixObject != null && !(prefixObject instanceof String)) {
                        throw new JSONStructureException(
                                "Namespace object within Namespaces array Prefix must be string");
                    }

                    checkUnknown("Namespaces item", namespaceMap);

                    // add this namespace to the NamespaceContext
                    try {
                        namespaceContext.add((String) prefixObject, (String) namespaceObject);
                    } catch (Exception e) {
                        throw new JSONStructureException("Unable to add namespace prefix='" + prefixObject
                                + "' URI='" + namespaceObject + "'");
                    }
                }
            }
        }

        // create the XPathExpressionWrapper to contain this value
        XPathExpressionWrapper xpathExpressionWrapper = new XPathExpressionWrapper(namespaceContext, xpathString);

        AttributeValue<Object> attributeValue = new StdAttributeValue<Object>(DataTypes.DT_XPATHEXPRESSION.getId(),
                xpathExpressionWrapper, xpathCategoryId);

        checkUnknown("XPathExpression", valueMap);

        return attributeValue;
    }

    /**
     * Use reflection to load the map with all the names of all DataTypes, both the long name and the
     * shorthand, and point each name to the appropriate Identifier. The shorthand map is used differently in
     * JSONRequest than in JSONResponse, so there are similarities and differences in the implementation. This
     * is done once the first time a Request is processed.
     */
    private static void initShorthandMap() throws JSONStructureException {
        Field[] declaredFields = XACML3.class.getDeclaredFields();
        shorthandMap = new HashMap<String, Identifier>();
        for (Field field : declaredFields) {
            if (Modifier.isStatic(field.getModifiers()) && field.getName().startsWith("ID_DATATYPE")
                    && Modifier.isPublic(field.getModifiers())) {
                try {
                    Identifier id = (Identifier) field.get(null);
                    String longName = id.stringValue();
                    // most names start with 'http://www.w3.org/2001/XMLSchema#'
                    int sharpIndex = longName.lastIndexOf("#");
                    if (sharpIndex <= 0) {
                        // some names start with 'urn:oasis:names:tc:xacml:1.0:data-type:'
                        // or urn:oasis:names:tc:xacml:2.0:data-type:
                        if (longName.contains(":data-type:")) {
                            sharpIndex = longName.lastIndexOf(":");
                        } else {
                            continue;
                        }
                    }
                    String shortName = longName.substring(sharpIndex + 1);
                    // put both the full name and the short name in the table
                    shorthandMap.put(longName, id);
                    shorthandMap.put(shortName, id);
                } catch (Exception e) {
                    throw new JSONStructureException("Error loading ID Table, e=" + e);
                }
            }
        }
    }

    //
    // MAIN PARSING CODE
    //

    /**
     * Handle a List of Attributes for a Category
     *
     * @param categoryID
     * @param attributes
     * @param stdMutableRequest
     * @throws JSONStructureException
     */
    private static List<Attribute> parseAttribute(Identifier categoryID, ArrayList<?> attributes)
            throws JSONStructureException {
        Iterator<?> iterAttributes = attributes.iterator();

        List<Attribute> collectedAttributes = new ArrayList<Attribute>();

        while (iterAttributes.hasNext()) {
            Map<?, ?> attributeMap = (Map<?, ?>) iterAttributes.next();
            if (!(attributeMap instanceof Map)) {
                throw new JSONStructureException(
                        "Expect Attribute content to be Map got " + attributeMap.getClass());
            }
            Attribute attribute = parseAttribute(categoryID, attributeMap);
            collectedAttributes.add(attribute);
        }

        // return list of all attributes for this Category
        return collectedAttributes;
    }

    /**
     * Given the map of the parsed JSON representation of an Attribute, create the Attribute from the map.
     *
     * @param categoryID
     * @param attributeMap
     * @param stdMutableRequest
     * @return
     * @throws JSONStructureException
     */
    private static Attribute parseAttribute(Identifier categoryID, Map<?, ?> attributeMap)
            throws JSONStructureException {

        // TODO - ASSUME that the spec will remove the requirement that we MUST "handle" JavaScript special
        // values NaN, INF, -INF, none of which make sense on this interface.

        // TODO - ASSUME that the spec will fix inconsistency between AttributeId and Id (both are mentioned),
        // but we have code using both so allow both on input.
        Object idString = attributeMap.remove("AttributeId");
        if (idString == null) {
            //
            // This is an annoying message, and since we have PEP's that already use it
            // make this a debugging message. Otherwise it will clog our log file.
            //
            if (logger.isDebugEnabled()) {
                logger.debug("Attribute missing AttributeId, looking for Id");
            }
            idString = attributeMap.remove("Id");
            if (idString == null) {
                throw new JSONStructureException("Attribute missing AttributeId (and Id)");
            }
        } else {
            // we have the AttributeId - should not also have Id
            if (attributeMap.remove("Id") != null) {
                throw new JSONStructureException(
                        "Found both AttributeId '" + idString + "' and Id field.  Please use only AttributeId.");
            }
        }
        if (!(idString instanceof String)) {
            throw new JSONStructureException("AttributeId must be String, got " + idString.getClass());
        }
        Identifier id = new IdentifierImpl(idString.toString());

        Object Value = attributeMap.remove("Value");
        if (Value == null) {
            throw new JSONStructureException("Attribute missing Value");
        }

        String Issuer = (String) attributeMap.remove("Issuer");

        Object includeInResultObject = attributeMap.remove("IncludeInResult");
        Boolean includeInResult = makeBoolean(includeInResultObject, "IncludeInResult");
        if (includeInResult == null) {
            includeInResult = Boolean.FALSE;
        }

        //
        // Data Type is complicated because:
        // - it may use shorthand (e.g. "integer" instead of full Id)
        // - it may be missing and have to be inferred
        // - inference on Arrays of values is tricky
        // - arrays must all use the same DataType
        // - we are limited in the data types that the Jackson parser is able to infer
        //
        Object DataType = attributeMap.remove("DataType");
        if (DataType != null && !(DataType instanceof String)) {
            throw new JSONStructureException("DataType must be String, got " + DataType.getClass());
        }
        // get Identifier for either long-form or shorthand notation of DataType
        String dataTypeString = null;
        if (DataType != null) {
            dataTypeString = DataType.toString();
        }
        Identifier dataTypeId = shorthandMap.get(dataTypeString);

        // check for unknown DataType
        if (DataType != null && dataTypeId == null) {
            // attribute contained a DataType but it was not known
            throw new JSONStructureException("Unknown DataType '" + dataTypeString + "'");
        }

        // At this point the dataTypeId may be null if no explicit DataType was given.
        // In that case we need to infer the data type from the value object.
        // The best we can do is infer based on the JSON data type, so we recognize boolean, integer, and
        // double, and everything else is handled as a string.
        // Unfortunately the value may be an array of values, so we need to look at all of them before making
        // a decision.
        // The algorithm for arrays is:
        // - take the dataType of the first element in the array as the array's data type
        // - if the array is of type Integer and we see a Double, make the array be Double.
        // Try to determine the over-all type for the list based on the contents.
        // The only mixing that is allowed is Integers and Doubles, in which case the list is Double

        // Values are converted to the current DataType wherever possible.
        // This includes converting doubles, integers and booleans into Strings if the DataType is String.
        // Auto-conversions generate warning messages to the logger.

        // TODO - ASSUME that we need to infer data type for array if not given. Spec is inconsistent on this
        // point, but author seems to want to do it.
        // TODO - Also ASSUME that
        // - everything other than JSON integer, double and boolean is handled as a string, and
        // - the only mixture of data types allowed within the same array is integer and double, yielding the
        // DataType for the array = Double, and
        // - an array of the same JSON data type has the same data type; for strings this means type=string
        // irrespective of what the strings represent (e.g. Date, URI, etc).

        if (Value instanceof List) {
            List<?> valueList = (List<?>) Value;
            // if nothing in the list then we don't care about the type
            if (valueList.size() > 0) {
                Identifier inferredDataTypeId = null;
                if (dataTypeId == null) {
                    // DataType was not given in Attribute - must infer it
                    for (Object item : (List<?>) Value) {
                        // figure out what data type to use for this array
                        if (inferredDataTypeId == null) {
                            // first item, need to set provisional inferred data type
                            if (item instanceof Boolean) {
                                inferredDataTypeId = DataTypes.DT_BOOLEAN.getId();
                            } else if (item instanceof Integer) {
                                inferredDataTypeId = DataTypes.DT_INTEGER.getId();
                            } else if (item instanceof Double) {
                                inferredDataTypeId = DataTypes.DT_DOUBLE.getId();
                            } else {
                                inferredDataTypeId = DataTypes.DT_STRING.getId();
                            }

                        } else if (inferredDataTypeId.equals(DataTypes.DT_INTEGER.getId())
                                && item instanceof Double) {
                            // special case - Double seen in Integer list means whole list is really
                            // Double
                            inferredDataTypeId = DataTypes.DT_DOUBLE.getId();
                        }
                    }
                    // we have inferred a data type for the whole array
                    dataTypeId = inferredDataTypeId;

                }
            }
        } else {
            // single-value attribute
            if (dataTypeId == null) {
                // single value with no DataType defined - Infer the XACML DataType for JSON data types
                if (Value instanceof Integer) {
                    dataTypeId = DataTypes.DT_INTEGER.getId();
                } else if (Value instanceof Double) {
                    dataTypeId = DataTypes.DT_DOUBLE.getId();
                } else if (Value instanceof Boolean) {
                    dataTypeId = DataTypes.DT_BOOLEAN.getId();
                } else {
                    // the Default DataType if none is given is String
                    dataTypeId = DataTypes.DT_STRING.getId();
                }
            }
            // all other data types are not explicitly checked for compatibility
        }

        // we now have the DataType to convert the values into.

        // create a single Attribute to return (it may contain multiple AttributeValues)
        Attribute attribute = null;

        DataType<?> dataType = dataTypeFactory.getDataType(dataTypeId);

        // Variable to use for reporting errors
        Object incomingValue = null;
        try {
            if (Value instanceof List) {
                // this attribute has a list of values
                List<AttributeValue<?>> attributeValueList = new ArrayList<AttributeValue<?>>();
                for (Object o : (List<?>) Value) {
                    // for error reporting we make a copy visible to the Catch clause
                    incomingValue = o;
                    AttributeValue<Object> attributeValue;
                    if (dataType.getId().equals(DataTypes.DT_XPATHEXPRESSION.getId())) {
                        if (!(o instanceof Map)) {
                            throw new JSONStructureException(
                                    "XPathExpression must contain object, not simple value");
                        }
                        attributeValue = convertXPathExpressionMapToAttributeValue((Map<?, ?>) o);
                    } else {
                        Object convertedValue = dataType.convert(o);
                        attributeValue = new StdAttributeValue<Object>(dataTypeId, convertedValue);
                        if ((convertedValue instanceof Integer || convertedValue instanceof Boolean
                                || convertedValue instanceof Double) && o instanceof String
                                || convertedValue instanceof Double && o instanceof Integer
                                || convertedValue instanceof String
                                        && (o instanceof Integer || o instanceof Boolean || o instanceof Double)) {
                            // we converted a String to something else
                            logger.warn("Attribute Id '" + id.stringValue() + "' Value '" + incomingValue
                                    + "' in Array auto-converted from '" + o.getClass().getName() + "' to type '"
                                    + dataType.getId().stringValue());
                        }
                    }
                    attributeValueList.add(attributeValue);
                }
                attribute = new StdAttribute(categoryID, id, attributeValueList, Issuer, includeInResult);
            } else {
                // for error reporting we make a copy visible to the Catch clause
                incomingValue = Value;
                // this attribute has a single value
                AttributeValue<Object> attributeValue;
                if (dataType.getId().equals(DataTypes.DT_XPATHEXPRESSION.getId())) {
                    if (!(Value instanceof Map)) {
                        throw new JSONStructureException("XPathExpression must contain object, not simple value");
                    }
                    attributeValue = convertXPathExpressionMapToAttributeValue((Map<?, ?>) Value);
                } else {
                    Object convertedValue = dataType.convert(Value);
                    attributeValue = new StdAttributeValue<Object>(dataTypeId, convertedValue);
                    // some auto-conversions should be logged because they shouldn't be necessary
                    if ((convertedValue instanceof BigInteger || convertedValue instanceof Boolean
                            || convertedValue instanceof Double) && Value instanceof String
                            || convertedValue instanceof Double && Value instanceof Integer
                            || convertedValue instanceof String && (Value instanceof Integer
                                    || Value instanceof Boolean || Value instanceof Double)) {
                        // we converted a String to something else
                        logger.warn("Attribute Id '" + id.stringValue() + "' Value '" + incomingValue
                                + "' auto-converted from '" + Value.getClass().getName() + "' to type '"
                                + dataType.getId().stringValue());
                    }
                }
                attribute = new StdAttribute(categoryID, id, attributeValue, Issuer, includeInResult);
            }
        } catch (Exception e) {
            throw new JSONStructureException("In Id='" + id.stringValue() + "' Unable to convert Attribute Value '"
                    + incomingValue + "' to type '" + dataTypeId.stringValue() + "'");
        }

        checkUnknown(id.stringValue() + "Attribute '" + idString.toString() + "'", attributeMap);

        return attribute;
    }

    /**
     * Convert the contents of a Content element from XML into XML Node
     *
     * @param xmlContent
     * @return Node
     * @throws Exception
     */
    public static Node parseXML(String xmlContent) throws JSONStructureException {

        if (xmlContent == null || xmlContent.length() == 0) {
            return null;
        }

        //
        // First of all, the String is possible escaped.
        //
        // The meaning of "escaped" is defined in section 4.2.3.1 in the JSON spec
        //
        String unescapedContent = xmlContent.replace("\\\"", "\"");
        unescapedContent = unescapedContent.replace("\\\\", "\\");

        // logger.info("Escaped content: \n" + unescapedContent);

        try (InputStream is = new ByteArrayInputStream(unescapedContent.getBytes("UTF-8"))) {
            Document doc = DOMUtil.loadDocument(is);
            if (doc != null) {
                return doc.getDocumentElement();
            }
            return null;
        } catch (Exception ex) {
            throw new JSONStructureException("Unable to parse Content '" + xmlContent + "'");
        }
    }

    /**
     * Helper to parse all components of one Category or default Category
     *
     * @param categoryMap
     * @param request
     */
    private static void parseCategory(Map<?, ?> categoryMap, String categoryName, Identifier defaultCategoryId,
            StdMutableRequest stdMutableRequest) throws JSONStructureException {

        Identifier categoryId = defaultCategoryId;
        Object categoryIDString = ((Map<?, ?>) categoryMap).remove("CategoryId");
        if (categoryIDString == null && defaultCategoryId == null) {
            throw new JSONStructureException("Category is missing CategoryId");
        }
        if (categoryIDString != null) {
            if (!(categoryIDString instanceof String)) {
                throw new JSONStructureException(
                        "Expect '" + categoryName + "' CategoryId to be String got " + categoryIDString.getClass());
            } else {
                // TODO Spec says CategoryId may be shorthand, but none have been specified
                categoryId = new IdentifierImpl(categoryIDString.toString());
            }
        }
        // if we know the category, make sure user gave correct Id
        if (defaultCategoryId != null && !defaultCategoryId.equals(categoryId)) {
            throw new JSONStructureException(categoryName + " given CategoryId '" + categoryId
                    + "' which does not match default id '" + defaultCategoryId + "'");
        }

        // get the Id, a.k.a xmlId
        String xmlId = (String) ((Map<?, ?>) categoryMap).remove("Id");

        // get the Attributes for this Category, if any
        List<Attribute> attributeList = new ArrayList<Attribute>();
        Object attributesMap = ((Map<?, ?>) categoryMap).remove("Attribute");
        if (attributesMap != null) {
            if (attributesMap instanceof ArrayList) {
                attributeList = parseAttribute(categoryId, (ArrayList<?>) attributesMap);
            } else if (attributesMap instanceof Map) {
                // underlying code expects only collections of Attributes, so create a collection of one to
                // pass this single value
                ArrayList<Map<?, ?>> listForOne = new ArrayList<Map<?, ?>>();
                listForOne.add((Map<?, ?>) attributesMap);
                attributeList = parseAttribute(categoryId, listForOne);
            } else {
                throw new JSONStructureException("Category '" + categoryName + "' saw unexpected Attribute class "
                        + attributesMap.getClass());
            }
        }

        // Get the Content node for this Category, if any
        Node contentRootNode = null;
        Object content = categoryMap.remove("Content");
        if (content != null) {
            if (content instanceof String) {
                //
                // Is it Base64 Encoded?
                //
                if (Base64.isBase64(((String) content).getBytes())) {
                    //
                    // Attempt to decode it
                    //
                    byte[] realContent = Base64.decodeBase64((String) content);
                    //
                    // Now what is it? JSON or XML? Should be XML.
                    //
                    try {
                        contentRootNode = parseXML(new String(realContent, "UTF-8"));
                    } catch (UnsupportedEncodingException e) {
                        throw new JSONStructureException(
                                "Category '" + categoryName + "' Unsupported encoding in Content");
                    }
                } else {
                    //
                    // No, so what is it? Should be XML escaped
                    //
                    contentRootNode = parseXML((String) content);
                }
            } else if (content instanceof byte[]) {
                //
                // Should be Base64
                //
                if (Base64.isBase64(((String) content).getBytes())) {
                    //
                    // Attempt to decode it
                    //
                    byte[] realContent = Base64.decodeBase64((String) content);
                    //
                    // Now what is it? JSON or XML? Should be XML.
                    //
                    try {
                        contentRootNode = parseXML(new String(realContent, "UTF-8"));
                    } catch (UnsupportedEncodingException e) {
                        throw new JSONStructureException(
                                "Category '" + categoryName + "' Unsupported encoding in Content");
                    }
                } else {
                    throw new JSONStructureException(
                            "Category '" + categoryName + "' Content expected Base64 value");
                }
            } else {
                throw new JSONStructureException("Category '" + categoryName
                        + "' Unable to determine what Content is " + content.getClass());
            }
        }

        checkUnknown(categoryName, categoryMap);

        StdMutableRequestAttributes attributeCategory = new StdMutableRequestAttributes(categoryId, attributeList,
                contentRootNode, xmlId);

        stdMutableRequest.add(attributeCategory);

    }

    /**
     * Load the "Default Category" objects, if any. This is used for the special cases of AccessSubject,
     * Action, Resource, and Environment
     *
     * @param jsonRequestMap
     * @param categoryName
     * @param categoryIdString
     * @param stdMutableRequest
     * @throws JSONStructureException
     */
    private static void parseDefaultCategory(Map<?, ?> jsonRequestMap, String categoryName, String categoryIdString,
            StdMutableRequest stdMutableRequest) throws JSONStructureException {
        Object categoryMap = jsonRequestMap.remove(categoryName);
        if (categoryMap != null) {
            Identifier defaultIdentifier = new IdentifierImpl(categoryIdString);
            // The contents may be either a single item (whose attributes are in a Map)
            // or a list of items
            if (categoryMap instanceof Map) {
                // default category contains a single object
                parseCategory((Map<?, ?>) categoryMap, categoryName, defaultIdentifier, stdMutableRequest);
            } else if (categoryMap instanceof List) {
                // Array (for Multiple Decision) of this default category - create separate element for each
                // item in list using same CategoryId for all
                List<?> categoryList = (List<?>) categoryMap;
                for (Object subCategory : categoryList) {
                    if (!(subCategory instanceof Map)) {
                        throw new JSONStructureException(
                                categoryName + " array can only contain objects within curly braces");
                    }
                    parseCategory((Map<?, ?>) subCategory, categoryName, defaultIdentifier, stdMutableRequest);
                }
            } else {
                // do not understand this
                throw new JSONStructureException(categoryName
                        + " must have one object contained within curly braces ({}) or an array of objects ([{}{}])");
            }
        }

    }

    //
    // Primary interface methods
    //

    /**
     * Parse and JSON string into a {@link org.apache.openaz.xacml.api.Request} object.
     *
     * @param jsonString
     * @return
     * @throws JSONStructureException
     */
    public static Request load(String jsonString) throws JSONStructureException {
        Request request = null;
        try (InputStream is = new ByteArrayInputStream(jsonString.getBytes("UTF-8"))) {
            request = JSONRequest.load(is);
        } catch (Exception ex) {
            throw new JSONStructureException("Exception loading String Request: " + ex.getMessage(), ex);
        }
        return request;
    }

    /**
     * Read a file containing the JSON description of a XACML Request and parse it into a
     * {@link org.apache.openaz.xacml.api.Request} Object. This is only used for testing. In normal operation a
     * Request arrives through the RESTful interface and is processed using
     * <code>load(String jsonString)</code>.
     *
     * @param fileRequest
     * @return
     * @throws JSONStructureException
     */
    public static Request load(File fileRequest) throws JSONStructureException {
        Request request = null;
        try (FileInputStream fis = new FileInputStream(fileRequest)) {
            request = JSONRequest.load(fis);
        } catch (Exception ex) {
            throw new JSONStructureException("Exception loading File Request: " + ex.getMessage(), ex);
        }
        return request;
    }

    /**
     * Read characters from the given <code>InputStream</code> and parse them into an XACML
     * {@link org.apache.openaz.xacml.api.Request} object.
     *
     * @param is
     * @return
     * @throws JSONStructureException
     */
    public static Request load(InputStream is) throws JSONStructureException {

        // TODO - ASSUME that order of members within an object does not matter (Different from XML, in JSON
        // everything is handled as Maps so order does not matter)

        // ensure shorthand map is set up
        if (shorthandMap == null) {
            initShorthandMap();
        }

        // ensure that we have an instance of the DataTypeFactory for generating AttributeValues by DataType
        if (dataTypeFactory == null) {
            try {
                dataTypeFactory = DataTypeFactory.newInstance();
                if (dataTypeFactory == null) {
                    throw new NullPointerException("No DataTypeFactory found");
                }
            } catch (FactoryException e) {
                throw new JSONStructureException("Unable to find DataTypeFactory, e=" + e);
            }
        }

        // create a new Request object to be filled in
        StdMutableRequest stdMutableRequest = null;

        String json = null;
        ObjectMapper mapper = null;
        try {

            // read the inputStream into a buffer (trick found online scans entire input looking for
            // end-of-file)
            java.util.Scanner scanner = new java.util.Scanner(is);
            scanner.useDelimiter("\\A");
            json = scanner.hasNext() ? scanner.next() : "";
            scanner.close();

            mapper = new ObjectMapper().setVisibility(PropertyAccessor.FIELD, Visibility.ANY);

            // TODO - ASSUME that any duplicated component is a bad thing (probably indicating an error in the
            // incoming JSON)
            mapper.configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, true);

            Map<?, ?> root = mapper.readValue(json, Map.class);

            //
            // Does the request exist?
            //
            Map<?, ?> jsonRequestMap = (Map<?, ?>) root.remove("Request");
            if (jsonRequestMap == null) {
                throw new JSONStructureException("No \"Request\" property found.");
            }

            checkUnknown("Top-level message", root);

            stdMutableRequest = new StdMutableRequest();

            //
            // Is there a Category?
            //
            Object categoryList = jsonRequestMap.remove("Category");
            if (categoryList != null && !(categoryList instanceof List)) {
                throw new JSONStructureException(
                        "Category must contain list of objects, not '" + categoryList.getClass() + "'");
            }
            if (categoryList != null) {
                //
                // Iterate each Category
                //
                Iterator<?> iter = ((List<?>) categoryList).iterator();
                while (iter.hasNext()) {
                    Object category = iter.next();
                    if (!(category instanceof Map)) {
                        throw new JSONStructureException(
                                "Category list must contain objects contained within curly braces ({})");
                    }

                    parseCategory((Map<?, ?>) category, "Category", null, stdMutableRequest);

                }
            }

            // The following may be either a single instance or an array. This allows multiple decisions to
            // work with the Default Category objects.
            // Example:
            // "AccessSubject" : [ {attributes group one},
            // {attributes group two}
            // ]

            //
            // Look for default Shorthand AccessSubject
            //
            parseDefaultCategory(jsonRequestMap, "AccessSubject",
                    "urn:oasis:names:tc:xacml:1.0:subject-category:access-subject", stdMutableRequest);
            //
            // Provide backward compatibility for our PEP's
            //
            parseDefaultCategory(jsonRequestMap, "Subject",
                    "urn:oasis:names:tc:xacml:1.0:subject-category:access-subject", stdMutableRequest);

            //
            // Look for default Shorthand Action
            //
            parseDefaultCategory(jsonRequestMap, "Action", "urn:oasis:names:tc:xacml:3.0:attribute-category:action",
                    stdMutableRequest);

            //
            // Look for default Shorthand Resource
            //
            parseDefaultCategory(jsonRequestMap, "Resource",
                    "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", stdMutableRequest);

            //
            // Look for default Shorthand Environment
            //
            parseDefaultCategory(jsonRequestMap, "Environment",
                    "urn:oasis:names:tc:xacml:3.0:attribute-category:environment", stdMutableRequest);

            //
            // Look for default Shorthand RecipientSubject
            //
            parseDefaultCategory(jsonRequestMap, "RecipientSubject",
                    "urn:oasis:names:tc:xacml:1.0:subject-category:recipient-subject", stdMutableRequest);

            //
            // Look for default Shorthand IntermediarySubject
            //
            parseDefaultCategory(jsonRequestMap, "IntermediarySubject",
                    "urn:oasis:names:tc:xacml:1.0:subject-category:intermediary-subject", stdMutableRequest);

            //
            // Look for default Shorthand Codebase
            //
            parseDefaultCategory(jsonRequestMap, "Codebase",
                    "urn:oasis:names:tc:xacml:1.0:subject-category:codebase", stdMutableRequest);

            //
            // Look for default Shorthand RequestingMachine
            //
            parseDefaultCategory(jsonRequestMap, "RequestingMachine",
                    "urn:oasis:names:tc:xacml:1.0:subject-category:requesting-machine", stdMutableRequest);

            //
            // MultiRequest
            //
            Map<?, ?> multiRequests = (Map<?, ?>) jsonRequestMap.remove("MultiRequests");
            if (multiRequests != null) {
                if (!(multiRequests instanceof Map)) {
                    throw new JSONStructureException("MultiRequests must be object structure, not single value");
                }

                List<?> requestReferenceList = (List<?>) multiRequests.remove("RequestReference");
                if (requestReferenceList == null) {
                    throw new JSONStructureException("MultiRequest must contain a RequestReference element");
                }
                if (requestReferenceList.size() < 1) {
                    throw new JSONStructureException(
                            "MultiRequest must contain at least one element in the RequestReference list");
                }

                checkUnknown("MultiRequest", multiRequests);

                for (Object requestReferenceMapObject : requestReferenceList) {
                    if (!(requestReferenceMapObject instanceof Map)) {
                        throw new JSONStructureException("MultiRequest RequestReference must be object");
                    }
                    Map<?, ?> requestReferenceMap = (Map<?, ?>) requestReferenceMapObject;

                    // each object within the list must contain a ReferenceId and only a ReferenceId
                    Object referenceIdListObject = requestReferenceMap.remove("ReferenceId");
                    if (referenceIdListObject == null) {
                        throw new JSONStructureException(
                                "MultiRequest RequestReference list element must contain ReferenceId");
                    }
                    List<?> referenceIdList = (List<?>) referenceIdListObject;
                    if (referenceIdList.size() == 0) {
                        // the spec does not disallow empty list RequestReference objects
                        continue;
                    }

                    checkUnknown("RequestReference", requestReferenceMap);

                    // create reference corresponding to RequestReference list element
                    StdMutableRequestReference requestReference = new StdMutableRequestReference();

                    for (Object referenceId : referenceIdList) {
                        // add attributes to the reference
                        // Since the order of the JSON is not constrained, we could process this section
                        // before the section containing attribute being referenced,
                        // so we cannot do a cross-check here to verify that the attribute reference exists.
                        // That will happen later when the PDP attempts to find the attribute.
                        StdRequestAttributesReference requestAttributesReference = new StdRequestAttributesReference(
                                (String) referenceId);
                        requestReference.add(requestAttributesReference);
                    }
                    stdMutableRequest.add(requestReference);
                }
            }

            //
            // ReturnPolicyIdList
            //
            // If omitted this is set to a default of false by the StdMutableRequest constructor.
            //
            Object returnPolicyIdList = jsonRequestMap.remove("ReturnPolicyIdList");
            Boolean returnPolicyIdListBoolean = makeBoolean(returnPolicyIdList, "ReturnPolicyIdList");
            if (returnPolicyIdList != null) {
                stdMutableRequest.setReturnPolicyIdList(returnPolicyIdListBoolean);
            }

            //
            // CombinedDecision
            //
            // If omitted this is set to a default of false by the StdMutableRequest constructor.
            //
            Object combinedDecision = jsonRequestMap.remove("CombinedDecision");
            Boolean combinedDecisionBoolean = makeBoolean(combinedDecision, "CombinedDecision");
            if (combinedDecision != null) {
                stdMutableRequest.setCombinedDecision(combinedDecisionBoolean);
            }

            //
            // XPath
            //

            // The JSON spec says that this has a default value, implying that if it is missing in the Request
            // we should fill it in.
            // However the XML (DOM) version does not do that. If the value is missing it leaves the
            // requestDefaults object blank.
            // We are following the XML approach and ignoring the Default value for this field in the spec.

            // TODO - Assume that no value for XPathVersion means "leave as null", not "fill in the default
            // value from spec. This violates the JSON spec
            Object xPath = jsonRequestMap.remove("XPathVersion");
            if (xPath != null) {
                // XPath is given in the JSON input
                if (!(xPath instanceof String)) {
                    throw new JSONStructureException("XPathVersion not a URI passed as a String");
                }
                URI xPathUri = null;
                try {
                    xPathUri = new URI(xPath.toString());
                } catch (Exception e) {
                    throw new JSONStructureException("XPathVersion not a valid URI: '" + xPath + "'", e);
                }

                StdRequestDefaults requestDefaults = new StdRequestDefaults(xPathUri);
                stdMutableRequest.setRequestDefaults(requestDefaults);
            }

            checkUnknown("Request", jsonRequestMap);

        } catch (JsonParseException e) {
            // try to point to problem area in JSON input, if possible
            JsonLocation location = e.getLocation();
            String locationOfError = "(unavailable)";
            if (location != null && location != JsonLocation.NA) {
                String jsonText = json;
                if (location.getLineNr() > 1) {
                    String[] jsonArray = jsonText.split("\\r?\\n|\\r");
                    jsonText = jsonArray[location.getLineNr()];
                }
                if (location.getCharOffset() < jsonText.length()) {
                    if (location.getCharOffset() > 0) {
                        locationOfError = jsonText.substring((int) location.getCharOffset() - 1);
                    }
                    if (locationOfError.length() > 30) {
                        locationOfError = locationOfError.substring(0, 30);
                    }
                }
            }
            throw new JSONStructureException("Unable to parse JSON starting at text'" + locationOfError
                    + "', input was '" + json + "', exception: " + e, e);
        } catch (JsonMappingException e) {
            throw new JSONStructureException("Unable to map JSON '" + json + "', exception: " + e, e);
        } catch (IOException e) {
            throw new JSONStructureException("Unable to read JSON input, exception: " + e, e);
        }

        // all done
        return new StdRequest(stdMutableRequest);
    }

    //
    // Generate JSON string from a Request object created by another means (e.g. XML).
    //

    /**
     * Convert the {@link org.apache.openaz.xacml.api.Request} into an JSON string with pretty-printing.
     *
     * @param request
     * @return
     * @throws Exception
     */
    public static String toString(Request request) throws Exception {
        return toString(request, true);
    }

    /**
     * Convert the {@link org.apache.openaz.xacml.api.Response} into an JSON string, pretty-printing is
     * optional. 
     *
     * @param response
     * @param prettyPrint
     * @return
     * @throws Exception
     */
    public static String toString(Request request, boolean prettyPrint) throws Exception {
        String outputString = null;
        try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
            convert(request, os, prettyPrint);
            outputString = new String(os.toByteArray(), "UTF-8");
        } catch (Exception ex) {
            throw ex;
        }
        return outputString;
    }

    /**
     * Convert the {@link org.apache.openaz.xacml.api.Request} object into a string suitable for output.
     * IMPORTANT: This method does NOT close the outputStream. It is the responsibility of the caller to (who
     * opened the stream) to close it.
     *
     * @param request
     * @param outputStream
     * @throws java.io.IOException
     * @throws JSONStructureException
     */
    public static void convert(Request request, OutputStream outputStream)
            throws IOException, JSONStructureException {
        convert(request, outputStream, false);
    }

    /**
     * Do the work of converting the {@link org.apache.openaz.xacml.api.Request} object to a string, allowing
     * for pretty-printing if desired. IMPORTANT: This method does NOT close the outputStream. It is the
     * responsibility of the caller to (who opened the stream) to close it.
     *
     * @param request
     * @param outputStream
     * @param prettyPrint
     * @throws java.io.IOException #throws JSONStructureException
     */
    public static void convert(Request request, OutputStream outputStream, boolean prettyPrint)
            throws IOException, JSONStructureException {

        if (request == null) {
            throw new NullPointerException("No Request in convert");
        }

        Map<String, Object> requestMap = new HashMap<String, Object>();

        // ReturnPolicyIdList
        requestMap.put("ReturnPolicyIdList", request.getReturnPolicyIdList());
        // Combined
        requestMap.put("CombinedDecision", request.getCombinedDecision());
        // XPath
        if (request.getRequestDefaults() != null) {
            requestMap.put("XPathVersion", request.getRequestDefaults().getXPathVersion());
        }

        // Categories
        Iterator<RequestAttributes> rait = request.getRequestAttributes().iterator();
        List<Map<String, Object>> generalCategoriesList = new ArrayList<Map<String, Object>>();
        while (rait.hasNext()) {
            RequestAttributes ra = rait.next();

            // create a new map for the category
            Map<String, Object> categoryMap = new HashMap<String, Object>();

            // fill in the category
            if (ra.getXmlId() != null) {
                categoryMap.put("Id", ra.getXmlId());
            }
            if (ra.getContentRoot() != null) {
                StringWriter writer = new StringWriter();
                Transformer transformer = null;
                try {
                    transformer = TransformerFactory.newInstance().newTransformer();
                    transformer.transform(new DOMSource(ra.getContentRoot()), new StreamResult(writer));
                } catch (Exception e) {
                    throw new JSONStructureException("Unable to Content node to string; e=" + e);
                }

                String xml = writer.toString();

                categoryMap.put("Content", xml);
            }

            Iterator<Attribute> attrIt = ra.getAttributes().iterator();
            List<Map<String, Object>> attributesList = new ArrayList<Map<String, Object>>();
            while (attrIt.hasNext()) {
                Attribute attr = attrIt.next();
                Map<String, Object> attrMap = new HashMap<String, Object>();
                attrMap.put("AttributeId", attr.getAttributeId().stringValue());
                if (attr.getIssuer() != null) {
                    attrMap.put("Issuer", attr.getIssuer());
                }
                attrMap.put("IncludeInResult", attr.getIncludeInResults());
                Collection<AttributeValue<?>> valuesCollection = attr.getValues();
                Iterator<AttributeValue<?>> valuesIt = valuesCollection.iterator();

                if (valuesCollection.size() == 1) {
                    // single-value
                    AttributeValue<?> attrValue = valuesIt.next();
                    attrMap.put("DataType", attrValue.getDataTypeId().stringValue());

                    attrMap.put("Value", jsonOutputObject(attrValue.getValue(), attrValue));

                } else if (valuesCollection.size() > 1) {
                    // multiple values
                    List<Object> attrValueList = new ArrayList<Object>();
                    while (valuesIt.hasNext()) {
                        AttributeValue<?> attrValue = valuesIt.next();
                        // assume all have the same type, so last one in list is fine
                        attrMap.put("DataType", attrValue.getDataTypeId().stringValue());

                        attrValueList.add(jsonOutputObject(attrValue.getValue(), attrValue));

                    }
                    attrMap.put("Value", attrValueList);

                }

                attributesList.add(attrMap);
            }
            if (attributesList.size() > 0) {
                categoryMap.put("Attribute", attributesList);
            }

            // We do not use the "Default" category objects because the XML may have multiples of the same
            // Category.
            // This is fine when the categories are contained in the array of Category objects,
            // but if we use the Default category objects we might end up with multiples of the same Category
            // name,
            // and the Jackson parser does not handle that well.
            // Example: This is ok because the AccessSubjects are independent items within the list:
            // { "Request" : {
            // "Category" : [
            // { "CategoryId" : ""subject", " },
            // { "CategoryId" : ""subject", " }
            // ]
            // }}
            //
            // This is NOT ok because the Subjects are seen as duplicate elements:
            // { "Request" : {
            // "AccessSubject" : {"},
            // "AccessSubject" : {"},
            // }}

            categoryMap.put("CategoryId", ra.getCategory().stringValue());
            generalCategoriesList.add(categoryMap);

        }

        if (generalCategoriesList.size() > 0) {
            requestMap.put("Category", generalCategoriesList);
        }

        // MultiRequests
        if (request.getMultiRequests() != null) {
            Collection<RequestReference> referenceCollection = request.getMultiRequests();

            Map<String, Object> multiRequestMap = new HashMap<String, Object>();
            List<Map<String, Object>> requestReferenceList = new ArrayList<Map<String, Object>>();

            Iterator<RequestReference> rrIt = referenceCollection.iterator();
            while (rrIt.hasNext()) {
                RequestReference rr = rrIt.next();
                Map<String, Object> requestReferenceMap = new HashMap<String, Object>();

                Collection<RequestAttributesReference> rarCollection = rr.getAttributesReferences();
                List<Object> ridList = new ArrayList<Object>();
                Iterator<RequestAttributesReference> rarIt = rarCollection.iterator();
                while (rarIt.hasNext()) {
                    RequestAttributesReference rar = rarIt.next();
                    ridList.add(rar.getReferenceId());
                }

                if (ridList.size() > 0) {
                    requestReferenceMap.put("ReferenceId", ridList);
                }

                if (requestReferenceMap.size() > 0) {
                    requestReferenceList.add(requestReferenceMap);
                }

                if (requestReferenceList.size() > 0) {
                    multiRequestMap.put("RequestReference", requestReferenceList);
                }
            }

            if (multiRequestMap.size() > 0) {
                requestMap.put("MultiRequests", multiRequestMap);
            }
        }

        //
        // Create the overall Request map
        //
        Map<String, Object> theWholeRequest = new HashMap<String, Object>();
        theWholeRequest.put("Request", requestMap);
        //
        // Create a string buffer
        //
        ObjectMapper mapper = new ObjectMapper().setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
        mapper.configure(SerializationFeature.INDENT_OUTPUT, prettyPrint);
        try (OutputStreamWriter osw = new OutputStreamWriter(outputStream)) {

            // convert the request to json string
            String json = mapper.writeValueAsString(theWholeRequest);

            // write it
            osw.write(json);

            // force output
            osw.flush();
        } catch (Exception e) {
            logger.error("Failed to write to json string: " + e.getLocalizedMessage(), e);
        }

    }

    /**
     * Create the appropriate object for JSON output. This needs to be a Boolean, Integer or Double for those
     * data types so that the ObjectMapper knows how to format the JSON text. For objects implementing
     * stringValue we use that string. for XPathExpressions use the Path. Otherwise default to using toString.
     *
     * @param obj
     * @return
     */
    private static Object jsonOutputObject(Object obj, AttributeValue<?> attrValue) throws JSONStructureException {
        if (obj instanceof String || obj instanceof Boolean || obj instanceof BigInteger) {
            return obj;
        } 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;
        } 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) {
            // create a map containing the complex value for the XPathExpression
            Map<String, Object> xpathExpressionMap = new HashMap<String, Object>();
            Identifier xpathCategoryId = attrValue.getXPathCategory();
            if (xpathCategoryId == null) {
                throw new JSONStructureException("XPathExpression is missing XPathCategory");
            }
            xpathExpressionMap.put("XPathCategory", attrValue.getXPathCategory().stringValue());

            XPathExpressionWrapper xw = (XPathExpressionWrapper) obj;
            xpathExpressionMap.put("XPath", xw.getPath());

            ExtendedNamespaceContext namespaceContext = xw.getNamespaceContext();
            if (namespaceContext != null) {
                List<Object> namespaceList = new ArrayList<Object>();

                // get the list of all namespace prefixes
                Iterator<String> prefixIt = namespaceContext.getAllPrefixes();
                while (prefixIt.hasNext()) {
                    String prefix = prefixIt.next();
                    String namespaceURI = namespaceContext.getNamespaceURI(prefix);
                    Map<String, Object> namespaceMap = new HashMap<String, Object>();
                    if (prefix != null && !prefix.equals(XMLConstants.DEFAULT_NS_PREFIX)) {
                        namespaceMap.put("Prefix", prefix);
                    }
                    namespaceMap.put("Namespace", namespaceURI);
                    namespaceList.add(namespaceMap);
                }

                xpathExpressionMap.put("Namespaces", namespaceList);
            }
            return xpathExpressionMap;

        } else {
            throw new JSONStructureException("Unhandled data type='" + obj.getClass().getName() + "'");
        }
    }

}

/*
 * Place to put very long output strings for editing during debugging
 */