]> git.aero2k.de Git - tmp/jakarta-migration.git/commitdiff
DRYD-23: Implement JSON to XML conversion.
authorRay Lee <rhlee@berkeley.edu>
Tue, 2 Aug 2016 07:51:30 +0000 (00:51 -0700)
committerRay Lee <rhlee@berkeley.edu>
Wed, 3 Aug 2016 18:09:45 +0000 (11:09 -0700)
26 files changed:
services/JaxRsServiceProvider/src/main/webapp/WEB-INF/web.xml
services/common/pom.xml
services/common/src/main/java/org/collectionspace/services/common/xmljson/ConversionUtils.java [new file with mode: 0644]
services/common/src/main/java/org/collectionspace/services/common/xmljson/JsonField.java [new file with mode: 0644]
services/common/src/main/java/org/collectionspace/services/common/xmljson/JsonToXmlFilter.java [new file with mode: 0644]
services/common/src/main/java/org/collectionspace/services/common/xmljson/JsonToXmlStreamConverter.java [new file with mode: 0644]
services/common/src/main/java/org/collectionspace/services/common/xmljson/RequestUtils.java
services/common/src/main/java/org/collectionspace/services/common/xmljson/XmlNode.java [new file with mode: 0644]
services/common/src/main/java/org/collectionspace/services/common/xmljson/XmlToJsonStreamConverter.java
services/common/src/main/java/org/collectionspace/services/common/xmljson/parsetree/Node.java [deleted file]
services/common/src/test/java/org/collectionspace/services/common/xmljson/test/ConversionUtilsTest.java [new file with mode: 0644]
services/common/src/test/java/org/collectionspace/services/common/xmljson/test/JsonFieldTest.java [new file with mode: 0644]
services/common/src/test/java/org/collectionspace/services/common/xmljson/test/JsonToXmlStreamConverterTest.java [new file with mode: 0644]
services/common/src/test/java/org/collectionspace/services/common/xmljson/test/RequestUtilsTest.java
services/common/src/test/java/org/collectionspace/services/common/xmljson/test/XmlNodeTest.java [new file with mode: 0644]
services/common/src/test/java/org/collectionspace/services/common/xmljson/test/XmlToJsonStreamConverterTest.java
services/common/src/test/resources/test-data/xmljson/boolean-json.json [new file with mode: 0644]
services/common/src/test/resources/test-data/xmljson/boolean-json.xml [new file with mode: 0644]
services/common/src/test/resources/test-data/xmljson/collectionobject.json
services/common/src/test/resources/test-data/xmljson/empty-json.json [new file with mode: 0644]
services/common/src/test/resources/test-data/xmljson/empty-json.xml [new file with mode: 0644]
services/common/src/test/resources/test-data/xmljson/numeric-json.json [new file with mode: 0644]
services/common/src/test/resources/test-data/xmljson/numeric-json.xml [new file with mode: 0644]
services/common/src/test/resources/test-data/xmljson/record.json
services/common/src/test/resources/test-data/xmljson/record.xml
services/report/pom.xml

index c375f1830926e5c731cdb1d5a6aeeb00b39d8c17..5e785a495fce24cdb63cd43d658344902a744482 100644 (file)
                <filter-name>CSpaceFilter</filter-name>
                <url-pattern>/*</url-pattern>
        </filter-mapping>
+
+    <!--
+        A filter that converts JSON requests to XML.
+    -->
+    <filter>
+        <filter-name>JsonToXmlFilter</filter-name>
+        <filter-class>org.collectionspace.services.common.xmljson.JsonToXmlFilter</filter-class>
+    </filter>
+    
+    <filter-mapping>
+        <filter-name>JsonToXmlFilter</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-mapping>
     
     <!--
         A filter that converts XML responses to JSON if needed.
index ef9c31cac2ed5fc76cd0d5fe89d61597e37ef21a..48645b4aca8cabb232c0211e74c8fa1d8da97bc4 100644 (file)
                        <version>3.4</version>
                        <scope>test</scope>
                </dependency>
+               <dependency>
+                       <groupId>org.xmlunit</groupId>
+                       <artifactId>xmlunit-core</artifactId>
+                       <version>2.2.1</version>
+                       <scope>test</scope>
+               </dependency>
        </dependencies>
 
        <build>
diff --git a/services/common/src/main/java/org/collectionspace/services/common/xmljson/ConversionUtils.java b/services/common/src/main/java/org/collectionspace/services/common/xmljson/ConversionUtils.java
new file mode 100644 (file)
index 0000000..e3e17ec
--- /dev/null
@@ -0,0 +1,107 @@
+package org.collectionspace.services.common.xmljson;
+
+import javax.xml.namespace.QName;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Utility methods for doing XML/JSON conversion.
+ */
+public class ConversionUtils {
+    /**
+     * Prefix to prepend to XML attribute names when converting to JSON
+     * field names.
+     */
+    public static final String XML_ATTRIBUTE_PREFIX = "@";
+    
+    /**
+     * Prefix to prepend to XML namespace prefixes when converting to
+     * JSON field names.
+     */
+    public static final String XML_NAMESPACE_PREFIX = XML_ATTRIBUTE_PREFIX + "xmlns:";
+    
+    /**
+     * Converts an XML attribute name to a JSON field name.
+     * 
+     * @param xmlAttributeName the XML attribute name
+     * @return the JSON field name
+     */
+    public static String xmlAttributeNameToJsonFieldName(String xmlAttributeName) {
+        return XML_ATTRIBUTE_PREFIX + xmlAttributeName;
+    }
+    
+    /**
+     * Converts a JSON field name to an XML attribute name.
+     * The field name must be in the expected format, as determined by
+     * isXmlAttribute().
+     * 
+     * @param jsonFieldName the JSON field name
+     * @return the XML attribute name
+     */
+    public static String jsonFieldNameToXmlAttributeName(String jsonFieldName) {
+        return jsonFieldName.substring(XML_ATTRIBUTE_PREFIX.length());
+    }
+
+    /**
+     * Converts an XML namespace prefix to a JSON field name.
+     * 
+     * @param xmlNamespacePrefix the XML namespace prefix
+     * @return the JSON field name
+     */
+    public static String xmlNamespacePrefixToJsonFieldName(String xmlNamespacePrefix) {
+        return XML_NAMESPACE_PREFIX + xmlNamespacePrefix;
+    }
+    
+    /**
+     * Converts a JSON field name to an XML namespace prefix.
+     * The field name must be in the expected format, as determined by
+     * isXmlNamespace().
+     * 
+     * @param jsonFieldName the JSON field name
+     * @return the XML namespace prefix
+     */
+    public static String jsonFieldNameToXmlNamespacePrefix(String jsonFieldName) {
+        return jsonFieldName.substring(XML_NAMESPACE_PREFIX.length());
+    }
+
+    /**
+     * Determines if a JSON field name represents an XML
+     * attribute.
+     *  
+     * @param jsonFieldName the field name to test
+     * @return true if the field name represents an XML attribute,
+     *         false otherwise
+     */
+    public static boolean isXmlAttribute(String jsonFieldName) {
+        return jsonFieldName.startsWith(XML_ATTRIBUTE_PREFIX);
+    }
+    
+    /**
+     * Determines if a JSON field name represents an XML
+     * namespace prefix.
+     * 
+     * @param jsonFieldName the field name to test
+     * @return true if the field name represents an XML namespace prefix,
+     *         false otherwise
+     */
+    public static boolean isXmlNamespace(String jsonFieldName) {
+        return jsonFieldName.startsWith(XML_NAMESPACE_PREFIX);
+    }
+    
+    /**
+     * Converts an XML element QName to a JSON field name.
+     * 
+     * @param name the XML element QName
+     * @return the JSON field name
+     */
+    public static String jsonFieldNameFromXMLQName(QName name) {
+        String prefix = name.getPrefix();
+        String localPart = name.getLocalPart();
+        
+        if (StringUtils.isNotEmpty(prefix)) {
+            return prefix + ":" + localPart;
+        }
+        
+        return localPart;
+    }
+}
diff --git a/services/common/src/main/java/org/collectionspace/services/common/xmljson/JsonField.java b/services/common/src/main/java/org/collectionspace/services/common/xmljson/JsonField.java
new file mode 100644 (file)
index 0000000..5f3ebf4
--- /dev/null
@@ -0,0 +1,94 @@
+package org.collectionspace.services.common.xmljson;
+
+/**
+ * A lightweight representation of a JSON field. Instances are created
+ * by JsonToXmlStreamConverter in the course of parsing JSON, in order
+ * to track the current state.
+ * 
+ * Each JSON field has a name and a type, which is either scalar, array,
+ * or object.
+ */
+public class JsonField {
+    private String name;
+    private Type type;
+    
+    /**
+     * Creates an unnamed JsonField.
+     */
+    public JsonField() {
+        
+    }
+    
+    /**
+     * Creates a JsonField with a given name, and scalar type.
+     * 
+     * @param name the name
+     */
+    public JsonField(String name) {
+        this.setName(name);
+        this.setType(Type.SCALAR);
+    }
+
+    /**
+     * Returns the name of the field.
+     * 
+     * @return the name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Sets the name of the field.
+     * 
+     * @param name the name
+     */
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    /**
+     * Returns the type of the field.
+     * 
+     * @return the type
+     */
+    public Type getType() {
+        return type;
+    }
+
+    /**
+     * Sets the type of the field.
+     * 
+     * @param type the type
+     */
+    public void setType(Type type) {
+        this.type = type;
+    }
+    
+    /**
+     * Determines if this is a scalar field.
+     * 
+     * @return true if this is a scalar field, false otherwise
+     */
+    public boolean isScalar() {
+        return (getType() == Type.SCALAR);
+    }
+    
+    /**
+     * Determines if this is an array field.
+     * 
+     * @return true if this is an array field, false otherwise
+     */
+    public boolean isArray() {
+        return (getType() == Type.ARRAY);
+    }
+    
+    /**
+     * The possible field types.
+     */
+    public enum Type {
+        SCALAR,
+        ARRAY,
+        OBJECT
+    }
+}
\ No newline at end of file
diff --git a/services/common/src/main/java/org/collectionspace/services/common/xmljson/JsonToXmlFilter.java b/services/common/src/main/java/org/collectionspace/services/common/xmljson/JsonToXmlFilter.java
new file mode 100644 (file)
index 0000000..e4c8efc
--- /dev/null
@@ -0,0 +1,87 @@
+package org.collectionspace.services.common.xmljson;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.ws.rs.core.MediaType;
+import javax.xml.stream.XMLStreamException;
+
+import org.apache.commons.io.output.ByteArrayOutputStream;
+
+/**
+ * A filter that translates JSON to XML.
+ */
+public class JsonToXmlFilter implements Filter {
+
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException {
+
+    }
+    
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        
+        if (RequestUtils.isJsonContent(httpRequest)) {
+            // The request contains a JSON payload. Wrap the request.
+            
+            RequestWrapper requestWrapper = new RequestWrapper(httpRequest);
+
+            chain.doFilter(requestWrapper, response);
+        }
+        else {
+            // The request doesn't contain a JSON payload. Just pass it along.
+            
+            chain.doFilter(request, response);
+        }
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+    
+    /**
+     * A request wrapper that .
+     */
+    public class RequestWrapper extends HttpServletRequestWrapper {
+        public RequestWrapper(HttpServletRequest request) {
+            super(request);
+        }
+
+        @Override
+        public String getContentType() {
+            return MediaType.APPLICATION_XML;
+        }
+
+        @Override
+        public ServletInputStream getInputStream() throws IOException {
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            
+            try {
+                JsonToXmlStreamConverter converter = new JsonToXmlStreamConverter(super.getInputStream(), out);
+                converter.convert();
+            } catch (XMLStreamException e) {
+                throw new IOException("error converting JSON stream to XML", e);
+            }
+            
+            final InputStream xmlInput = out.toInputStream();
+
+            return new ServletInputStream() {
+                @Override
+                public int read() throws IOException {
+                    return xmlInput.read();
+                }
+            };
+        }
+    }
+}
diff --git a/services/common/src/main/java/org/collectionspace/services/common/xmljson/JsonToXmlStreamConverter.java b/services/common/src/main/java/org/collectionspace/services/common/xmljson/JsonToXmlStreamConverter.java
new file mode 100644 (file)
index 0000000..bda3a6f
--- /dev/null
@@ -0,0 +1,366 @@
+package org.collectionspace.services.common.xmljson;
+
+import static org.collectionspace.services.common.xmljson.ConversionUtils.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Stack;
+
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+
+/**
+ * Converts a CSpace JSON payload to an XML payload.
+ *
+ * This class is not intended to serve as a general purpose JSON to XML
+ * translator. It is instead a lightweight processor tuned for the kinds
+ * of JSON generated by CSpace, and the particular transformations needed
+ * to generate XML for CSpace.
+ * 
+ * The conversion is performed as follows:
+ * <ul>
+ * <li>JSON fields starting with "@xmlns:" are converted XML namespace declarations.</li>
+ * <li>JSON fields starting with "@" are converted to XML attributes.</li>
+ * <li>Other JSON fields are converted to identically-named XML elements.</li>
+ * <li>The contents of JSON objects are converted to XML child elements.</li>
+ * <li>The contents of JSON arrays are expanded into multiple XML elements, each
+ *     named with the field name of the JSON array.</li>
+ * </ul>
+ *
+ * This implementation is schema-unaware. It operates by examining only the input
+ * document, without utilizing any XML schema information.
+ * 
+ * Example:
+ *
+ * JSON
+ * <pre>
+ * {
+ *   "document": {
+ *     "@name": "collectionobjects",
+ *     "ns2:collectionobjects_common": {
+ *       "@xmlns:ns2": "http://collectionspace.org/services/collectionobject",
+ *       "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
+ *       "objectNumber": "2016.1.1",
+ *       "objectNameList": {
+ *         "objectNameGroup": [
+ *           {
+ *             "objectNameCurrency": null,
+ *             "objectNameLanguage": null,
+ *             "objectName": "Object name",
+ *             "objectNameSystem": null,
+ *             "objectNameType": null,
+ *             "objectNameNote": null,
+ *             "objectNameLevel": null
+ *           },
+ *           {
+ *             "objectNameCurrency": null,
+ *             "objectNameLanguage": null,
+ *             "objectName": "Another name",
+ *             "objectNameSystem": null,
+ *             "objectNameType": null,
+ *             "objectNameNote": null,
+ *             "objectNameLevel": null
+ *           }
+ *         ]
+ *       },
+ *       "comments": {
+ *         "comment": [
+ *           "Some comment text",
+ *           "Another comment"
+ *         ]
+ *       }
+ *     }
+ *   }
+ * }
+ * </pre>
+ *
+ * XML
+ * <pre>
+ * <document name="collectionobjects">
+ *   <ns2:collectionobjects_common xmlns:ns2="http://collectionspace.org/services/collectionobject" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ *     <objectNumber>2016.1.1</objectNumber>
+ *     <objectNameList>
+ *       <objectNameGroup>
+ *         <objectNameCurrency/>
+ *         <objectNameLanguage/>
+ *         <objectName>Object name</objectName>
+ *         <objectNameSystem/>
+ *         <objectNameType/>
+ *         <objectNameNote/>
+ *         <objectNameLevel/>
+ *       </objectNameGroup>
+ *       <objectNameGroup>
+ *         <objectNameCurrency/>
+ *         <objectNameLanguage/>
+ *         <objectName>Another name</objectName>
+ *         <objectNameSystem/>
+ *         <objectNameType/>
+ *         <objectNameNote/>
+ *         <objectNameLevel/>
+ *       </objectNameGroup>
+ *     </objectNameList>
+ *     <comments>
+ *       <comment>Some comment text</comment>
+ *       <comment>Another comment</comment>
+ *     </comments>
+ *   </ns2:collectionobjects_common>
+ * </document>
+ * </pre>
+ * 
+ * This implementation uses a streaming JSON parser and a streaming
+ * XML writer to do a direct stream-to-stream conversion, without
+ * building a complete in-memory representation of the document.
+ */
+public class JsonToXmlStreamConverter {
+    
+    /**
+     * The JSON parser used to parse the input stream.
+     */
+    protected JsonParser jsonParser;
+    
+    /**
+     * The XML writer used to write to the output stream.
+     */
+    protected XMLStreamWriter xmlWriter;
+    
+    /**
+     * A stack used to track the state of JSON parsing.
+     * JsonField instances are pushed onto the stack as fields
+     * are entered, and popped off as fields are exited.
+     * References to fields are not retained once they
+     * are popped from the stack, so there is never a full
+     * representation of the JSON document in memory.
+     */
+    protected Stack<JsonField> stack = new Stack<JsonField>();
+    
+    /**
+     * Creates an JsonToXmlStreamConverter that reads JSON from an input stream,
+     * and writes XML to an output stream.
+     * 
+     * @param in the JSON input stream
+     * @param out the XML output stream
+     * @throws JsonParseException
+     * @throws IOException
+     * @throws XMLStreamException
+     */
+    public JsonToXmlStreamConverter(InputStream in, OutputStream out) throws JsonParseException, IOException, XMLStreamException {
+        JsonFactory jsonFactory = new JsonFactory();
+        XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance();
+
+        jsonParser = jsonFactory.createParser(in);
+        xmlWriter = xmlFactory.createXMLStreamWriter(out);
+    }
+    
+    /**
+     * Performs the conversion.
+     * 
+     * @throws JsonParseException
+     * @throws IOException
+     * @throws XMLStreamException
+     */
+    public void convert() throws JsonParseException, IOException, XMLStreamException {
+        xmlWriter.writeStartDocument();
+        
+        // Read tokens from the input stream, and dispatch to handlers.
+        // Handlers may write XML to the output stream.
+        
+        while (jsonParser.nextToken() != null) {
+            JsonToken token = jsonParser.currentToken();
+            
+            switch(token) {
+                case FIELD_NAME:
+                    onFieldName(jsonParser.getText());
+                    break;
+                case VALUE_STRING:
+                    onScalar(jsonParser.getText());
+                    break;
+                case VALUE_NULL:
+                    onScalar("");
+                    break;
+                case START_OBJECT:
+                    onStartObject();
+                    break;
+                case END_OBJECT:
+                    onEndObject();
+                    break;
+                case START_ARRAY:
+                    onStartArray();
+                    break;
+                case END_ARRAY:
+                    onEndArray();
+                    break;
+                case VALUE_TRUE:
+                    onScalar("true");
+                    break;
+                case VALUE_FALSE:
+                    onScalar("false");
+                    break;
+                case VALUE_NUMBER_INT:
+                    onScalar(jsonParser.getValueAsString());
+                    break;
+                case VALUE_NUMBER_FLOAT:
+                    onScalar(jsonParser.getValueAsString());
+                    break;
+                default:
+            }
+        }
+        
+        xmlWriter.writeEndDocument();
+    }
+
+    /**
+     * Event handler executed when a field name is encountered
+     * in the input stream.
+     * 
+     * @param name the field name
+     */
+    public void onFieldName(String name) {
+        // Push the field onto the stack.
+        
+        stack.push(new JsonField(name));
+    }
+    
+    /**
+     * Event handler executed when a scalar field value is encountered
+     * in the input stream. Boolean, integer, float, and null values are
+     * converted to strings prior to being passed in.
+     * 
+     * @param value the scalar value, as a string
+     * @throws XMLStreamException
+     * @throws IOException
+     */
+    public void onScalar(String value) throws XMLStreamException, IOException {
+        JsonField field = stack.peek();
+        String name = field.getName();
+        
+        if (field.isScalar() && isXmlAttribute(name)) {
+            // We're in a scalar field whose name looks like an XML attribute
+            // or namespace declaration.
+            
+            if (isXmlNamespace(name)) {
+                // It looks like a namespace declaration.
+                // Output an XML namespace declaration.
+                
+                String prefix = jsonFieldNameToXmlNamespacePrefix(name);
+                String namespaceUri = value;
+                
+                xmlWriter.writeNamespace(prefix, namespaceUri);
+            }
+            else {
+                // It looks like an attribute.
+                // Output an XML attribute.
+                
+                String localName = jsonFieldNameToXmlAttributeName(name);
+                
+                xmlWriter.writeAttribute(localName, value);
+            }
+        }
+        else {
+            // It doesn't look like an XML attribute or namespace declaration.
+            // Output an XML element with the same name as the field, whose
+            // contents are the value.
+            
+            xmlWriter.writeStartElement(name);
+            xmlWriter.writeCharacters(value);
+            xmlWriter.writeEndElement();
+        }
+
+        if (!field.isArray()) {
+            // If the field we're in is not an array, we're done with it.
+            // Pop it off the stack.
+            
+            // If it is an array, there may be more values to come. The
+            // field shouldn't be popped until the end of array is
+            // found.
+            
+            stack.pop();
+        }
+    }
+    
+    /**
+     * Event handler executed when an object start ({) is encountered
+     * in the input stream.
+     * 
+     * @throws XMLStreamException
+     */
+    public void onStartObject() throws XMLStreamException {
+        if (stack.isEmpty()) {
+            // This is the root object. Do nothing.
+            
+            return;
+        }
+        
+        JsonField field = stack.peek();
+        
+        if (field.isArray()) {
+            // If we're in an array, an object should be expanded
+            // into a field with the same name as the array.
+            
+            field = new JsonField(field.getName());
+            stack.push(field);
+        }
+        
+        field.setType(JsonField.Type.OBJECT);
+        
+        // Write an XML start tag to the output stream.
+        
+        xmlWriter.writeStartElement(field.getName());
+    }
+    
+    /**
+     * Event handler executed when an object end (}) is encountered
+     * in the input stream.
+     *
+     * @throws XMLStreamException
+     */
+    public void onEndObject() throws XMLStreamException {
+        if (stack.isEmpty()) {
+            // This is the root object. Do nothing.
+            
+            return;
+        }
+
+        // Pop the current field off the stack.
+        
+        stack.pop();
+        
+        // Write an XML end tag to the output stream.
+        
+        xmlWriter.writeEndElement();
+    }
+    
+    /**
+     * Event handler executed when an array start ([) is encountered
+     * in the input stream.
+     *
+     * @throws IOException
+     * @throws XMLStreamException
+     */
+    public void onStartArray() throws IOException, XMLStreamException {
+        // Set the current field type to array.
+        
+        JsonField field = stack.peek();
+        
+        field.setType(JsonField.Type.ARRAY);
+    }
+    
+    /**
+     * Event handler executed when an array end (]) is encountered
+     * in the input stream.
+     *
+     * @throws XMLStreamException
+     */
+    public void onEndArray() throws XMLStreamException {
+       // Pop the current field off the stack.
+       
+       stack.pop();
+    }
+}
+
index 850e6a2f50a0235f915bd30003d4bf5bf59c12f5..790297c1c677f59fca918e921a81b748205b386e 100644 (file)
@@ -16,6 +16,17 @@ import org.jboss.resteasy.util.MediaTypeHelper;
  * XML/JSON conversion.
  */
 public class RequestUtils {
+    
+    /**
+     * Determines if a request's content type is JSON.
+     * 
+     * @param request the request
+     * @return true if the request contains JSON content, false otherwise
+     */
+    public static boolean isJsonContent(HttpServletRequest request) {
+        return StringUtils.equals(request.getContentType(), MediaType.APPLICATION_JSON);
+    }
+    
     /**
      * Determines if a request's preferred response content
      * type is JSON.
diff --git a/services/common/src/main/java/org/collectionspace/services/common/xmljson/XmlNode.java b/services/common/src/main/java/org/collectionspace/services/common/xmljson/XmlNode.java
new file mode 100644 (file)
index 0000000..99145aa
--- /dev/null
@@ -0,0 +1,389 @@
+package org.collectionspace.services.common.xmljson;
+
+import static org.collectionspace.services.common.xmljson.ConversionUtils.*;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * A lightweight representation of an XML node. Instances are created
+ * by XmlToJsonStreamConverter in the course of parsing XML. This class
+ * differs from a DOM node in that it is intended to contain just the
+ * information needed to generate JSON for CSpace, in a structure that
+ * is optimized for doing that generation.
+ * 
+ * Each XML node has a name, and optionally namespaces and attributes.
+ * The node may contain either text or child nodes (not both, as CSpace
+ * XML is assumed not to contain mixed-content elements).
+ */
+public class XmlNode {
+    /**
+     * The name of the node.
+     */
+    private String name;
+    
+    /**
+     * The text content of the node, if this is a text node.
+     */
+    private String text = "";
+    
+    /**
+     * The namespaces (prefix and uri) of the node, keyed by prefix.
+     */
+    private Map<String, String> namespaces = new LinkedHashMap<String, String>();
+    
+    /**
+     * The attributes (name and value) of the node, keyed by name.
+     */
+    private Map<String, String> attributes = new LinkedHashMap<String, String>();
+    
+    /**
+     * The children of the node, keyed by name. A child may be a single XmlNode,
+     * or a list of XmlNodes, if more than one child has the same name.
+     */
+    private Map<String, Object> children = new LinkedHashMap<String, Object>();
+    
+    /**
+     * Is text is allowed in this node? This starts off as true.
+     * Adding a child node causes it to become false.
+     */
+    private boolean isTextAllowed = true;
+    
+    /**
+     * Should empty children be retained? If false, children that
+     * contain no content at the time of addition are not retained.
+     */
+    private boolean isRetainEmptyChildren = true;
+    
+    /**
+     * Creates an XmlNode.
+     */
+    public XmlNode() {
+        
+    }
+
+    /**
+     * Creates an XmlNode with a name.
+     * 
+     * @param name the name
+     */
+    public XmlNode(String name) {
+        setName(name);
+    }
+    
+    /**
+     * Gets the value of the node. If this is a text node, the
+     * value is a String. Otherwise it's a map of the node's
+     * namespaces, attributes, and children, via
+     * getCombinedMap().
+     * 
+     * Note that namespaces and attributes are not returned
+     * as part of a text node's value. It is assumed that text
+     * nodes do not have namespace declarations or attributes.
+     * 
+     * @return the node's value
+     */
+    @JsonValue
+    public Object getValue() {
+        if (hasChildren()) {
+            return getCombinedMap();
+        }
+            
+        if (hasText()) {
+            return getText();
+        }
+        
+        return null;
+    }
+    
+    /**
+     * Determines if this node has content. A node has
+     * content if it contains non-empty text, or if it has
+     * any children.
+     * 
+     * @return true if the node has no content, false otherwise
+     */
+    public boolean isEmpty() {
+        return (!(hasChildren() || hasText()));
+    }
+    
+    /**
+     * Returns a map containing the node's namespaces, attributes, and
+     * children. The keys for namespaces and attributes are computed
+     * using ConversionUtils.xmlNamespacePrefixToJsonFieldName() and
+     * ConversionUtils.xmlNamespacePrefixToJsonFieldName() respectively.
+     * Children are keyed by their names.
+     * 
+     * @return a map of namespaces, attributes, and children
+     */
+    public Map<String, Object> getCombinedMap() {
+        Map<String, Object> combined = new LinkedHashMap<String, Object>();
+        Map<String, String> namespaces = getNamespaces();
+        Map<String, String> attributes = getAttributes();
+
+        for (String prefix : namespaces.keySet()) {
+            combined.put(xmlNamespacePrefixToJsonFieldName(prefix), namespaces.get(prefix));
+        }
+
+        for (String name : attributes.keySet()) {
+            combined.put(xmlAttributeNameToJsonFieldName(name), attributes.get(name));
+        }
+        
+        combined.putAll(getChildren());
+        
+        return combined;
+    }
+    
+    /**
+     * Returns the name of the node.
+     * 
+     * @return the name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Sets the name of the node.
+     * 
+     * @param name the name
+     */
+    public void setName(String name) {
+        this.name = name;
+    }
+    
+    /**
+     * Returns the text content of the node.
+     * 
+     * @return the text content
+     */
+    public String getText() {
+        return text;
+    }
+
+    /**
+     * Sets the text content of the node. This has
+     * no effect of text is not allowed, as determined
+     * by isTextAllowed().
+     * 
+     * @param text the text content
+     */
+    public void setText(String text) {
+        if (!isTextAllowed()) {
+            return;
+        }
+
+        this.text = text;
+    }
+    
+    /**
+     * Adds text to the text content of the node.
+     * This has no effect of text is not allowed, as
+     * determined by isTextAllowed().
+     * 
+     * @param text the text to append
+     */
+    public void addText(String text) {
+        if (!isTextAllowed()) {
+            return;
+        }
+        
+        this.text = this.text + text;
+    }
+    
+    /**
+     * Determines if this node contains text content.
+     * 
+     * @return true if the node contains text, false if text is not
+     *         allowed in the node, or if the text content is empty
+     */
+    public boolean hasText() {
+        return (isTextAllowed() && StringUtils.isNotEmpty(text));
+    }
+
+    /**
+     * Returns the namespaces of the node.
+     * 
+     * @return a map of namespaces, where keys are namespace prefixes
+     *         and values are namespace uris
+     */
+    public Map<String, String> getNamespaces() {
+        return namespaces;
+    }
+
+    /**
+     * Sets the namespaces of the node.
+     * 
+     * @param namespaces a map of namespaces, where keys are namespace
+     *                   prefixes and values are namespace uris
+     */
+    public void setNamespaces(Map<String, String> namespaces) {
+        this.namespaces = namespaces;
+    }
+    
+    /**
+     * Adds a namespace to the node.
+     * 
+     * @param prefix the namespace prefix
+     * @param uri the namespace uri
+     */
+    public void addNamespace(String prefix, String uri) {
+        this.namespaces.put(prefix, uri);
+    }
+
+    /**
+     * Returns the attributes of the node.
+     * 
+     * @return a map of attributes, where keys are attribute names
+     *         and values are attribute values
+     */
+    public Map<String, String> getAttributes() {
+        return attributes;
+    }
+
+    /**
+     * Sets the attributes of the node.
+     * 
+     * @param attributes a map of attributes, where keys are attribute
+     *                   names and values are attribute values
+     */
+    public void setAttributes(Map<String, String> attributes) {
+        this.attributes = attributes;
+    }
+    
+    /**
+     * Adds an attribute to the node.
+     * 
+     * @param name the attribute name
+     * @param value the attribute value
+     */
+    public void addAttribute(String name, String value) {
+        this.attributes.put(name, value);
+    }
+
+    /**
+     * Returns the children of the node.
+     * 
+     * @return a map of children, where keys are node names and
+     *         values are nodes or lists of nodes (where multiple
+     *         child nodes have the same name)
+     */
+    public Map<String, Object> getChildren() {
+        return children;
+    }
+
+    /**
+     * Sets the children of the node.
+     * 
+     * @param children a map of children, where keys are node names and
+     *                 values are nodes or lists of nodes (where multiple
+     *                 child nodes have the same name)
+     */
+    public void setChildren(Map<String, Object> children) {
+        this.children = children;
+    }
+    
+    /**
+     * Determines if the node has children.
+     * 
+     * @return true if the node has any children, false otherwise
+     */
+    public boolean hasChildren() {
+        return this.children.size() > 0;
+    }
+
+    /**
+     * Adds a child node to this node.
+     * 
+     * If the node contains any text content, the text content
+     * is removed, and text content is disallowed from being added
+     * in the future.
+     * 
+     * If the node to be added contains no content, and
+     * isDiscardEmptyChildren() is true, the node is not added.
+     * 
+     * @param node the node to add as a child
+     */
+    public void addChild(XmlNode node) {
+        // Assume mixed content is not allowed. If a child node is
+        // added, text is no longer allowed, and any existing
+        // text is removed.
+        
+        setTextAllowed(false);
+        
+        if (node.isEmpty() && !isRetainEmptyChildren()) {
+            return;
+        }
+        
+        Map<String, Object> children = this.getChildren();
+        String name = node.getName();
+        
+        if (children.containsKey(name)) {
+            Object existing = children.get(name);
+            
+            if (existing instanceof List) {
+                ((List<XmlNode>) existing).add(node);
+            }
+            else if (existing instanceof XmlNode) {
+                List<XmlNode> list = new ArrayList<XmlNode>();
+                
+                list.add((XmlNode) existing);
+                list.add(node);
+                
+                children.put(name, list);
+            }
+        }
+        else {
+            children.put(name, node);
+        }
+    }
+
+    /**
+     * Determines if text content is allowed in this node.
+     * 
+     * @return true if text content is allowed, false otherwise
+     */
+    public boolean isTextAllowed() {
+        return isTextAllowed;
+    }
+
+    /**
+     * Sets whether or not text content is allowed in this node.
+     * 
+     * @param isTextAllowed true if text content should be allowed,
+     *                      false otherwise
+     */
+    public void setTextAllowed(boolean isTextAllowed) {
+        if (!isTextAllowed) {
+            setText("");
+        }
+        
+        this.isTextAllowed = isTextAllowed;
+    }
+
+    /**
+     * Determines if empty children should be retained.
+     * 
+     * @return true if empty children should be retained,
+     *         false otherwise
+     */
+    public boolean isRetainEmptyChildren() {
+        return isRetainEmptyChildren;
+    }
+
+    /**
+     * Sets whether or not empty children should be retained.
+     * 
+     * @param isRetainEmptyChildren true if empty children should be retained,
+     *                              false otherwise
+     */
+    public void setRetainEmptyChildren(boolean isRetainEmptyChildren) {
+        this.isRetainEmptyChildren = isRetainEmptyChildren;
+    }
+}
index f849971edd07e5e42854d3ab6033358a8337b5f2..f7add54f4997ab58405f45055087a1156445eed0 100644 (file)
@@ -3,7 +3,6 @@ package org.collectionspace.services.common.xmljson;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.io.PrintWriter;
 import java.util.Iterator;
 import java.util.Stack;
 
@@ -17,40 +16,240 @@ import javax.xml.stream.events.Namespace;
 import javax.xml.stream.events.StartElement;
 import javax.xml.stream.events.XMLEvent;
 
-import org.apache.commons.lang3.StringUtils;
-import org.collectionspace.services.common.xmljson.parsetree.Node;
-
 import com.fasterxml.jackson.core.JsonGenerationException;
 import com.fasterxml.jackson.databind.JsonMappingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
+/**
+ * Converts a CSpace XML payload to a JSON payload.
+ *
+ * This class is not intended to serve as a general purpose XML to JSON
+ * translator. It is instead a lightweight processor tuned for the kinds
+ * of XML generated by CSpace, and the particular transformations needed
+ * to generate JSON for CSpace. The XML input is expected to conform to
+ * conventions (described below) of CSpace XML payloads.
+ * 
+ * The conversion is performed as follows:
+ * <ul>
+ * <li>XML elements are converted to identically-named JSON fields.</li>
+ * <li>XML attributes are converted to JSON fields prepended with "@".</li>
+ * <li>XML namespace declarations are converted to JSON fields prepended with "@xmlns:".</li>
+ * <li>Sibling XML elements that have the same name are converted to JSON arrays.</li>
+ * </ul>
+ *
+ * This implementation is schema-unaware. It operates by examining only the input
+ * document, without utilizing any XML schema information. This allows for speed
+ * and simplicity, but has some consequences:
+ *
+ * <ul>
+ * <li>Since type information is not available, all text content is converted to
+ *     JSON strings.</li>
+ * <li>Lists are inferred by the presence of multiple child elements with
+ *     the same name. If an element contains only one child with a given name, it
+ *     will not be converted to a JSON array, even if multiples are allowed by
+ *     the XML schema.</li>
+ * <li>Lists are not known ahead of time, and must be inferred by the presence of
+ *     multiple identically-named children. This means that all children of an element
+ *     must be known before JSON for that element can be generated. This makes it
+ *     necessary to read the entire XML document into memory first, instead of
+ *     doing a direct stream-to-stream conversion.</li>
+ * </ul>
+ *
+ * Example:
+ * 
+ * XML
+ * <pre>
+ * <document name="collectionobjects">
+ *   <ns2:collectionspace_core xmlns:ns2="http://collectionspace.org/collectionspace_core/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ *     <createdBy>admin@core.collectionspace.org</createdBy>
+ *     <createdAt>2016-07-27T04:31:38.290Z</createdAt>
+ *   </ns2:collectionspace_core>
+ *   <ns2:collectionobjects_common xmlns:ns2="http://collectionspace.org/services/collectionobject" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ *     <objectNumber>2016.1.1</objectNumber>
+ *     <objectNameList>
+ *       <objectNameGroup>
+ *         <objectNameCurrency/>
+ *         <objectNameLanguage/>
+ *         <objectName>Object name</objectName>
+ *         <objectNameSystem/>
+ *         <objectNameType/>
+ *         <objectNameNote/>
+ *         <objectNameLevel/>
+ *       </objectNameGroup>
+ *       <objectNameGroup>
+ *         <objectNameCurrency/>
+ *         <objectNameLanguage/>
+ *         <objectName>Another name</objectName>
+ *         <objectNameSystem/>
+ *         <objectNameType/>
+ *         <objectNameNote/>
+ *         <objectNameLevel/>
+ *       </objectNameGroup>
+ *     </objectNameList>
+ *     <comments>
+ *       <comment>Some comment text</comment>
+ *       <comment>Another comment</comment>
+ *     </comments>
+ *   </ns2:collectionobjects_common>
+ * </document>
+ * </pre>
+ * 
+ * JSON
+ * <pre>
+ * {
+ *   "document": {
+ *     "@name": "collectionobjects",
+ *     "ns2:collectionspace_core": {
+ *       "@xmlns:ns2": "http://collectionspace.org/collectionspace_core/",
+ *       "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
+ *       "createdBy": "admin@core.collectionspace.org",
+ *       "createdAt": "2016-07-27T04:31:38.290Z"
+ *     },
+ *     "ns2:collectionobjects_common": {
+ *       "@xmlns:ns2": "http://collectionspace.org/services/collectionobject",
+ *       "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
+ *       "objectNumber": "2016.1.1",
+ *       "objectNameList": {
+ *         "objectNameGroup": [
+ *           {
+ *             "objectNameCurrency": null,
+ *             "objectNameLanguage": null,
+ *             "objectName": "Object name",
+ *             "objectNameSystem": null,
+ *             "objectNameType": null,
+ *             "objectNameNote": null,
+ *             "objectNameLevel": null
+ *           },
+ *           {
+ *             "objectNameCurrency": null,
+ *             "objectNameLanguage": null,
+ *             "objectName": "Another name",
+ *             "objectNameSystem": null,
+ *             "objectNameType": null,
+ *             "objectNameNote": null,
+ *             "objectNameLevel": null
+ *           }
+ *         ]
+ *       },
+ *       "comments": {
+ *         "comment": [
+ *           "Some comment text",
+ *           "Another comment"
+ *         ]
+ *       }
+ *     }
+ *   }
+ * }
+ * </pre>
+ * 
+ * The conversion algorithm assumes that the input XML adheres to the following 
+ * conventions:
+ * 
+ * <ul>
+ * <li>The XML does not contain mixed-content elements. Elements may contain text
+ *     or child elements, but not both. If an element contains child elements,
+ *     any text adjacent to those child elements is discarded.</li>
+ * <li>The XML does not have namespace declarations or attributes on text elements.
+ *     If namespace declarations or attributes appear on elements containing
+ *     only text, they are discarded.</li>
+ * <li>The XML does not contain sequences of identically-named elements that are
+ *     interrupted by other elements; or if it does, those interruptions are not
+ *     important. For example, the parent node below contains a list of item
+ *     elements, interrupted by an other element:
+ *     
+ *     <pre>
+ *       <parent>
+ *         <item>a</item>
+ *         <item>b</item>
+ *         <item>c</item>
+ *         <other>uh oh</other>
+ *         <item>d</item>
+ *         <item>e</item>
+ *       </parent>
+ *     </pre>
+ *     
+ *     This is translated to:
+ *     
+ *     <pre>
+ *       "parent": {
+ *          "item": [
+ *            "a",
+ *            "b",
+ *            "c",
+ *            "d",
+ *            "e"
+ *          ],
+ *          "other": "uh oh"
+ *       }
+ *     </pre>
+ *     
+ *     All of the item children of parent are converted into a single
+ *     list, so the placement of the other element is not retained in
+ *     JSON.
+ * </li>
+ * </ul>
+ * 
+ * This implementation uses a StAX parser to generate a lightweight 
+ * representation of the input XML document in memory, performs the
+ * necessary transformations, and outputs a JSON rendering of the
+ * transformed document. A direct stream-to-stream conversion is
+ * not possible because of the need to collect identically-named
+ * XML elements for output as a JSON array; for any element, all children
+ * must be known before JSON for that element may be written to the
+ * output stream.
+ */
 public class XmlToJsonStreamConverter {
-    protected XMLEventReader eventReader;
-    protected PrintWriter writer;
-    protected Stack<Node> stack = new Stack<Node>();
-    protected Node parseResult = null;
+    /**
+     * The StAX event reader used to parse the XML input stream.
+     */
+    protected XMLEventReader xmlEventReader;
     
-    public static String nodeNameForQName(QName name) {
-        String prefix = name.getPrefix();
-        String localPart = name.getLocalPart();
-        
-        if (StringUtils.isNotEmpty(prefix)) {
-            return prefix + ":" + localPart;
-        }
-        
-        return localPart;
-    }
     
+    /**
+     * The JSON output stream.
+     */
+    protected OutputStream jsonStream;
+    
+    /**
+     * A stack used to track the current state of XML parsing.
+     * XmlNode instances are pushed onto the stack as elements
+     * are entered, and popped off as elements are exited.
+     */
+    protected Stack<XmlNode> stack = new Stack<XmlNode>();
+    
+    /**
+     * The result of parsing the XML.
+     */
+    protected XmlNode parseResult = null;
+    
+    /**
+     * Creates an XmlToJsonStreamConverter that reads XML from an input stream,
+     * and writes JSON to an output stream.
+     * 
+     * @param in the XML input stream
+     * @param out the JSON output stream
+     * @throws XMLStreamException
+     */
     public XmlToJsonStreamConverter(InputStream in, OutputStream out) throws XMLStreamException {
         XMLInputFactory factory = XMLInputFactory.newInstance();
         
-        eventReader = factory.createXMLEventReader(in);
-        writer = new PrintWriter(out);
+        xmlEventReader = factory.createXMLEventReader(in);
+        jsonStream = out;
     }
     
+    /**
+     * Performs the conversion.
+     * 
+     * @throws XMLStreamException
+     * @throws JsonGenerationException
+     * @throws JsonMappingException
+     * @throws IOException
+     */
     public void convert() throws XMLStreamException, JsonGenerationException, JsonMappingException, IOException {
-        while(eventReader.hasNext()){
-            XMLEvent event = eventReader.nextEvent();
+        // Read in the XML stream.
+        
+        while(xmlEventReader.hasNext()) {
+            XMLEvent event = xmlEventReader.nextEvent();
             
             switch(event.getEventType()) {
                 case XMLStreamConstants.CHARACTERS:
@@ -71,34 +270,58 @@ public class XmlToJsonStreamConverter {
             }
         }
         
+        // The XML has been parsed into parseResult.
+        // Write it out as JSON.
+        
         ObjectMapper objectMapper = new ObjectMapper();
-        objectMapper.writeValue(writer, parseResult);
+        objectMapper.writeValue(jsonStream, parseResult);
         
-        writer.flush();
+        jsonStream.flush();
     }
     
+    /**
+     * Event handler executed when the start of the XML document is
+     * encountered in the input stream.
+     * 
+     * @param event the event
+     */
     protected void onStartDocument(XMLEvent event) {
-        stack.push(new Node());
+        // Push an unnamed node on the stack to represent the
+        // document.
+        
+        stack.push(new XmlNode());
     }
 
+    /**
+     * Event handler executed when the end of the XML document is
+     * encountered in the input stream.
+     * 
+     * @param event the event
+     */
     protected void onEndDocument(XMLEvent event) {
+        // The last remaining node on the stack should be
+        // the one representing the document. Pop it and
+        // store it in parseResult.
+        
         parseResult = stack.pop();
     }
     
+    /**
+     * Event handler executed when the start of an XML element is
+     * encountered in the input stream.
+     * 
+     * @param event the event
+     */
     @SuppressWarnings("unchecked")
     protected void onStartElement(XMLEvent event) {
+        // Create a node to represent the element.
+        
         StartElement element = event.asStartElement();
         QName name = element.getName();
 
-        Node node = new Node(nodeNameForQName(name));
+        XmlNode node = new XmlNode(ConversionUtils.jsonFieldNameFromXMLQName(name));
 
-        Iterator<Attribute> attrIter = element.getAttributes();
-        
-        while(attrIter.hasNext()) {
-            Attribute attr = attrIter.next();
-            
-            node.addAttribute(attr.getName().toString(), attr.getValue());
-        }
+        // Add namespace declarations, if any.
         
         Iterator<Namespace> nsIter = element.getNamespaces();
         
@@ -107,21 +330,55 @@ public class XmlToJsonStreamConverter {
             
             node.addNamespace(ns.getPrefix(), ns.getNamespaceURI());
         }
+
+        // Add attributes, if any.
+        
+        Iterator<Attribute> attrIter = element.getAttributes();
+        
+        while(attrIter.hasNext()) {
+            Attribute attr = attrIter.next();
+            
+            node.addAttribute(attr.getName().toString(), attr.getValue());
+        }
+        
+        // Push the node onto the stack.
         
         stack.push(node);
     }
-    
+
+    /**
+     * Event handler executed when the end of an XML element is
+     * encountered in the input stream.
+     * 
+     * @param event the event
+     */
+    protected void onEndElement(XMLEvent event) {
+        // Pop the node corresponding to this element off the stack.
+        
+        XmlNode node = stack.pop();
+        XmlNode parent = stack.peek();
+        
+        // Add the node to its parent. This is done here instead of
+        // in onStartElement(), because we now know the entire contents
+        // of the element. This gives us the possibility to prevent
+        // adding elements that are empty. In onStartElement(), we don't
+        // yet know if the element is going to be empty.
+        
+        parent.addChild(node);
+    }
+
+    /**
+     * Event handler executed when character content is
+     * encountered in the input stream.
+     * 
+     * @param event the event
+     */
     protected void onCharacters(XMLEvent event) {
+        // Add the text to the parent element.
+        
         String text = event.asCharacters().getData();
-        Node parent = stack.peek();
+        XmlNode parent = stack.peek();
         
         parent.addText(text);
     }
-    
-    protected void onEndElement(XMLEvent event) {
-        Node node = stack.pop();
-        Node parent = stack.peek();
-        
-        parent.addChild(node);
-    }
 }
diff --git a/services/common/src/main/java/org/collectionspace/services/common/xmljson/parsetree/Node.java b/services/common/src/main/java/org/collectionspace/services/common/xmljson/parsetree/Node.java
deleted file mode 100644 (file)
index b66dc1c..0000000
+++ /dev/null
@@ -1,186 +0,0 @@
-package org.collectionspace.services.common.xmljson.parsetree;
-
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-import org.apache.commons.lang3.StringUtils;
-
-import com.fasterxml.jackson.annotation.JsonValue;
-
-/**
- * 
- */
-public class Node {
-    private String name;
-    private String text = "";
-    private Map<String, String> namespaces = new LinkedHashMap<String, String>();
-    private Map<String, String> attributes = new LinkedHashMap<String, String>();
-    private Map<String, Object> children = new LinkedHashMap<String, Object>();
-    private boolean isTextAllowed = true;
-    
-    public static String hashKeyForAttributeName(String name) {
-        return "@" + name;
-    }
-
-    public static String hashKeyForNamespacePrefix(String prefix) {
-        return hashKeyForAttributeName("xmlns:" + prefix);
-    }
-    
-    public Node() {
-        
-    }
-
-    public Node(String name) {
-        setName(name);
-    }
-    
-    @JsonValue
-    public Object getValue() {
-        if (hasChildren()) {
-            return getCombinedMap();
-        }
-            
-        if (hasText()) {
-            return getText();
-        }
-        
-        return null;
-    }
-    
-    public boolean isEmpty() {
-        return (!(hasChildren() || hasText()));
-    }
-    
-    public Map<String, Object> getCombinedMap() {
-        Map<String, Object> combined = new LinkedHashMap<String, Object>();
-        Map<String, String> namespaces = getNamespaces();
-        Map<String, String> attributes = getAttributes();
-
-        for (String prefix : namespaces.keySet()) {
-            combined.put(hashKeyForNamespacePrefix(prefix), namespaces.get(prefix));
-        }
-
-        for (String name : attributes.keySet()) {
-            combined.put(hashKeyForAttributeName(name), attributes.get(name));
-        }
-        
-        combined.putAll(getChildren());
-        
-        return combined;
-    }
-    
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-    
-    public String getText() {
-        return text;
-    }
-
-    public void setText(String text) {
-        if (!isTextAllowed()) {
-            return;
-        }
-
-        this.text = text;
-    }
-    
-    public void addText(String text) {
-        if (!isTextAllowed()) {
-            return;
-        }
-        
-        this.text = this.text + text;
-    }
-    
-    public boolean hasText() {
-        return (isTextAllowed() && StringUtils.isNotEmpty(text));
-    }
-
-    public Map<String, String> getNamespaces() {
-        return namespaces;
-    }
-
-    public void setNamespaces(Map<String, String> namespaces) {
-        this.namespaces = namespaces;
-    }
-    
-    public void addNamespace(String prefix, String uri) {
-        this.namespaces.put(prefix, uri);
-    }
-
-    public Map<String, String> getAttributes() {
-        return attributes;
-    }
-
-    public void setAttributes(Map<String, String> attributes) {
-        this.attributes = attributes;
-    }
-    
-    public void addAttribute(String name, String value) {
-        this.attributes.put(name, value);
-    }
-
-    public Map<String, Object> getChildren() {
-        return children;
-    }
-
-    public void setChildren(Map<String, Object> children) {
-        this.children = children;
-    }
-    
-    public boolean hasChildren() {
-        return this.children.size() > 0;
-    }
-
-    public void addChild(Node node) {
-        // Assume mixed content is not allowed. If a child node is
-        // added, text is no longer allowed, and any existing
-        // text is removed.
-        
-        if (isTextAllowed()) {
-            setText("");
-            setTextAllowed(false);
-        }
-        
-        if (node.isEmpty()) {
-            return;
-        }
-        
-        Map<String, Object> children = this.getChildren();
-        String name = node.getName();
-        
-        if (children.containsKey(name)) {
-            Object existing = children.get(name);
-            
-            if (existing instanceof List) {
-                ((List<Node>) existing).add(node);
-            }
-            else if (existing instanceof Node) {
-                List<Node> list = new ArrayList<Node>();
-                
-                list.add((Node) existing);
-                list.add(node);
-                
-                children.put(name, list);
-            }
-        }
-        else {
-            children.put(name, node);
-        }
-    }
-
-    public boolean isTextAllowed() {
-        return isTextAllowed;
-    }
-
-    public void setTextAllowed(boolean isTextAllowed) {
-        this.isTextAllowed = isTextAllowed;
-    }
-}
diff --git a/services/common/src/test/java/org/collectionspace/services/common/xmljson/test/ConversionUtilsTest.java b/services/common/src/test/java/org/collectionspace/services/common/xmljson/test/ConversionUtilsTest.java
new file mode 100644 (file)
index 0000000..87f4c8c
--- /dev/null
@@ -0,0 +1,55 @@
+package org.collectionspace.services.common.xmljson.test;
+
+import static org.collectionspace.services.common.xmljson.ConversionUtils.*;
+import static org.testng.Assert.*;
+
+import javax.xml.namespace.QName;
+
+import org.testng.annotations.Test;
+
+public class ConversionUtilsTest {
+
+    @Test
+    public void testXmlAttributeNameToJsonFieldName() {
+        assertEquals(xmlAttributeNameToJsonFieldName("xyz"), "@xyz");
+    }
+    
+    @Test
+    public void testJsonFieldNameToXmlAttributeName() {
+        assertEquals(jsonFieldNameToXmlAttributeName("@xyz"), "xyz");
+    }
+    
+    @Test
+    public void testXmlNamespacePrefixToJsonFieldName() {
+        assertEquals(xmlNamespacePrefixToJsonFieldName("ns2"), "@xmlns:ns2");
+    }
+
+    @Test
+    public void testJsonFieldNameToXmlNamespacePrefix() {
+        assertEquals(jsonFieldNameToXmlNamespacePrefix("@xmlns:ns2"), "ns2");
+    }
+    
+    @Test
+    public void testIsXmlAttribute() {
+        assertTrue(isXmlAttribute("@name"));
+        assertTrue(isXmlAttribute("@xmlns:hello"));
+
+        assertFalse(isXmlAttribute("name"));
+        assertFalse(isXmlAttribute("xmlns:hello"));
+    }
+    
+    @Test
+    public void testIsXmlNamespace() {
+        assertTrue(isXmlNamespace("@xmlns:hello"));
+
+        assertFalse(isXmlNamespace("@name"));
+        assertFalse(isXmlNamespace("name"));
+        assertFalse(isXmlNamespace("xmlns:hello"));
+    }
+    
+    @Test
+    public void testJsonFieldNameFromXMLQName() {
+        assertEquals(jsonFieldNameFromXMLQName(new QName("foo")), "foo");
+        assertEquals(jsonFieldNameFromXMLQName(new QName("http://foo.com", "foo", "ns")), "ns:foo");
+    }
+}
diff --git a/services/common/src/test/java/org/collectionspace/services/common/xmljson/test/JsonFieldTest.java b/services/common/src/test/java/org/collectionspace/services/common/xmljson/test/JsonFieldTest.java
new file mode 100644 (file)
index 0000000..18403f0
--- /dev/null
@@ -0,0 +1,34 @@
+package org.collectionspace.services.common.xmljson.test;
+
+import static org.testng.Assert.*;
+
+import org.collectionspace.services.common.xmljson.JsonField;
+import org.testng.annotations.Test;
+
+public class JsonFieldTest {
+    @Test
+    public void testJsonField() {
+        JsonField field = new JsonField("name");
+        
+        assertEquals(field.getName(), "name");
+        assertTrue(field.isScalar());
+        assertFalse(field.isArray());
+        assertEquals(field.getType(), JsonField.Type.SCALAR);
+        
+        field.setName("newName");
+        
+        assertEquals(field.getName(), "newName");
+        
+        field.setType(JsonField.Type.ARRAY);
+        
+        assertFalse(field.isScalar());
+        assertTrue(field.isArray());
+        assertEquals(field.getType(), JsonField.Type.ARRAY);
+        
+        field.setType(JsonField.Type.OBJECT);
+        
+        assertFalse(field.isScalar());
+        assertFalse(field.isArray());
+        assertEquals(field.getType(), JsonField.Type.OBJECT);
+    }
+}
diff --git a/services/common/src/test/java/org/collectionspace/services/common/xmljson/test/JsonToXmlStreamConverterTest.java b/services/common/src/test/java/org/collectionspace/services/common/xmljson/test/JsonToXmlStreamConverterTest.java
new file mode 100644 (file)
index 0000000..154b802
--- /dev/null
@@ -0,0 +1,84 @@
+package org.collectionspace.services.common.xmljson.test;
+
+import static org.testng.Assert.*;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+import javax.xml.stream.XMLStreamException;
+
+import org.apache.commons.io.output.ByteArrayOutputStream;
+import org.collectionspace.services.common.xmljson.JsonToXmlStreamConverter;
+import org.testng.annotations.Test;
+import org.xmlunit.builder.DiffBuilder;
+import org.xmlunit.builder.Input;
+import org.xmlunit.diff.Diff;
+
+import com.fasterxml.jackson.core.JsonParseException;
+
+public class JsonToXmlStreamConverterTest {
+    public final String FILE_PATH = "test-data/xmljson/";
+    
+    @Test
+    public void testConvert() throws XMLStreamException, JsonParseException, IOException {
+        testConvert("record");
+        testConvert("collectionobject");
+        testConvert("collectionobject-list");
+        testConvert("accountperms");
+        testConvert("permissions");
+        testConvert("vocabulary-items");
+        testConvert("numeric-json");
+        testConvert("boolean-json");
+        testConvertThrows("empty-json", XMLStreamException.class);
+    }
+    
+    private void testConvert(String fileName) throws XMLStreamException, IOException {
+        System.out.println("---------------------------------------------------------");
+        System.out.println("Converting JSON to XML: " + fileName);
+        System.out.println("---------------------------------------------------------");
+
+        ClassLoader classLoader = getClass().getClassLoader();
+        File jsonFile = new File(classLoader.getResource(FILE_PATH + fileName + ".json").getFile());
+        File xmlFile = new File(classLoader.getResource(FILE_PATH + fileName + ".xml").getFile());
+        
+        FileInputStream in = new FileInputStream(jsonFile);
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        
+        JsonToXmlStreamConverter converter = new JsonToXmlStreamConverter(in, out);
+        converter.convert();
+        
+        System.out.println(out.toString("UTF-8"));
+        
+        Diff diff = DiffBuilder
+                .compare(Input.fromStream(out.toInputStream()))
+                .withTest(Input.fromFile(xmlFile))
+                .ignoreComments()
+                .ignoreWhitespace()
+                .build();
+        
+        System.out.println(diff.toString());
+        
+        assertFalse(diff.hasDifferences());
+    }
+    
+    private void testConvertThrows(String fileName, Class<?> exceptionClass) throws XMLStreamException, IOException {
+        boolean caught = false;
+        
+        try {
+            testConvert(fileName);
+        }
+        catch(XMLStreamException|IOException e) {
+            if (e.getClass().isAssignableFrom(exceptionClass)) {
+                caught = true;
+                
+                System.out.println(e.toString());
+            }
+            else {
+                throw e;
+            }
+        }
+        
+        assertTrue(caught);
+    }
+}
index 1d0db320a2effc76ae4713ecec1d80908c01dff0..dfed6c5933f90e6180d6e03db6957a733249bac9 100644 (file)
@@ -13,6 +13,13 @@ import org.testng.annotations.Test;
 
 public class RequestUtilsTest {
 
+    @Test
+    public void testIsJsonContent() {
+        assertFalse(isJsonContent(requestWithContentType(null)));
+        assertFalse(isJsonContent(requestWithContentType("application/xml")));
+        assertTrue(isJsonContent(requestWithContentType("application/json")));
+    }
+    
     @Test
     public void testIsJsonPreferred() {
         assertEquals(
@@ -92,4 +99,15 @@ public class RequestUtilsTest {
         
         return request;
     }
+    
+    private HttpServletRequest requestWithContentType(String contentType) {
+        HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
+        
+        EasyMock.expect(request.getContentType())
+            .andReturn(contentType);
+        
+        EasyMock.replay(request);
+        
+        return request;
+    }
 }
diff --git a/services/common/src/test/java/org/collectionspace/services/common/xmljson/test/XmlNodeTest.java b/services/common/src/test/java/org/collectionspace/services/common/xmljson/test/XmlNodeTest.java
new file mode 100644 (file)
index 0000000..1eb58aa
--- /dev/null
@@ -0,0 +1,133 @@
+package org.collectionspace.services.common.xmljson.test;
+
+import java.util.HashMap;
+
+import org.apache.commons.lang3.StringUtils;
+import org.collectionspace.services.common.xmljson.XmlNode;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+public class XmlNodeTest {
+    
+    @Test void testConstructor() {
+        XmlNode node = new XmlNode();
+        
+        assertNull(node.getName());
+
+        XmlNode namedNode = new XmlNode("name");
+
+        assertEquals(namedNode.getName(), "name");
+    }
+    
+    @Test
+    public void testXmlNodeWithText() {
+        XmlNode node = new XmlNode("collectionspace_core");
+        
+        assertEquals(node.getName(), "collectionspace_core");
+        
+        node.setName("collectionobjects_common");
+        
+        assertEquals(node.getName(), "collectionobjects_common");
+        assertTrue(node.isEmpty());
+        assertFalse(node.hasChildren());
+        assertFalse(node.hasText());
+        assertTrue(node.isTextAllowed());
+        
+        node.setText("some text");
+        
+        assertEquals(node.getText(), "some text");
+        assertTrue(node.hasText());
+        
+        node.addText(" and more");
+        
+        assertEquals(node.getText(), "some text and more");
+        assertTrue(node.hasText());
+
+        node.addAttribute("name", "value");
+        node.addNamespace("ns", "http://collectionspace.org");
+        
+        assertEquals(node.getAttributes().get("name"), "value");
+        assertEquals(node.getNamespaces().get("ns"), "http://collectionspace.org");
+        
+        assertEquals(node.getCombinedMap(), new HashMap<String, Object>() {{
+            put("@name", "value");
+            put("@xmlns:ns", "http://collectionspace.org");
+        }});
+        
+        assertEquals(node.getValue(), "some text and more");
+        
+        node.setTextAllowed(false);
+        
+        assertFalse(node.isTextAllowed());
+        assertTrue(StringUtils.isEmpty(node.getText()));
+        
+        node.addText("hello");
+
+        assertTrue(StringUtils.isEmpty(node.getText()));
+
+        node.setText("hello");
+
+        assertTrue(StringUtils.isEmpty(node.getText()));
+        assertNull(node.getValue());
+    }
+    
+    @Test
+    public void testXmlNodeWithChildren() {
+        XmlNode node = new XmlNode("collectionspace_core");
+
+        assertFalse(node.hasChildren());
+        assertEquals(node.getCombinedMap(), new HashMap<String, Object>());
+        assertNull(node.getValue());
+        assertTrue(node.isRetainEmptyChildren());
+        
+        final XmlNode descNode = new XmlNode("description");
+        
+        node.addChild(descNode);
+
+        assertEquals(node.getChildren(), new HashMap<String, Object>() {{
+            put("description", descNode);
+        }});
+        
+        assertEquals(node.getCombinedMap(), new HashMap<String, Object>() {{
+            put("description", descNode);
+        }});
+        
+        assertEquals(node.getValue(), new HashMap<String, Object>() {{
+            put("description", descNode);
+        }});
+        
+        assertFalse(node.isTextAllowed());
+        
+        node.addAttribute("name", "value");
+        node.addNamespace("ns", "http://collectionspace.org");
+
+        assertEquals(node.getCombinedMap(), new HashMap<String, Object>() {{
+            put("@name", "value");
+            put("@xmlns:ns", "http://collectionspace.org");
+            put("description", descNode);
+        }});
+
+        assertEquals(node.getValue(), new HashMap<String, Object>() {{
+            put("@name", "value");
+            put("@xmlns:ns", "http://collectionspace.org");
+            put("description", descNode);
+        }});
+        
+        node.setRetainEmptyChildren(false);
+        
+        assertFalse(node.isRetainEmptyChildren());
+        
+        final XmlNode nameNode = new XmlNode("name");
+        
+        assertTrue(nameNode.isEmpty());
+        
+        node.addChild(nameNode);
+        
+        // Should not have been retained
+        
+        assertEquals(node.getChildren(), new HashMap<String, Object>() {{
+            put("description", descNode);
+        }});
+    }
+}
index ee66e2fde7745d73fa41fd64315e61f7b0687606..40e9b2ad7116085bb8864f276fa317bc22683dc2 100644 (file)
@@ -1,6 +1,6 @@
 package org.collectionspace.services.common.xmljson.test;
 
-import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.*;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -25,7 +25,7 @@ public class XmlToJsonStreamConverterTest {
     private JsonFactory jsonFactory = mapper.getFactory();
     
     @Test
-    public void testConvert() throws XMLStreamException, JsonParseException, IOException {
+    public void testConvert() throws XMLStreamException, IOException {
         testConvert("record");
         testConvert("collectionobject");
         testConvert("collectionobject-list");
@@ -34,10 +34,10 @@ public class XmlToJsonStreamConverterTest {
         testConvert("vocabulary-items");
     }
     
-    private void testConvert(String fileName) throws XMLStreamException, JsonParseException, IOException {
-        System.out.println("-------------------------------------------");
-        System.out.println("Converting " + fileName);
-        System.out.println("-------------------------------------------");
+    private void testConvert(String fileName) throws XMLStreamException, IOException {
+        System.out.println("---------------------------------------------------------");
+        System.out.println("Converting XML to JSON: " + fileName);
+        System.out.println("---------------------------------------------------------");
 
         ClassLoader classLoader = getClass().getClassLoader();
         File xmlFile = new File(classLoader.getResource(FILE_PATH + fileName + ".xml").getFile());
diff --git a/services/common/src/test/resources/test-data/xmljson/boolean-json.json b/services/common/src/test/resources/test-data/xmljson/boolean-json.json
new file mode 100644 (file)
index 0000000..1ca1e6d
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "document": {
+    "@name": "examples",
+    "ns:examples_common": {
+      "@xmlns:ns": "http://collectionspace.org/services/example",
+      "boolTrue": true,
+      "boolFalse": false
+    }
+  }
+}
diff --git a/services/common/src/test/resources/test-data/xmljson/boolean-json.xml b/services/common/src/test/resources/test-data/xmljson/boolean-json.xml
new file mode 100644 (file)
index 0000000..bf156e1
--- /dev/null
@@ -0,0 +1,6 @@
+<document name="examples">
+    <ns:examples_common xmlns:ns="http://collectionspace.org/services/example">
+        <boolTrue>true</boolTrue>
+        <boolFalse>false</boolFalse>
+    </ns:examples_common>
+</document>
index 26dd95bc31094d0aefda5b17a1ea6a8cd0eabda2..c5824419c1d74f28c90d8759d5ddb036683fce0b 100644 (file)
     "ns2:collectionobjects_common": {
       "@xmlns:ns2": "http://collectionspace.org/services/collectionobject",
       "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
+      "objectProductionDateGroupList": null,
+      "fieldCollectionMethods": null,
+      "titleGroupList": null,
+      "assocEventPeoples": null,
+      "nonTextualInscriptionGroupList": null,
+      "assocActivityGroupList": null,
       "responsibleDepartments": {
         "responsibleDepartment": "new DEPT"
       },
+      "assocOrganizationGroupList": null,
+      "measuredPartGroupList": null,
+      "contentPositions": null,
+      "styles": null,
+      "assocObjectGroupList": null,
+      "assocPeopleGroupList": null,
+      "objectProductionOrganizationGroupList": null,
+      "ownershipDateGroupList": null,
+      "owners": null,
+      "objectProductionReasons": null,
+      "contentLanguages": null,
+      "otherNumberList": null,
+      "assocCulturalContextGroupList": null,
+      "objectProductionPersonGroupList": null,
       "objectNameList": {
         "objectNameGroup": {
-          "objectName": "new OBJNAME"
+          "objectNameCurrency": null,
+          "objectNameLanguage": null,
+          "objectName": "new OBJNAME",
+          "objectNameSystem": null,
+          "objectNameType": null,
+          "objectNameNote": null,
+          "objectNameLevel": null
         }
       },
+      "objectStatusList": null,
+      "assocDateGroupList": null,
+      "viewersReferences": null,
+      "assocEventPersons": null,
+      "assocPlaceGroupList": null,
       "comments": {
         "comment": "new COMMENTS"
       },
+      "textualInscriptionGroupList": null,
+      "briefDescriptions": null,
+      "contentOrganizations": null,
+      "objectProductionPlaceGroupList": null,
+      "contentActivities": null,
+      "contentPersons": null,
+      "contentScripts": null,
       "objectNumber": "2",
-      "distinguishingFeatures": "new DISTFEATURES"
+      "colors": null,
+      "ownersReferences": null,
+      "contentConcepts": null,
+      "fieldColEventNames": null,
+      "techniqueGroupList": null,
+      "assocEventPlaces": null,
+      "fieldCollectionDateGroup": {
+        "dateEarliestSingleQualifier": null,
+        "scalarValuesComputed": null,
+        "dateLatestDay": null,
+        "dateLatestYear": null,
+        "dateAssociation": null,
+        "dateEarliestSingleEra": null,
+        "dateDisplayDate": null,
+        "dateEarliestSingleCertainty": null,
+        "dateLatestEra": null,
+        "dateEarliestSingleQualifierValue": null,
+        "dateLatestCertainty": null,
+        "dateEarliestSingleYear": null,
+        "dateLatestQualifier": null,
+        "dateLatestQualifierValue": null,
+        "dateEarliestSingleQualifierUnit": null,
+        "datePeriod": null,
+        "dateEarliestScalarValue": null,
+        "dateLatestMonth": null,
+        "dateNote": null,
+        "dateLatestScalarValue": null,
+        "dateLatestQualifierUnit": null,
+        "dateEarliestSingleDay": null,
+        "dateEarliestSingleMonth": null
+      },
+      "contentPlaces": null,
+      "contentPeoples": null,
+      "objectComponentGroupList": null,
+      "technicalAttributeGroupList": null,
+      "referenceGroupList": null,
+      "fieldCollectionSources": null,
+      "forms": null,
+      "distinguishingFeatures": "new DISTFEATURES",
+      "assocConceptGroupList": null,
+      "contentDateGroup": {
+        "dateEarliestSingleQualifier": null,
+        "scalarValuesComputed": null,
+        "dateLatestDay": null,
+        "dateLatestYear": null,
+        "dateAssociation": null,
+        "dateEarliestSingleEra": null,
+        "dateDisplayDate": null,
+        "dateEarliestSingleCertainty": null,
+        "dateLatestEra": null,
+        "dateEarliestSingleQualifierValue": null,
+        "dateLatestCertainty": null,
+        "dateEarliestSingleYear": null,
+        "dateLatestQualifier": null,
+        "dateLatestQualifierValue": null,
+        "dateEarliestSingleQualifierUnit": null,
+        "datePeriod": null,
+        "dateEarliestScalarValue": null,
+        "dateLatestMonth": null,
+        "dateNote": null,
+        "dateLatestScalarValue": null,
+        "dateLatestQualifierUnit": null,
+        "dateEarliestSingleDay": null,
+        "dateEarliestSingleMonth": null
+      },
+      "usageGroupList": null,
+      "fieldCollectors": null,
+      "assocPersonGroupList": null,
+      "assocEventOrganizations": null,
+      "contentEventNameGroupList": null,
+      "contentOtherGroupList": null,
+      "materialGroupList": null,
+      "contentObjectGroupList": null,
+      "objectProductionPeopleGroupList": null
     },
     "ns2:account_permission": {
       "@xmlns:ns2": "http://collectionspace.org/services/authorization",
diff --git a/services/common/src/test/resources/test-data/xmljson/empty-json.json b/services/common/src/test/resources/test-data/xmljson/empty-json.json
new file mode 100644 (file)
index 0000000..9e26dfe
--- /dev/null
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/services/common/src/test/resources/test-data/xmljson/empty-json.xml b/services/common/src/test/resources/test-data/xmljson/empty-json.xml
new file mode 100644 (file)
index 0000000..5a887e8
--- /dev/null
@@ -0,0 +1 @@
+<!-- Intentionally blank. Attempting to convert empty-json.xml should throw an exception. -->
\ No newline at end of file
diff --git a/services/common/src/test/resources/test-data/xmljson/numeric-json.json b/services/common/src/test/resources/test-data/xmljson/numeric-json.json
new file mode 100644 (file)
index 0000000..1209215
--- /dev/null
@@ -0,0 +1,12 @@
+{
+  "document": {
+    "@name": "examples",
+    "ns:examples_common": {
+      "@xmlns:ns": "http://collectionspace.org/services/example",
+      "integer": 123,
+      "negInt": -8,
+      "float": 3.14159,
+      "negFloat": -341.24
+    }
+  }
+}
diff --git a/services/common/src/test/resources/test-data/xmljson/numeric-json.xml b/services/common/src/test/resources/test-data/xmljson/numeric-json.xml
new file mode 100644 (file)
index 0000000..6c4026e
--- /dev/null
@@ -0,0 +1,8 @@
+<document name="examples">
+    <ns:examples_common xmlns:ns="http://collectionspace.org/services/example">
+        <integer>123</integer>
+        <negInt>-8</negInt>
+        <float>3.14159</float>
+        <negFloat>-341.24</negFloat>
+    </ns:examples_common>
+</document>
index a008f4ddb8d9bbf14ee0500cb2696484d146433b..6e6b8d08121fa84a6abf08fa8d7007579e4debc6 100644 (file)
@@ -30,6 +30,7 @@
           "repeating scalar field value 4"
         ]
       },
+      "empty": null,
       "repeatstruct": {
         "innerstruct": [
           {
@@ -52,6 +53,9 @@
             }
           }
         ]
+      },
+      "emptytop": {
+        "emptynested": null
       }
     }
   }
index ff18c8aaf84d1e806e729159aa4c6099a3e27cb1..8a613c5d797668c6090e975c55032813337109e1 100644 (file)
@@ -10,6 +10,7 @@
     </ns:collectionspace_core>
     <ns:examples_common xmlns:ns="http://collectionspace.org/services/example">
         <scalar>This is a scalar field value</scalar>
+        <!-- A random comment -->
         <struct>
             <field1>A value in a structured field</field1>
             <field2>Another structured field value</field2>
index df07a56f859ecff218d6f991beec9b6846e845ae..22d226d01842816b6bbd2461032688235b0ff806 100644 (file)
                                        <artifactId>itext</artifactId>
                                        <groupId>com.lowagie</groupId>
                                </exclusion>
+                               <exclusion>
+                                       <!-- The services/common module requires a later version of jackson than the 2.1.4 used by jasper, so exclude it here. -->
+                                       <artifactId>jackson-core</artifactId>
+                                       <groupId>com.fasterxml.jackson.core</groupId>
+                               </exclusion>
                        </exclusions>
                </dependency>
                <!-- Jasper Reports fonts are still at 4.0.0, but are compatible with later