1 package org.collectionspace.services.common.xmljson;
3 import static org.collectionspace.services.common.xmljson.ConversionUtils.*;
5 import java.io.IOException;
6 import java.io.InputStream;
7 import java.io.OutputStream;
8 import java.util.Stack;
10 import javax.xml.stream.XMLOutputFactory;
11 import javax.xml.stream.XMLStreamException;
12 import javax.xml.stream.XMLStreamWriter;
14 import com.fasterxml.jackson.core.JsonFactory;
15 import com.fasterxml.jackson.core.JsonParseException;
16 import com.fasterxml.jackson.core.JsonParser;
17 import com.fasterxml.jackson.core.JsonToken;
20 * Converts a CSpace JSON payload to an XML payload.
22 * This class is not intended to serve as a general purpose JSON to XML
23 * translator. It is instead a lightweight processor tuned for the kinds
24 * of JSON generated by CSpace, and the particular transformations needed
25 * to generate XML for CSpace.
27 * The conversion is performed as follows:
29 * <li>JSON fields starting with "@xmlns:" are converted XML namespace declarations.</li>
30 * <li>JSON fields starting with "@" are converted to XML attributes.</li>
31 * <li>Other JSON fields are converted to identically-named XML elements.</li>
32 * <li>The contents of JSON objects are converted to XML child elements.</li>
33 * <li>The contents of JSON arrays are expanded into multiple XML elements, each
34 * named with the field name of the JSON array.</li>
37 * This implementation is schema-unaware. It operates by examining only the input
38 * document, without utilizing any XML schema information.
46 * "@name": "collectionobjects",
47 * "ns2:collectionobjects_common": {
48 * "@xmlns:ns2": "http://collectionspace.org/services/collectionobject",
49 * "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
50 * "objectNumber": "2016.1.1",
52 * "objectNameGroup": [
54 * "objectNameCurrency": null,
55 * "objectNameLanguage": null,
56 * "objectName": "Object name",
57 * "objectNameSystem": null,
58 * "objectNameType": null,
59 * "objectNameNote": null,
60 * "objectNameLevel": null
63 * "objectNameCurrency": null,
64 * "objectNameLanguage": null,
65 * "objectName": "Another name",
66 * "objectNameSystem": null,
67 * "objectNameType": null,
68 * "objectNameNote": null,
69 * "objectNameLevel": null
75 * "Some comment text",
86 * <document name="collectionobjects">
87 * <ns2:collectionobjects_common xmlns:ns2="http://collectionspace.org/services/collectionobject" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
88 * <objectNumber>2016.1.1</objectNumber>
91 * <objectNameCurrency/>
92 * <objectNameLanguage/>
93 * <objectName>Object name</objectName>
100 * <objectNameCurrency/>
101 * <objectNameLanguage/>
102 * <objectName>Another name</objectName>
103 * <objectNameSystem/>
110 * <comment>Some comment text</comment>
111 * <comment>Another comment</comment>
113 * </ns2:collectionobjects_common>
117 * This implementation uses a streaming JSON parser and a streaming
118 * XML writer to do a direct stream-to-stream conversion, without
119 * building a complete in-memory representation of the document.
121 public class JsonToXmlStreamConverter {
124 * The JSON parser used to parse the input stream.
126 protected JsonParser jsonParser;
129 * The XML writer used to write to the output stream.
131 protected XMLStreamWriter xmlWriter;
134 * A stack used to track the state of JSON parsing.
135 * JsonField instances are pushed onto the stack as fields
136 * are entered, and popped off as fields are exited.
137 * References to fields are not retained once they
138 * are popped from the stack, so there is never a full
139 * representation of the JSON document in memory.
141 protected Stack<JsonField> stack = new Stack<JsonField>();
144 * Creates an JsonToXmlStreamConverter that reads JSON from an input stream,
145 * and writes XML to an output stream.
147 * @param in the JSON input stream
148 * @param out the XML output stream
149 * @throws JsonParseException
150 * @throws IOException
151 * @throws XMLStreamException
153 public JsonToXmlStreamConverter(InputStream in, OutputStream out) throws JsonParseException, IOException, XMLStreamException {
154 JsonFactory jsonFactory = new JsonFactory();
155 XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance();
157 jsonParser = jsonFactory.createParser(in);
158 xmlWriter = xmlFactory.createXMLStreamWriter(out);
162 * Performs the conversion.
164 * @throws JsonParseException
165 * @throws IOException
166 * @throws XMLStreamException
168 public void convert() throws JsonParseException, IOException, XMLStreamException {
169 xmlWriter.writeStartDocument();
171 // Read tokens from the input stream, and dispatch to handlers.
172 // Handlers may write XML to the output stream.
174 while (jsonParser.nextToken() != null) {
175 JsonToken token = jsonParser.currentToken();
179 onFieldName(jsonParser.getText());
182 onScalar(jsonParser.getText());
205 case VALUE_NUMBER_INT:
206 onScalar(jsonParser.getValueAsString());
208 case VALUE_NUMBER_FLOAT:
209 onScalar(jsonParser.getValueAsString());
215 xmlWriter.writeEndDocument();
219 * Event handler executed when a field name is encountered
220 * in the input stream.
222 * @param name the field name
224 public void onFieldName(String name) {
225 // Push the field onto the stack.
227 stack.push(new JsonField(name));
231 * Event handler executed when a scalar field value is encountered
232 * in the input stream. Boolean, integer, float, and null values are
233 * converted to strings prior to being passed in.
235 * @param value the scalar value, as a string
236 * @throws XMLStreamException
237 * @throws IOException
239 public void onScalar(String value) throws XMLStreamException, IOException {
240 JsonField field = stack.peek();
241 String name = field.getName();
243 if (field.isScalar() && isXmlAttribute(name)) {
244 // We're in a scalar field whose name looks like an XML attribute
245 // or namespace declaration.
247 if (isXmlNamespace(name)) {
248 // It looks like a namespace declaration.
249 // Output an XML namespace declaration.
251 String prefix = jsonFieldNameToXmlNamespacePrefix(name);
252 String namespaceUri = value;
254 xmlWriter.writeNamespace(prefix, namespaceUri);
257 // It looks like an attribute.
258 // Output an XML attribute.
260 String localName = jsonFieldNameToXmlAttributeName(name);
262 xmlWriter.writeAttribute(localName, value);
266 // It doesn't look like an XML attribute or namespace declaration.
267 // Output an XML element with the same name as the field, whose
268 // contents are the value.
270 xmlWriter.writeStartElement(name);
271 xmlWriter.writeCharacters(value);
272 xmlWriter.writeEndElement();
275 if (!field.isArray()) {
276 // If the field we're in is not an array, we're done with it.
277 // Pop it off the stack.
279 // If it is an array, there may be more values to come. The
280 // field shouldn't be popped until the end of array is
288 * Event handler executed when an object start ({) is encountered
289 * in the input stream.
291 * @throws XMLStreamException
293 public void onStartObject() throws XMLStreamException {
294 if (stack.isEmpty()) {
295 // This is the root object. Do nothing.
300 JsonField field = stack.peek();
302 if (field.isArray()) {
303 // If we're in an array, an object should be expanded
304 // into a field with the same name as the array.
306 field = new JsonField(field.getName());
310 field.setType(JsonField.Type.OBJECT);
312 // Write an XML start tag to the output stream.
314 xmlWriter.writeStartElement(field.getName());
318 * Event handler executed when an object end (}) is encountered
319 * in the input stream.
321 * @throws XMLStreamException
323 public void onEndObject() throws XMLStreamException {
324 if (stack.isEmpty()) {
325 // This is the root object. Do nothing.
330 // Pop the current field off the stack.
334 // Write an XML end tag to the output stream.
336 xmlWriter.writeEndElement();
340 * Event handler executed when an array start ([) is encountered
341 * in the input stream.
343 * @throws IOException
344 * @throws XMLStreamException
346 public void onStartArray() throws IOException, XMLStreamException {
347 // Set the current field type to array.
349 JsonField field = stack.peek();
351 field.setType(JsonField.Type.ARRAY);
355 * Event handler executed when an array end (]) is encountered
356 * in the input stream.
358 * @throws XMLStreamException
360 public void onEndArray() throws XMLStreamException {
361 // Pop the current field off the stack.