| /* |
| * 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. |
| * |
| */ |
| |
| /* |
| * AT&T - PROPRIETARY |
| * THIS FILE CONTAINS PROPRIETARY INFORMATION OF |
| * AT&T AND IS NOT TO BE DISCLOSED OR USED EXCEPT IN |
| * ACCORDANCE WITH APPLICABLE AGREEMENTS. |
| * |
| * Copyright (c) 2013 AT&T Knowledge Ventures |
| * Unpublished and Not for Publication |
| * All Rights Reserved |
| */ |
| 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). |
| // |
| // Used only in Testing |
| // |
| |
| /** |
| * Convert the {@link org.apache.openaz.xacml.api.Request} into an JSON string with pretty-printing. This |
| * is used only for debugging. |
| * |
| * @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. This is used only for debugging. |
| * |
| * @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 |
| */ |