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 * <p>Converts a CSpace JSON payload to an XML payload.</p>
22 * <p>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.</p>
28 * The conversion is performed as follows:
30 * <li>JSON fields starting with "@xmlns:" are converted XML namespace declarations.</li>
31 * <li>JSON fields starting with "@" are converted to XML attributes.</li>
32 * <li>Other JSON fields are converted to identically-named XML elements.</li>
33 * <li>The contents of JSON objects are converted to XML child elements.</li>
34 * <li>The contents of JSON arrays are expanded into multiple XML elements, each
35 * named with the field name of the JSON array.</li>
39 * <p>This implementation is schema-unaware. It operates by examining only the input
40 * document, without utilizing any XML schema information.</p>
49 * "@name": "collectionobjects",
50 * "ns2:collectionobjects_common": {
51 * "@xmlns:ns2": "http://collectionspace.org/services/collectionobject",
52 * "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
53 * "objectNumber": "2016.1.1",
55 * "objectNameGroup": [
57 * "objectNameCurrency": null,
58 * "objectNameLanguage": null,
59 * "objectName": "Object name",
60 * "objectNameSystem": null,
61 * "objectNameType": null,
62 * "objectNameNote": null,
63 * "objectNameLevel": null
66 * "objectNameCurrency": null,
67 * "objectNameLanguage": null,
68 * "objectName": "Another name",
69 * "objectNameSystem": null,
70 * "objectNameType": null,
71 * "objectNameNote": null,
72 * "objectNameLevel": null
78 * "Some comment text",
91 * <document name="collectionobjects">
92 * <ns2:collectionobjects_common xmlns:ns2="http://collectionspace.org/services/collectionobject" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
93 * <objectNumber>2016.1.1</objectNumber>
94 * <objectNameList>
95 * <objectNameGroup>
96 * <objectNameCurrency/>
97 * <objectNameLanguage/>
98 * <objectName>Object name</objectName>
99 * <objectNameSystem/>
100 * <objectNameType/>
101 * <objectNameNote/>
102 * <objectNameLevel/>
103 * </objectNameGroup>
104 * <objectNameGroup>
105 * <objectNameCurrency/>
106 * <objectNameLanguage/>
107 * <objectName>Another name</objectName>
108 * <objectNameSystem/>
109 * <objectNameType/>
110 * <objectNameNote/>
111 * <objectNameLevel/>
112 * </objectNameGroup>
113 * </objectNameList>
115 * <comment>Some comment text</comment>
116 * <comment>Another comment</comment>
118 * </ns2:collectionobjects_common>
123 * <p>This implementation uses a streaming JSON parser and a streaming
124 * XML writer to do a direct stream-to-stream conversion, without
125 * building a complete in-memory representation of the document.</p>
127 public class JsonToXmlStreamConverter {
130 * The JSON parser used to parse the input stream.
132 protected JsonParser jsonParser;
135 * The XML writer used to write to the output stream.
137 protected XMLStreamWriter xmlWriter;
140 * A stack used to track the state of JSON parsing.
141 * JsonField instances are pushed onto the stack as fields
142 * are entered, and popped off as fields are exited.
143 * References to fields are not retained once they
144 * are popped from the stack, so there is never a full
145 * representation of the JSON document in memory.
147 protected Stack<JsonField> stack = new Stack<JsonField>();
150 * Creates an JsonToXmlStreamConverter that reads JSON from an input stream,
151 * and writes XML to an output stream.
153 * @param in the JSON input stream
154 * @param out the XML output stream
155 * @throws JsonParseException
156 * @throws IOException
157 * @throws XMLStreamException
159 public JsonToXmlStreamConverter(InputStream in, OutputStream out) throws JsonParseException, IOException, XMLStreamException {
160 JsonFactory jsonFactory = new JsonFactory();
161 XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance();
163 jsonParser = jsonFactory.createParser(in);
164 xmlWriter = xmlFactory.createXMLStreamWriter(out);
168 * Performs the conversion.
170 * @throws JsonParseException
171 * @throws IOException
172 * @throws XMLStreamException
174 public void convert() throws JsonParseException, IOException, XMLStreamException {
175 xmlWriter.writeStartDocument();
177 // Read tokens from the input stream, and dispatch to handlers.
178 // Handlers may write XML to the output stream.
180 while (jsonParser.nextToken() != null) {
181 JsonToken token = jsonParser.currentToken();
185 onFieldName(jsonParser.getText());
188 onScalar(jsonParser.getText());
211 case VALUE_NUMBER_INT:
212 onScalar(jsonParser.getValueAsString());
214 case VALUE_NUMBER_FLOAT:
215 onScalar(jsonParser.getValueAsString());
221 xmlWriter.writeEndDocument();
225 * Event handler executed when a field name is encountered
226 * in the input stream.
228 * @param name the field name
230 public void onFieldName(String name) {
231 // Push the field onto the stack.
233 stack.push(new JsonField(name));
237 * Event handler executed when a scalar field value is encountered
238 * in the input stream. Boolean, integer, float, and null values are
239 * converted to strings prior to being passed in.
241 * @param value the scalar value, as a string
242 * @throws XMLStreamException
243 * @throws IOException
245 public void onScalar(String value) throws XMLStreamException, IOException {
246 JsonField field = stack.peek();
247 String name = field.getName();
249 if (field.isScalar() && isXmlAttribute(name)) {
250 // We're in a scalar field whose name looks like an XML attribute
251 // or namespace declaration.
253 if (isXmlNamespace(name)) {
254 // It looks like a namespace declaration.
255 // Output an XML namespace declaration.
257 String prefix = jsonFieldNameToXmlNamespacePrefix(name);
258 String namespaceUri = value;
260 xmlWriter.writeNamespace(prefix, namespaceUri);
263 // It looks like an attribute.
264 // Output an XML attribute.
266 String localName = jsonFieldNameToXmlAttributeName(name);
268 xmlWriter.writeAttribute(localName, value);
272 // It doesn't look like an XML attribute or namespace declaration.
273 // Output an XML element with the same name as the field, whose
274 // contents are the value.
276 xmlWriter.writeStartElement(name);
277 xmlWriter.writeCharacters(value);
278 xmlWriter.writeEndElement();
281 if (!field.isArray()) {
282 // If the field we're in is not an array, we're done with it.
283 // Pop it off the stack.
285 // If it is an array, there may be more values to come. The
286 // field shouldn't be popped until the end of array is
294 * Event handler executed when an object start ({) is encountered
295 * in the input stream.
297 * @throws XMLStreamException
299 public void onStartObject() throws XMLStreamException {
300 if (stack.isEmpty()) {
301 // This is the root object. Do nothing.
306 JsonField field = stack.peek();
308 if (field.isArray()) {
309 // If we're in an array, an object should be expanded
310 // into a field with the same name as the array.
312 field = new JsonField(field.getName());
316 field.setType(JsonField.Type.OBJECT);
318 // Write an XML start tag to the output stream.
320 xmlWriter.writeStartElement(field.getName());
324 * Event handler executed when an object end (}) is encountered
325 * in the input stream.
327 * @throws XMLStreamException
329 public void onEndObject() throws XMLStreamException {
330 if (stack.isEmpty()) {
331 // This is the root object. Do nothing.
336 // Pop the current field off the stack.
340 // Write an XML end tag to the output stream.
342 xmlWriter.writeEndElement();
346 * Event handler executed when an array start ([) is encountered
347 * in the input stream.
349 * @throws IOException
350 * @throws XMLStreamException
352 public void onStartArray() throws IOException, XMLStreamException {
353 // Set the current field type to array.
355 JsonField field = stack.peek();
357 field.setType(JsonField.Type.ARRAY);
361 * Event handler executed when an array end (]) is encountered
362 * in the input stream.
364 * @throws XMLStreamException
366 public void onEndArray() throws XMLStreamException {
367 // Pop the current field off the stack.