]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
0a4c924383e20eb05dcff4843a9d2405dd1b4ab3
[tmp/jakarta-migration.git] /
1 package org.collectionspace.services.common.xmljson;
2
3 import static org.collectionspace.services.common.xmljson.ConversionUtils.*;
4
5 import java.io.IOException;
6 import java.io.InputStream;
7 import java.io.OutputStream;
8 import java.util.Stack;
9
10 import javax.xml.stream.XMLOutputFactory;
11 import javax.xml.stream.XMLStreamException;
12 import javax.xml.stream.XMLStreamWriter;
13
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;
18
19 /**
20  * <p>Converts a CSpace JSON payload to an XML payload.</p>
21  *
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>
26  * 
27  * <p>
28  * The conversion is performed as follows:
29  * <ul>
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>
36  * </ul>
37  * </p>
38  *
39  * <p>This implementation is schema-unaware. It operates by examining only the input
40  * document, without utilizing any XML schema information.</p>
41  * 
42  * <p>Example:</p>
43  *
44  * <p>
45  * JSON
46  * <pre>
47  * {
48  *   "document": {
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",
54  *       "objectNameList": {
55  *         "objectNameGroup": [
56  *           {
57  *             "objectNameCurrency": null,
58  *             "objectNameLanguage": null,
59  *             "objectName": "Object name",
60  *             "objectNameSystem": null,
61  *             "objectNameType": null,
62  *             "objectNameNote": null,
63  *             "objectNameLevel": null
64  *           },
65  *           {
66  *             "objectNameCurrency": null,
67  *             "objectNameLanguage": null,
68  *             "objectName": "Another name",
69  *             "objectNameSystem": null,
70  *             "objectNameType": null,
71  *             "objectNameNote": null,
72  *             "objectNameLevel": null
73  *           }
74  *         ]
75  *       },
76  *       "comments": {
77  *         "comment": [
78  *           "Some comment text",
79  *           "Another comment"
80  *         ]
81  *       }
82  *     }
83  *   }
84  * }
85  * </pre>
86  * </p>
87  * 
88  * <p>
89  * XML
90  * <pre>
91  * &lt;document name="collectionobjects"&gt;
92  *   &lt;ns2:collectionobjects_common xmlns:ns2="http://collectionspace.org/services/collectionobject" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"&gt;
93  *     &lt;objectNumber&gt;2016.1.1&lt;/objectNumber&gt;
94  *     &lt;objectNameList&gt;
95  *       &lt;objectNameGroup&gt;
96  *         &lt;objectNameCurrency/&gt;
97  *         &lt;objectNameLanguage/&gt;
98  *         &lt;objectName&gt;Object name&lt;/objectName&gt;
99  *         &lt;objectNameSystem/&gt;
100  *         &lt;objectNameType/&gt;
101  *         &lt;objectNameNote/&gt;
102  *         &lt;objectNameLevel/&gt;
103  *       &lt;/objectNameGroup&gt;
104  *       &lt;objectNameGroup&gt;
105  *         &lt;objectNameCurrency/&gt;
106  *         &lt;objectNameLanguage/&gt;
107  *         &lt;objectName&gt;Another name&lt;/objectName&gt;
108  *         &lt;objectNameSystem/&gt;
109  *         &lt;objectNameType/&gt;
110  *         &lt;objectNameNote/&gt;
111  *         &lt;objectNameLevel/&gt;
112  *       &lt;/objectNameGroup&gt;
113  *     &lt;/objectNameList&gt;
114  *     &lt;comments&gt;
115  *       &lt;comment&gt;Some comment text&lt;/comment&gt;
116  *       &lt;comment&gt;Another comment&lt;/comment&gt;
117  *     &lt;/comments&gt;
118  *   &lt;/ns2:collectionobjects_common&gt;
119  * &lt;/document&gt;
120  * </pre>
121  * </p>
122  * 
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>
126  */
127 public class JsonToXmlStreamConverter {
128     
129     /**
130      * The JSON parser used to parse the input stream.
131      */
132     protected JsonParser jsonParser;
133     
134     /**
135      * The XML writer used to write to the output stream.
136      */
137     protected XMLStreamWriter xmlWriter;
138     
139     /**
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.
146      */
147     protected Stack<JsonField> stack = new Stack<JsonField>();
148     
149     /**
150      * Creates an JsonToXmlStreamConverter that reads JSON from an input stream,
151      * and writes XML to an output stream.
152      * 
153      * @param in the JSON input stream
154      * @param out the XML output stream
155      * @throws JsonParseException
156      * @throws IOException
157      * @throws XMLStreamException
158      */
159     public JsonToXmlStreamConverter(InputStream in, OutputStream out) throws JsonParseException, IOException, XMLStreamException {
160         JsonFactory jsonFactory = new JsonFactory();
161         XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance();
162
163         jsonParser = jsonFactory.createParser(in);
164         xmlWriter = xmlFactory.createXMLStreamWriter(out);
165     }
166     
167     /**
168      * Performs the conversion.
169      * 
170      * @throws JsonParseException
171      * @throws IOException
172      * @throws XMLStreamException
173      */
174     public void convert() throws JsonParseException, IOException, XMLStreamException {
175         xmlWriter.writeStartDocument();
176         
177         // Read tokens from the input stream, and dispatch to handlers.
178         // Handlers may write XML to the output stream.
179         
180         while (jsonParser.nextToken() != null) {
181             JsonToken token = jsonParser.currentToken();
182             
183             switch(token) {
184                 case FIELD_NAME:
185                     onFieldName(jsonParser.getText());
186                     break;
187                 case VALUE_STRING:
188                     onScalar(jsonParser.getText());
189                     break;
190                 case VALUE_NULL:
191                     onScalar("");
192                     break;
193                 case START_OBJECT:
194                     onStartObject();
195                     break;
196                 case END_OBJECT:
197                     onEndObject();
198                     break;
199                 case START_ARRAY:
200                     onStartArray();
201                     break;
202                 case END_ARRAY:
203                     onEndArray();
204                     break;
205                 case VALUE_TRUE:
206                     onScalar("true");
207                     break;
208                 case VALUE_FALSE:
209                     onScalar("false");
210                     break;
211                 case VALUE_NUMBER_INT:
212                     onScalar(jsonParser.getValueAsString());
213                     break;
214                 case VALUE_NUMBER_FLOAT:
215                     onScalar(jsonParser.getValueAsString());
216                     break;
217                 default:
218             }
219         }
220         
221         xmlWriter.writeEndDocument();
222     }
223
224     /**
225      * Event handler executed when a field name is encountered
226      * in the input stream.
227      * 
228      * @param name the field name
229      */
230     public void onFieldName(String name) {
231         // Push the field onto the stack.
232         
233         stack.push(new JsonField(name));
234     }
235     
236     /**
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.
240      * 
241      * @param value the scalar value, as a string
242      * @throws XMLStreamException
243      * @throws IOException
244      */
245     public void onScalar(String value) throws XMLStreamException, IOException {
246         JsonField field = stack.peek();
247         String name = field.getName();
248         
249         if (field.isScalar() && isXmlAttribute(name)) {
250             // We're in a scalar field whose name looks like an XML attribute
251             // or namespace declaration.
252             
253             if (isXmlNamespace(name)) {
254                 // It looks like a namespace declaration.
255                 // Output an XML namespace declaration.
256                 
257                 String prefix = jsonFieldNameToXmlNamespacePrefix(name);
258                 String namespaceUri = value;
259                 
260                 xmlWriter.writeNamespace(prefix, namespaceUri);
261             }
262             else {
263                 // It looks like an attribute.
264                 // Output an XML attribute.
265                 
266                 String localName = jsonFieldNameToXmlAttributeName(name);
267                 
268                 xmlWriter.writeAttribute(localName, value);
269             }
270         }
271         else {
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.
275             
276             xmlWriter.writeStartElement(name);
277             xmlWriter.writeCharacters(value);
278             xmlWriter.writeEndElement();
279         }
280
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.
284             
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
287             // found.
288             
289             stack.pop();
290         }
291     }
292     
293     /**
294      * Event handler executed when an object start ({) is encountered
295      * in the input stream.
296      * 
297      * @throws XMLStreamException
298      */
299     public void onStartObject() throws XMLStreamException {
300         if (stack.isEmpty()) {
301             // This is the root object. Do nothing.
302             
303             return;
304         }
305         
306         JsonField field = stack.peek();
307         
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.
311             
312             field = new JsonField(field.getName());
313             stack.push(field);
314         }
315         
316         field.setType(JsonField.Type.OBJECT);
317         
318         // Write an XML start tag to the output stream.
319         
320         xmlWriter.writeStartElement(field.getName());
321     }
322     
323     /**
324      * Event handler executed when an object end (}) is encountered
325      * in the input stream.
326      *
327      * @throws XMLStreamException
328      */
329     public void onEndObject() throws XMLStreamException {
330         if (stack.isEmpty()) {
331             // This is the root object. Do nothing.
332             
333             return;
334         }
335
336         // Pop the current field off the stack.
337         
338         stack.pop();
339         
340         // Write an XML end tag to the output stream.
341         
342         xmlWriter.writeEndElement();
343     }
344     
345     /**
346      * Event handler executed when an array start ([) is encountered
347      * in the input stream.
348      *
349      * @throws IOException
350      * @throws XMLStreamException
351      */
352     public void onStartArray() throws IOException, XMLStreamException {
353         // Set the current field type to array.
354         
355         JsonField field = stack.peek();
356         
357         field.setType(JsonField.Type.ARRAY);
358     }
359     
360     /**
361      * Event handler executed when an array end (]) is encountered
362      * in the input stream.
363      *
364      * @throws XMLStreamException
365      */
366     public void onEndArray() throws XMLStreamException {
367        // Pop the current field off the stack.
368        
369        stack.pop();
370     }
371 }
372