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