acec7b869c8ef4f9bd3e4a0e177dc20a3becf073
[jalview.git] / src / org / json / JSONML.java
1 package org.json;
2
3 /*
4 Copyright (c) 2008 JSON.org
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy
7 of this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights
9 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 copies of the Software, and to permit persons to whom the Software is
11 furnished to do so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 The Software shall be used for Good, not Evil.
17
18 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 SOFTWARE.
25 */
26
27 /**
28  * This provides static methods to convert an XML text into a JSONArray or
29  * JSONObject, and to covert a JSONArray or JSONObject into an XML text using
30  * the JsonML transform.
31  *
32  * @author JSON.org
33  * @version 2016-01-30
34  */
35 public class JSONML {
36     /**
37      * Parse XML values and store them in a JSONArray.
38      * @param x       The XMLTokener containing the source string.
39      * @param arrayForm true if array form, false if object form.
40      * @param ja      The JSONArray that is containing the current tag or null
41      *     if we are at the outermost level.
42      * @param keepStrings       Don't type-convert text nodes and attribute values
43      * @return A JSONArray if the value is the outermost tag, otherwise null.
44      * @throws JSONException
45      */
46     private static Object parse(
47         XMLTokener x,
48         boolean    arrayForm,
49         JSONArray  ja,
50         boolean keepStrings
51     ) throws JSONException {
52         String     attribute;
53         char       c;
54         String     closeTag = null;
55         int        i;
56         JSONArray  newja = null;
57         JSONObject newjo = null;
58         Object     token;
59         String     tagName = null;
60
61 // Test for and skip past these forms:
62 //      <!-- ... -->
63 //      <![  ... ]]>
64 //      <!   ...   >
65 //      <?   ...  ?>
66
67         while (true) {
68             if (!x.more()) {
69                 throw x.syntaxError("Bad XML");
70             }
71             token = x.nextContent();
72             if (token == XML.LT) {
73                 token = x.nextToken();
74                 if (token instanceof Character) {
75                     if (token == XML.SLASH) {
76
77 // Close tag </
78
79                         token = x.nextToken();
80                         if (!(token instanceof String)) {
81                             throw new JSONException(
82                                     "Expected a closing name instead of '" +
83                                     token + "'.");
84                         }
85                         if (x.nextToken() != XML.GT) {
86                             throw x.syntaxError("Misshaped close tag");
87                         }
88                         return token;
89                     } else if (token == XML.BANG) {
90
91 // <!
92
93                         c = x.next();
94                         if (c == '-') {
95                             if (x.next() == '-') {
96                                 x.skipPast("-->");
97                             } else {
98                                 x.back();
99                             }
100                         } else if (c == '[') {
101                             token = x.nextToken();
102                             if (token.equals("CDATA") && x.next() == '[') {
103                                 if (ja != null) {
104                                     ja.put(x.nextCDATA());
105                                 }
106                             } else {
107                                 throw x.syntaxError("Expected 'CDATA['");
108                             }
109                         } else {
110                             i = 1;
111                             do {
112                                 token = x.nextMeta();
113                                 if (token == null) {
114                                     throw x.syntaxError("Missing '>' after '<!'.");
115                                 } else if (token == XML.LT) {
116                                     i += 1;
117                                 } else if (token == XML.GT) {
118                                     i -= 1;
119                                 }
120                             } while (i > 0);
121                         }
122                     } else if (token == XML.QUEST) {
123
124 // <?
125
126                         x.skipPast("?>");
127                     } else {
128                         throw x.syntaxError("Misshaped tag");
129                     }
130
131 // Open tag <
132
133                 } else {
134                     if (!(token instanceof String)) {
135                         throw x.syntaxError("Bad tagName '" + token + "'.");
136                     }
137                     tagName = (String)token;
138                     newja = new JSONArray();
139                     newjo = new JSONObject();
140                     if (arrayForm) {
141                         newja.put(tagName);
142                         if (ja != null) {
143                             ja.put(newja);
144                         }
145                     } else {
146                         newjo.put("tagName", tagName);
147                         if (ja != null) {
148                             ja.put(newjo);
149                         }
150                     }
151                     token = null;
152                     for (;;) {
153                         if (token == null) {
154                             token = x.nextToken();
155                         }
156                         if (token == null) {
157                             throw x.syntaxError("Misshaped tag");
158                         }
159                         if (!(token instanceof String)) {
160                             break;
161                         }
162
163 // attribute = value
164
165                         attribute = (String)token;
166                         if (!arrayForm && ("tagName".equals(attribute) || "childNode".equals(attribute))) {
167                             throw x.syntaxError("Reserved attribute.");
168                         }
169                         token = x.nextToken();
170                         if (token == XML.EQ) {
171                             token = x.nextToken();
172                             if (!(token instanceof String)) {
173                                 throw x.syntaxError("Missing value");
174                             }
175                             newjo.accumulate(attribute, keepStrings ? ((String)token) :XML.stringToValue((String)token));
176                             token = null;
177                         } else {
178                             newjo.accumulate(attribute, "");
179                         }
180                     }
181                     if (arrayForm && newjo.length() > 0) {
182                         newja.put(newjo);
183                     }
184
185 // Empty tag <.../>
186
187                     if (token == XML.SLASH) {
188                         if (x.nextToken() != XML.GT) {
189                             throw x.syntaxError("Misshaped tag");
190                         }
191                         if (ja == null) {
192                             if (arrayForm) {
193                                 return newja;
194                             }
195                             return newjo;
196                         }
197
198 // Content, between <...> and </...>
199
200                     } else {
201                         if (token != XML.GT) {
202                             throw x.syntaxError("Misshaped tag");
203                         }
204                         closeTag = (String)parse(x, arrayForm, newja, keepStrings);
205                         if (closeTag != null) {
206                             if (!closeTag.equals(tagName)) {
207                                 throw x.syntaxError("Mismatched '" + tagName +
208                                         "' and '" + closeTag + "'");
209                             }
210                             tagName = null;
211                             if (!arrayForm && newja.length() > 0) {
212                                 newjo.put("childNodes", newja);
213                             }
214                             if (ja == null) {
215                                 if (arrayForm) {
216                                     return newja;
217                                 }
218                                 return newjo;
219                             }
220                         }
221                     }
222                 }
223             } else {
224                 if (ja != null) {
225                     ja.put(token instanceof String
226                         ? keepStrings ? XML.unescape((String)token) :XML.stringToValue((String)token)
227                         : token);
228                 }
229             }
230         }
231     }
232
233
234     /**
235      * Convert a well-formed (but not necessarily valid) XML string into a
236      * JSONArray using the JsonML transform. Each XML tag is represented as
237      * a JSONArray in which the first element is the tag name. If the tag has
238      * attributes, then the second element will be JSONObject containing the
239      * name/value pairs. If the tag contains children, then strings and
240      * JSONArrays will represent the child tags.
241      * Comments, prologs, DTDs, and <code>&lt;[ [ ]]></code> are ignored.
242      * @param string The source string.
243      * @return A JSONArray containing the structured data from the XML string.
244      * @throws JSONException Thrown on error converting to a JSONArray
245      */
246     public static JSONArray toJSONArray(String string) throws JSONException {
247         return (JSONArray)parse(new XMLTokener(string), true, null, false);
248     }
249
250
251     /**
252      * Convert a well-formed (but not necessarily valid) XML string into a
253      * JSONArray using the JsonML transform. Each XML tag is represented as
254      * a JSONArray in which the first element is the tag name. If the tag has
255      * attributes, then the second element will be JSONObject containing the
256      * name/value pairs. If the tag contains children, then strings and
257      * JSONArrays will represent the child tags.
258      * As opposed to toJSONArray this method does not attempt to convert 
259      * any text node or attribute value to any type 
260      * but just leaves it as a string.
261      * Comments, prologs, DTDs, and <code>&lt;[ [ ]]></code> are ignored.
262      * @param string The source string.
263      * @param keepStrings If true, then values will not be coerced into boolean
264      *  or numeric values and will instead be left as strings
265      * @return A JSONArray containing the structured data from the XML string.
266      * @throws JSONException Thrown on error converting to a JSONArray
267      */
268     public static JSONArray toJSONArray(String string, boolean keepStrings) throws JSONException {
269         return (JSONArray)parse(new XMLTokener(string), true, null, keepStrings);
270     }
271
272
273     /**
274      * Convert a well-formed (but not necessarily valid) XML string into a
275      * JSONArray using the JsonML transform. Each XML tag is represented as
276      * a JSONArray in which the first element is the tag name. If the tag has
277      * attributes, then the second element will be JSONObject containing the
278      * name/value pairs. If the tag contains children, then strings and
279      * JSONArrays will represent the child content and tags.
280      * As opposed to toJSONArray this method does not attempt to convert 
281      * any text node or attribute value to any type 
282      * but just leaves it as a string.
283      * Comments, prologs, DTDs, and <code>&lt;[ [ ]]></code> are ignored.
284      * @param x An XMLTokener.
285      * @param keepStrings If true, then values will not be coerced into boolean
286      *  or numeric values and will instead be left as strings
287      * @return A JSONArray containing the structured data from the XML string.
288      * @throws JSONException Thrown on error converting to a JSONArray
289      */
290     public static JSONArray toJSONArray(XMLTokener x, boolean keepStrings) throws JSONException {
291         return (JSONArray)parse(x, true, null, keepStrings);
292     }
293
294
295     /**
296      * Convert a well-formed (but not necessarily valid) XML string into a
297      * JSONArray using the JsonML transform. Each XML tag is represented as
298      * a JSONArray in which the first element is the tag name. If the tag has
299      * attributes, then the second element will be JSONObject containing the
300      * name/value pairs. If the tag contains children, then strings and
301      * JSONArrays will represent the child content and tags.
302      * Comments, prologs, DTDs, and <code>&lt;[ [ ]]></code> are ignored.
303      * @param x An XMLTokener.
304      * @return A JSONArray containing the structured data from the XML string.
305      * @throws JSONException Thrown on error converting to a JSONArray
306      */
307     public static JSONArray toJSONArray(XMLTokener x) throws JSONException {
308         return (JSONArray)parse(x, true, null, false);
309     }
310
311
312     /**
313      * Convert a well-formed (but not necessarily valid) XML string into a
314      * JSONObject using the JsonML transform. Each XML tag is represented as
315      * a JSONObject with a "tagName" property. If the tag has attributes, then
316      * the attributes will be in the JSONObject as properties. If the tag
317      * contains children, the object will have a "childNodes" property which
318      * will be an array of strings and JsonML JSONObjects.
319
320      * Comments, prologs, DTDs, and <code>&lt;[ [ ]]></code> are ignored.
321      * @param string The XML source text.
322      * @return A JSONObject containing the structured data from the XML string.
323      * @throws JSONException Thrown on error converting to a JSONObject
324      */
325     public static JSONObject toJSONObject(String string) throws JSONException {
326         return (JSONObject)parse(new XMLTokener(string), false, null, false);
327     }
328     
329     
330     /**
331      * Convert a well-formed (but not necessarily valid) XML string into a
332      * JSONObject using the JsonML transform. Each XML tag is represented as
333      * a JSONObject with a "tagName" property. If the tag has attributes, then
334      * the attributes will be in the JSONObject as properties. If the tag
335      * contains children, the object will have a "childNodes" property which
336      * will be an array of strings and JsonML JSONObjects.
337
338      * Comments, prologs, DTDs, and <code>&lt;[ [ ]]></code> are ignored.
339      * @param string The XML source text.
340      * @param keepStrings If true, then values will not be coerced into boolean
341      *  or numeric values and will instead be left as strings
342      * @return A JSONObject containing the structured data from the XML string.
343      * @throws JSONException Thrown on error converting to a JSONObject
344      */
345     public static JSONObject toJSONObject(String string, boolean keepStrings) throws JSONException {
346         return (JSONObject)parse(new XMLTokener(string), false, null, keepStrings);
347     }
348
349     
350     /**
351      * Convert a well-formed (but not necessarily valid) XML string into a
352      * JSONObject using the JsonML transform. Each XML tag is represented as
353      * a JSONObject with a "tagName" property. If the tag has attributes, then
354      * the attributes will be in the JSONObject as properties. If the tag
355      * contains children, the object will have a "childNodes" property which
356      * will be an array of strings and JsonML JSONObjects.
357
358      * Comments, prologs, DTDs, and <code>&lt;[ [ ]]></code> are ignored.
359      * @param x An XMLTokener of the XML source text.
360      * @return A JSONObject containing the structured data from the XML string.
361      * @throws JSONException Thrown on error converting to a JSONObject
362      */
363     public static JSONObject toJSONObject(XMLTokener x) throws JSONException {
364            return (JSONObject)parse(x, false, null, false);
365     }
366
367
368     /**
369      * Convert a well-formed (but not necessarily valid) XML string into a
370      * JSONObject using the JsonML transform. Each XML tag is represented as
371      * a JSONObject with a "tagName" property. If the tag has attributes, then
372      * the attributes will be in the JSONObject as properties. If the tag
373      * contains children, the object will have a "childNodes" property which
374      * will be an array of strings and JsonML JSONObjects.
375
376      * Comments, prologs, DTDs, and <code>&lt;[ [ ]]></code> are ignored.
377      * @param x An XMLTokener of the XML source text.
378      * @param keepStrings If true, then values will not be coerced into boolean
379      *  or numeric values and will instead be left as strings
380      * @return A JSONObject containing the structured data from the XML string.
381      * @throws JSONException Thrown on error converting to a JSONObject
382      */
383     public static JSONObject toJSONObject(XMLTokener x, boolean keepStrings) throws JSONException {
384            return (JSONObject)parse(x, false, null, keepStrings);
385     }
386
387
388     /**
389      * Reverse the JSONML transformation, making an XML text from a JSONArray.
390      * @param ja A JSONArray.
391      * @return An XML string.
392      * @throws JSONException Thrown on error converting to a string
393      */
394     public static String toString(JSONArray ja) throws JSONException {
395         int                 i;
396         JSONObject          jo;
397         int                 length;
398         Object              object;
399         StringBuilder        sb = new StringBuilder();
400         String              tagName;
401
402 // Emit <tagName
403
404         tagName = ja.getString(0);
405         XML.noSpace(tagName);
406         tagName = XML.escape(tagName);
407         sb.append('<');
408         sb.append(tagName);
409
410         object = ja.opt(1);
411         if (object instanceof JSONObject) {
412             i = 2;
413             jo = (JSONObject)object;
414
415 // Emit the attributes
416
417             // Don't use the new entrySet API to maintain Android support
418             for (final String key : jo.keySet()) {
419                 final Object value = jo.opt(key);
420                 XML.noSpace(key);
421                 if (value != null) {
422                     sb.append(' ');
423                     sb.append(XML.escape(key));
424                     sb.append('=');
425                     sb.append('"');
426                     sb.append(XML.escape(value.toString()));
427                     sb.append('"');
428                 }
429             }
430         } else {
431             i = 1;
432         }
433
434 // Emit content in body
435
436         length = ja.length();
437         if (i >= length) {
438             sb.append('/');
439             sb.append('>');
440         } else {
441             sb.append('>');
442             do {
443                 object = ja.get(i);
444                 i += 1;
445                 if (object != null) {
446                     if (object instanceof String) {
447                         sb.append(XML.escape(object.toString()));
448                     } else if (object instanceof JSONObject) {
449                         sb.append(toString((JSONObject)object));
450                     } else if (object instanceof JSONArray) {
451                         sb.append(toString((JSONArray)object));
452                     } else {
453                         sb.append(object.toString());
454                     }
455                 }
456             } while (i < length);
457             sb.append('<');
458             sb.append('/');
459             sb.append(tagName);
460             sb.append('>');
461         }
462         return sb.toString();
463     }
464
465     /**
466      * Reverse the JSONML transformation, making an XML text from a JSONObject.
467      * The JSONObject must contain a "tagName" property. If it has children,
468      * then it must have a "childNodes" property containing an array of objects.
469      * The other properties are attributes with string values.
470      * @param jo A JSONObject.
471      * @return An XML string.
472      * @throws JSONException Thrown on error converting to a string
473      */
474     public static String toString(JSONObject jo) throws JSONException {
475         StringBuilder sb = new StringBuilder();
476         int                 i;
477         JSONArray           ja;
478         int                 length;
479         Object              object;
480         String              tagName;
481         Object              value;
482
483 //Emit <tagName
484
485         tagName = jo.optString("tagName");
486         if (tagName == null) {
487             return XML.escape(jo.toString());
488         }
489         XML.noSpace(tagName);
490         tagName = XML.escape(tagName);
491         sb.append('<');
492         sb.append(tagName);
493
494 //Emit the attributes
495
496         // Don't use the new entrySet API to maintain Android support
497         for (final String key : jo.keySet()) {
498             if (!"tagName".equals(key) && !"childNodes".equals(key)) {
499                 XML.noSpace(key);
500                 value = jo.opt(key);
501                 if (value != null) {
502                     sb.append(' ');
503                     sb.append(XML.escape(key));
504                     sb.append('=');
505                     sb.append('"');
506                     sb.append(XML.escape(value.toString()));
507                     sb.append('"');
508                 }
509             }
510         }
511
512 //Emit content in body
513
514         ja = jo.optJSONArray("childNodes");
515         if (ja == null) {
516             sb.append('/');
517             sb.append('>');
518         } else {
519             sb.append('>');
520             length = ja.length();
521             for (i = 0; i < length; i += 1) {
522                 object = ja.get(i);
523                 if (object != null) {
524                     if (object instanceof String) {
525                         sb.append(XML.escape(object.toString()));
526                     } else if (object instanceof JSONObject) {
527                         sb.append(toString((JSONObject)object));
528                     } else if (object instanceof JSONArray) {
529                         sb.append(toString((JSONArray)object));
530                     } else {
531                         sb.append(object.toString());
532                     }
533                 }
534             }
535             sb.append('<');
536             sb.append('/');
537             sb.append(tagName);
538             sb.append('>');
539         }
540         return sb.toString();
541     }
542 }