JAL-3438 spotless for 2.11.2.0
[jalview.git] / src / org / json / XML.java
1 package org.json;
2
3 /*
4 Copyright (c) 2015 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 import java.io.Reader;
28 import java.io.StringReader;
29 import java.util.Iterator;
30
31 /**
32  * This provides static methods to convert an XML text into a JSONObject, and to
33  * covert a JSONObject into an XML text.
34  * 
35  * @author JSON.org
36  * @version 2016-08-10
37  */
38 @SuppressWarnings("boxing")
39 public class XML
40 {
41   /** The Character '&'. */
42   public static final Character AMP = '&';
43
44   /** The Character '''. */
45   public static final Character APOS = '\'';
46
47   /** The Character '!'. */
48   public static final Character BANG = '!';
49
50   /** The Character '='. */
51   public static final Character EQ = '=';
52
53   /** The Character '>'. */
54   public static final Character GT = '>';
55
56   /** The Character '<'. */
57   public static final Character LT = '<';
58
59   /** The Character '?'. */
60   public static final Character QUEST = '?';
61
62   /** The Character '"'. */
63   public static final Character QUOT = '"';
64
65   /** The Character '/'. */
66   public static final Character SLASH = '/';
67
68   /**
69    * Creates an iterator for navigating Code Points in a string instead of
70    * characters. Once Java7 support is dropped, this can be replaced with <code>
71    * string.codePoints()
72    * </code> which is available in Java8 and above.
73    * 
74    * @see <a href=
75    *      "http://stackoverflow.com/a/21791059/6030888">http://stackoverflow.com/a/21791059/6030888</a>
76    */
77   private static Iterable<Integer> codePointIterator(final String string)
78   {
79     return new Iterable<Integer>()
80     {
81       @Override
82       public Iterator<Integer> iterator()
83       {
84         return new Iterator<Integer>()
85         {
86           private int nextIndex = 0;
87
88           private int length = string.length();
89
90           @Override
91           public boolean hasNext()
92           {
93             return this.nextIndex < this.length;
94           }
95
96           @Override
97           public Integer next()
98           {
99             int result = string.codePointAt(this.nextIndex);
100             this.nextIndex += Character.charCount(result);
101             return result;
102           }
103
104           @Override
105           public void remove()
106           {
107             throw new UnsupportedOperationException();
108           }
109         };
110       }
111     };
112   }
113
114   /**
115    * Replace special characters with XML escapes:
116    * 
117    * <pre>
118    * &amp; <small>(ampersand)</small> is replaced by &amp;amp;
119    * &lt; <small>(less than)</small> is replaced by &amp;lt;
120    * &gt; <small>(greater than)</small> is replaced by &amp;gt;
121    * &quot; <small>(double quote)</small> is replaced by &amp;quot;
122    * &apos; <small>(single quote / apostrophe)</small> is replaced by &amp;apos;
123    * </pre>
124    * 
125    * @param string
126    *          The string to be escaped.
127    * @return The escaped string.
128    */
129   public static String escape(String string)
130   {
131     StringBuilder sb = new StringBuilder(string.length());
132     for (final int cp : codePointIterator(string))
133     {
134       switch (cp)
135       {
136       case '&':
137         sb.append("&amp;");
138         break;
139       case '<':
140         sb.append("&lt;");
141         break;
142       case '>':
143         sb.append("&gt;");
144         break;
145       case '"':
146         sb.append("&quot;");
147         break;
148       case '\'':
149         sb.append("&apos;");
150         break;
151       default:
152         if (mustEscape(cp))
153         {
154           sb.append("&#x");
155           sb.append(Integer.toHexString(cp));
156           sb.append(';');
157         }
158         else
159         {
160           sb.appendCodePoint(cp);
161         }
162       }
163     }
164     return sb.toString();
165   }
166
167   /**
168    * @param cp
169    *          code point to test
170    * @return true if the code point is not valid for an XML
171    */
172   private static boolean mustEscape(int cp)
173   {
174     /* Valid range from https://www.w3.org/TR/REC-xml/#charsets
175      * 
176      * #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] 
177      * 
178      * any Unicode character, excluding the surrogate blocks, FFFE, and FFFF. 
179      */
180     // isISOControl is true when (cp >= 0 && cp <= 0x1F) || (cp >= 0x7F && cp <=
181     // 0x9F)
182     // all ISO control characters are out of range except tabs and new lines
183     return (Character.isISOControl(cp) && cp != 0x9 && cp != 0xA
184             && cp != 0xD) || !(
185     // valid the range of acceptable characters that aren't control
186     (cp >= 0x20 && cp <= 0xD7FF) || (cp >= 0xE000 && cp <= 0xFFFD)
187             || (cp >= 0x10000 && cp <= 0x10FFFF));
188   }
189
190   /**
191    * Removes XML escapes from the string.
192    * 
193    * @param string
194    *          string to remove escapes from
195    * @return string with converted entities
196    */
197   public static String unescape(String string)
198   {
199     StringBuilder sb = new StringBuilder(string.length());
200     for (int i = 0, length = string.length(); i < length; i++)
201     {
202       char c = string.charAt(i);
203       if (c == '&')
204       {
205         final int semic = string.indexOf(';', i);
206         if (semic > i)
207         {
208           final String entity = string.substring(i + 1, semic);
209           sb.append(XMLTokener.unescapeEntity(entity));
210           // skip past the entity we just parsed.
211           i += entity.length() + 1;
212         }
213         else
214         {
215           // this shouldn't happen in most cases since the parser
216           // errors on unclosed entries.
217           sb.append(c);
218         }
219       }
220       else
221       {
222         // not part of an entity
223         sb.append(c);
224       }
225     }
226     return sb.toString();
227   }
228
229   /**
230    * Throw an exception if the string contains whitespace. Whitespace is not
231    * allowed in tagNames and attributes.
232    * 
233    * @param string
234    *          A string.
235    * @throws JSONException
236    *           Thrown if the string contains whitespace or is empty.
237    */
238   public static void noSpace(String string) throws JSONException
239   {
240     int i, length = string.length();
241     if (length == 0)
242     {
243       throw new JSONException("Empty string.");
244     }
245     for (i = 0; i < length; i += 1)
246     {
247       if (Character.isWhitespace(string.charAt(i)))
248       {
249         throw new JSONException(
250                 "'" + string + "' contains a space character.");
251       }
252     }
253   }
254
255   /**
256    * Scan the content following the named tag, attaching it to the context.
257    * 
258    * @param x
259    *          The XMLTokener containing the source string.
260    * @param context
261    *          The JSONObject that will include the new material.
262    * @param name
263    *          The tag name.
264    * @return true if the close tag is processed.
265    * @throws JSONException
266    */
267   private static boolean parse(XMLTokener x, JSONObject context,
268           String name, boolean keepStrings) throws JSONException
269   {
270     char c;
271     int i;
272     JSONObject jsonobject = null;
273     String string;
274     String tagName;
275     Object token;
276
277     // Test for and skip past these forms:
278     // <!-- ... -->
279     // <! ... >
280     // <![ ... ]]>
281     // <? ... ?>
282     // Report errors for these forms:
283     // <>
284     // <=
285     // <<
286
287     token = x.nextToken();
288
289     // <!
290
291     if (token == BANG)
292     {
293       c = x.next();
294       if (c == '-')
295       {
296         if (x.next() == '-')
297         {
298           x.skipPast("-->");
299           return false;
300         }
301         x.back();
302       }
303       else if (c == '[')
304       {
305         token = x.nextToken();
306         if ("CDATA".equals(token))
307         {
308           if (x.next() == '[')
309           {
310             string = x.nextCDATA();
311             if (string.length() > 0)
312             {
313               context.accumulate("content", string);
314             }
315             return false;
316           }
317         }
318         throw x.syntaxError("Expected 'CDATA['");
319       }
320       i = 1;
321       do
322       {
323         token = x.nextMeta();
324         if (token == null)
325         {
326           throw x.syntaxError("Missing '>' after '<!'.");
327         }
328         else if (token == LT)
329         {
330           i += 1;
331         }
332         else if (token == GT)
333         {
334           i -= 1;
335         }
336       } while (i > 0);
337       return false;
338     }
339     else if (token == QUEST)
340     {
341
342       // <?
343       x.skipPast("?>");
344       return false;
345     }
346     else if (token == SLASH)
347     {
348
349       // Close tag </
350
351       token = x.nextToken();
352       if (name == null)
353       {
354         throw x.syntaxError("Mismatched close tag " + token);
355       }
356       if (!token.equals(name))
357       {
358         throw x.syntaxError("Mismatched " + name + " and " + token);
359       }
360       if (x.nextToken() != GT)
361       {
362         throw x.syntaxError("Misshaped close tag");
363       }
364       return true;
365
366     }
367     else if (token instanceof Character)
368     {
369       throw x.syntaxError("Misshaped tag");
370
371       // Open tag <
372
373     }
374     else
375     {
376       tagName = (String) token;
377       token = null;
378       jsonobject = new JSONObject();
379       for (;;)
380       {
381         if (token == null)
382         {
383           token = x.nextToken();
384         }
385         // attribute = value
386         if (token instanceof String)
387         {
388           string = (String) token;
389           token = x.nextToken();
390           if (token == EQ)
391           {
392             token = x.nextToken();
393             if (!(token instanceof String))
394             {
395               throw x.syntaxError("Missing value");
396             }
397             jsonobject.accumulate(string, keepStrings ? ((String) token)
398                     : stringToValue((String) token));
399             token = null;
400           }
401           else
402           {
403             jsonobject.accumulate(string, "");
404           }
405
406         }
407         else if (token == SLASH)
408         {
409           // Empty tag <.../>
410           if (x.nextToken() != GT)
411           {
412             throw x.syntaxError("Misshaped tag");
413           }
414           if (jsonobject.length() > 0)
415           {
416             context.accumulate(tagName, jsonobject);
417           }
418           else
419           {
420             context.accumulate(tagName, "");
421           }
422           return false;
423
424         }
425         else if (token == GT)
426         {
427           // Content, between <...> and </...>
428           for (;;)
429           {
430             token = x.nextContent();
431             if (token == null)
432             {
433               if (tagName != null)
434               {
435                 throw x.syntaxError("Unclosed tag " + tagName);
436               }
437               return false;
438             }
439             else if (token instanceof String)
440             {
441               string = (String) token;
442               if (string.length() > 0)
443               {
444                 jsonobject.accumulate("content",
445                         keepStrings ? string : stringToValue(string));
446               }
447
448             }
449             else if (token == LT)
450             {
451               // Nested element
452               if (parse(x, jsonobject, tagName, keepStrings))
453               {
454                 if (jsonobject.length() == 0)
455                 {
456                   context.accumulate(tagName, "");
457                 }
458                 else if (jsonobject.length() == 1
459                         && jsonobject.opt("content") != null)
460                 {
461                   context.accumulate(tagName, jsonobject.opt("content"));
462                 }
463                 else
464                 {
465                   context.accumulate(tagName, jsonobject);
466                 }
467                 return false;
468               }
469             }
470           }
471         }
472         else
473         {
474           throw x.syntaxError("Misshaped tag");
475         }
476       }
477     }
478   }
479
480   /**
481    * This method is the same as {@link JSONObject#stringToValue(String)}.
482    * 
483    * @param string
484    *          String to convert
485    * @return JSON value of this string or the string
486    */
487   // To maintain compatibility with the Android API, this method is a direct
488   // copy of
489   // the one in JSONObject. Changes made here should be reflected there.
490   public static Object stringToValue(String string)
491   {
492     if (string.equals(""))
493     {
494       return string;
495     }
496     if (string.equalsIgnoreCase("true"))
497     {
498       return Boolean.TRUE;
499     }
500     if (string.equalsIgnoreCase("false"))
501     {
502       return Boolean.FALSE;
503     }
504     if (string.equalsIgnoreCase("null"))
505     {
506       return JSONObject.NULL;
507     }
508
509     /*
510      * If it might be a number, try converting it. If a number cannot be
511      * produced, then the value will just be a string.
512      */
513
514     char initial = string.charAt(0);
515     if ((initial >= '0' && initial <= '9') || initial == '-')
516     {
517       try
518       {
519         // if we want full Big Number support this block can be replaced with:
520         // return stringToNumber(string);
521         if (string.indexOf('.') > -1 || string.indexOf('e') > -1
522                 || string.indexOf('E') > -1 || "-0".equals(string))
523         {
524           Double d = Double.valueOf(string);
525           if (!d.isInfinite() && !d.isNaN())
526           {
527             return d;
528           }
529         }
530         else
531         {
532           Long myLong = Long.valueOf(string);
533           if (string.equals(myLong.toString()))
534           {
535             if (myLong.longValue() == myLong.intValue())
536             {
537               return Integer.valueOf(myLong.intValue());
538             }
539             return myLong;
540           }
541         }
542       } catch (Exception ignore)
543       {
544       }
545     }
546     return string;
547   }
548
549   /**
550    * Convert a well-formed (but not necessarily valid) XML string into a
551    * JSONObject. Some information may be lost in this transformation because
552    * JSON is a data format and XML is a document format. XML uses elements,
553    * attributes, and content text, while JSON uses unordered collections of
554    * name/value pairs and arrays of values. JSON does not does not like to
555    * distinguish between elements and attributes. Sequences of similar elements
556    * are represented as JSONArrays. Content text may be placed in a "content"
557    * member. Comments, prologs, DTDs, and <code>&lt;[ [ ]]></code> are ignored.
558    * 
559    * @param string
560    *          The source string.
561    * @return A JSONObject containing the structured data from the XML string.
562    * @throws JSONException
563    *           Thrown if there is an errors while parsing the string
564    */
565   public static JSONObject toJSONObject(String string) throws JSONException
566   {
567     return toJSONObject(string, false);
568   }
569
570   /**
571    * Convert a well-formed (but not necessarily valid) XML into a JSONObject.
572    * Some information may be lost in this transformation because JSON is a data
573    * format and XML is a document format. XML uses elements, attributes, and
574    * content text, while JSON uses unordered collections of name/value pairs and
575    * arrays of values. JSON does not does not like to distinguish between
576    * elements and attributes. Sequences of similar elements are represented as
577    * JSONArrays. Content text may be placed in a "content" member. Comments,
578    * prologs, DTDs, and <code>&lt;[ [ ]]></code> are ignored.
579    *
580    * @param reader
581    *          The XML source reader.
582    * @return A JSONObject containing the structured data from the XML string.
583    * @throws JSONException
584    *           Thrown if there is an errors while parsing the string
585    */
586   public static JSONObject toJSONObject(Reader reader) throws JSONException
587   {
588     return toJSONObject(reader, false);
589   }
590
591   /**
592    * Convert a well-formed (but not necessarily valid) XML into a JSONObject.
593    * Some information may be lost in this transformation because JSON is a data
594    * format and XML is a document format. XML uses elements, attributes, and
595    * content text, while JSON uses unordered collections of name/value pairs and
596    * arrays of values. JSON does not does not like to distinguish between
597    * elements and attributes. Sequences of similar elements are represented as
598    * JSONArrays. Content text may be placed in a "content" member. Comments,
599    * prologs, DTDs, and <code>&lt;[ [ ]]></code> are ignored.
600    *
601    * All values are converted as strings, for 1, 01, 29.0 will not be coerced to
602    * numbers but will instead be the exact value as seen in the XML document.
603    *
604    * @param reader
605    *          The XML source reader.
606    * @param keepStrings
607    *          If true, then values will not be coerced into boolean or numeric
608    *          values and will instead be left as strings
609    * @return A JSONObject containing the structured data from the XML string.
610    * @throws JSONException
611    *           Thrown if there is an errors while parsing the string
612    */
613   public static JSONObject toJSONObject(Reader reader, boolean keepStrings)
614           throws JSONException
615   {
616     JSONObject jo = new JSONObject();
617     XMLTokener x = new XMLTokener(reader);
618     while (x.more())
619     {
620       x.skipPast("<");
621       if (x.more())
622       {
623         parse(x, jo, null, keepStrings);
624       }
625     }
626     return jo;
627   }
628
629   /**
630    * Convert a well-formed (but not necessarily valid) XML string into a
631    * JSONObject. Some information may be lost in this transformation because
632    * JSON is a data format and XML is a document format. XML uses elements,
633    * attributes, and content text, while JSON uses unordered collections of
634    * name/value pairs and arrays of values. JSON does not does not like to
635    * distinguish between elements and attributes. Sequences of similar elements
636    * are represented as JSONArrays. Content text may be placed in a "content"
637    * member. Comments, prologs, DTDs, and <code>&lt;[ [ ]]></code> are ignored.
638    * 
639    * All values are converted as strings, for 1, 01, 29.0 will not be coerced to
640    * numbers but will instead be the exact value as seen in the XML document.
641    * 
642    * @param string
643    *          The source string.
644    * @param keepStrings
645    *          If true, then values will not be coerced into boolean or numeric
646    *          values and will instead be left as strings
647    * @return A JSONObject containing the structured data from the XML string.
648    * @throws JSONException
649    *           Thrown if there is an errors while parsing the string
650    */
651   public static JSONObject toJSONObject(String string, boolean keepStrings)
652           throws JSONException
653   {
654     return toJSONObject(new StringReader(string), keepStrings);
655   }
656
657   /**
658    * Convert a JSONObject into a well-formed, element-normal XML string.
659    * 
660    * @param object
661    *          A JSONObject.
662    * @return A string.
663    * @throws JSONException
664    *           Thrown if there is an error parsing the string
665    */
666   public static String toString(Object object) throws JSONException
667   {
668     return toString(object, null);
669   }
670
671   /**
672    * Convert a JSONObject into a well-formed, element-normal XML string.
673    * 
674    * @param object
675    *          A JSONObject.
676    * @param tagName
677    *          The optional name of the enclosing tag.
678    * @return A string.
679    * @throws JSONException
680    *           Thrown if there is an error parsing the string
681    */
682   public static String toString(final Object object, final String tagName)
683           throws JSONException
684   {
685     StringBuilder sb = new StringBuilder();
686     JSONArray ja;
687     JSONObject jo;
688     String string;
689
690     if (object instanceof JSONObject)
691     {
692
693       // Emit <tagName>
694       if (tagName != null)
695       {
696         sb.append('<');
697         sb.append(tagName);
698         sb.append('>');
699       }
700
701       // Loop thru the keys.
702       // don't use the new entrySet accessor to maintain Android Support
703       jo = (JSONObject) object;
704       for (final String key : jo.keySet())
705       {
706         Object value = jo.opt(key);
707         if (value == null)
708         {
709           value = "";
710         }
711         else if (value.getClass().isArray())
712         {
713           value = new JSONArray(value);
714         }
715
716         // Emit content in body
717         if ("content".equals(key))
718         {
719           if (value instanceof JSONArray)
720           {
721             ja = (JSONArray) value;
722             int jaLength = ja.length();
723             // don't use the new iterator API to maintain support for Android
724             for (int i = 0; i < jaLength; i++)
725             {
726               if (i > 0)
727               {
728                 sb.append('\n');
729               }
730               Object val = ja.opt(i);
731               sb.append(escape(val.toString()));
732             }
733           }
734           else
735           {
736             sb.append(escape(value.toString()));
737           }
738
739           // Emit an array of similar keys
740
741         }
742         else if (value instanceof JSONArray)
743         {
744           ja = (JSONArray) value;
745           int jaLength = ja.length();
746           // don't use the new iterator API to maintain support for Android
747           for (int i = 0; i < jaLength; i++)
748           {
749             Object val = ja.opt(i);
750             if (val instanceof JSONArray)
751             {
752               sb.append('<');
753               sb.append(key);
754               sb.append('>');
755               sb.append(toString(val));
756               sb.append("</");
757               sb.append(key);
758               sb.append('>');
759             }
760             else
761             {
762               sb.append(toString(val, key));
763             }
764           }
765         }
766         else if ("".equals(value))
767         {
768           sb.append('<');
769           sb.append(key);
770           sb.append("/>");
771
772           // Emit a new tag <k>
773
774         }
775         else
776         {
777           sb.append(toString(value, key));
778         }
779       }
780       if (tagName != null)
781       {
782
783         // Emit the </tagname> close tag
784         sb.append("</");
785         sb.append(tagName);
786         sb.append('>');
787       }
788       return sb.toString();
789
790     }
791
792     if (object != null
793             && (object instanceof JSONArray || object.getClass().isArray()))
794     {
795       if (object.getClass().isArray())
796       {
797         ja = new JSONArray(object);
798       }
799       else
800       {
801         ja = (JSONArray) object;
802       }
803       int jaLength = ja.length();
804       // don't use the new iterator API to maintain support for Android
805       for (int i = 0; i < jaLength; i++)
806       {
807         Object val = ja.opt(i);
808         // XML does not have good support for arrays. If an array
809         // appears in a place where XML is lacking, synthesize an
810         // <array> element.
811         sb.append(toString(val, tagName == null ? "array" : tagName));
812       }
813       return sb.toString();
814     }
815
816     string = (object == null) ? "null" : escape(object.toString());
817     return (tagName == null) ? "\"" + string + "\""
818             : (string.length() == 0) ? "<" + tagName + "/>"
819                     : "<" + tagName + ">" + string + "</" + tagName + ">";
820
821   }
822 }