001package org.json;
002
003/*
004Copyright (c) 2015 JSON.org
005
006Permission is hereby granted, free of charge, to any person obtaining a copy
007of this software and associated documentation files (the "Software"), to deal
008in the Software without restriction, including without limitation the rights
009to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010copies of the Software, and to permit persons to whom the Software is
011furnished to do so, subject to the following conditions:
012
013The above copyright notice and this permission notice shall be included in all
014copies or substantial portions of the Software.
015
016The Software shall be used for Good, not Evil.
017
018THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
019IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
020FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
021AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
022LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
023OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
024SOFTWARE.
025*/
026
027import java.util.Iterator;
028
029/**
030 * This provides static methods to convert an XML text into a JSONObject, and to
031 * covert a JSONObject into an XML text.
032 * 
033 * @author JSON.org
034 * @version 2016-01-30
035 */
036@SuppressWarnings("boxing")
037public class XML {
038
039    /** The Character '&'. */
040    public static final Character AMP = '&';
041
042    /** The Character '''. */
043    public static final Character APOS = '\'';
044
045    /** The Character '!'. */
046    public static final Character BANG = '!';
047
048    /** The Character '='. */
049    public static final Character EQ = '=';
050
051    /** The Character '>'. */
052    public static final Character GT = '>';
053
054    /** The Character '<'. */
055    public static final Character LT = '<';
056
057    /** The Character '?'. */
058    public static final Character QUEST = '?';
059
060    /** The Character '"'. */
061    public static final Character QUOT = '"';
062
063    /** The Character '/'. */
064    public static final Character SLASH = '/';
065
066    /**
067     * Replace special characters with XML escapes:
068     * 
069     * <pre>
070     * &amp; <small>(ampersand)</small> is replaced by &amp;amp;
071     * &lt; <small>(less than)</small> is replaced by &amp;lt;
072     * &gt; <small>(greater than)</small> is replaced by &amp;gt;
073     * &quot; <small>(double quote)</small> is replaced by &amp;quot;
074     * </pre>
075     * 
076     * @param string
077     *            The string to be escaped.
078     * @return The escaped string.
079     */
080    public static String escape(String string) {
081        StringBuilder sb = new StringBuilder(string.length());
082        for (int i = 0, length = string.length(); i < length; i++) {
083            char c = string.charAt(i);
084            switch (c) {
085            case '&':
086                sb.append("&amp;");
087                break;
088            case '<':
089                sb.append("&lt;");
090                break;
091            case '>':
092                sb.append("&gt;");
093                break;
094            case '"':
095                sb.append("&quot;");
096                break;
097            case '\'':
098                sb.append("&apos;");
099                break;
100            default:
101                sb.append(c);
102            }
103        }
104        return sb.toString();
105    }
106
107    /**
108     * Throw an exception if the string contains whitespace. Whitespace is not
109     * allowed in tagNames and attributes.
110     * 
111     * @param string
112     *            A string.
113     * @throws JSONException
114     */
115    public static void noSpace(String string) throws JSONException {
116        int i, length = string.length();
117        if (length == 0) {
118            throw new JSONException("Empty string.");
119        }
120        for (i = 0; i < length; i += 1) {
121            if (Character.isWhitespace(string.charAt(i))) {
122                throw new JSONException("'" + string
123                        + "' contains a space character.");
124            }
125        }
126    }
127
128    /**
129     * Scan the content following the named tag, attaching it to the context.
130     * 
131     * @param x
132     *            The XMLTokener containing the source string.
133     * @param context
134     *            The JSONObject that will include the new material.
135     * @param name
136     *            The tag name.
137     * @return true if the close tag is processed.
138     * @throws JSONException
139     */
140    private static boolean parse(XMLTokener x, JSONObject context, String name)
141            throws JSONException {
142        char c;
143        int i;
144        JSONObject jsonobject = null;
145        String string;
146        String tagName;
147        Object token;
148
149        // Test for and skip past these forms:
150        // <!-- ... -->
151        // <! ... >
152        // <![ ... ]]>
153        // <? ... ?>
154        // Report errors for these forms:
155        // <>
156        // <=
157        // <<
158
159        token = x.nextToken();
160
161        // <!
162
163        if (token == BANG) {
164            c = x.next();
165            if (c == '-') {
166                if (x.next() == '-') {
167                    x.skipPast("-->");
168                    return false;
169                }
170                x.back();
171            } else if (c == '[') {
172                token = x.nextToken();
173                if ("CDATA".equals(token)) {
174                    if (x.next() == '[') {
175                        string = x.nextCDATA();
176                        if (string.length() > 0) {
177                            context.accumulate("content", string);
178                        }
179                        return false;
180                    }
181                }
182                throw x.syntaxError("Expected 'CDATA['");
183            }
184            i = 1;
185            do {
186                token = x.nextMeta();
187                if (token == null) {
188                    throw x.syntaxError("Missing '>' after '<!'.");
189                } else if (token == LT) {
190                    i += 1;
191                } else if (token == GT) {
192                    i -= 1;
193                }
194            } while (i > 0);
195            return false;
196        } else if (token == QUEST) {
197
198            // <?
199            x.skipPast("?>");
200            return false;
201        } else if (token == SLASH) {
202
203            // Close tag </
204
205            token = x.nextToken();
206            if (name == null) {
207                throw x.syntaxError("Mismatched close tag " + token);
208            }
209            if (!token.equals(name)) {
210                throw x.syntaxError("Mismatched " + name + " and " + token);
211            }
212            if (x.nextToken() != GT) {
213                throw x.syntaxError("Misshaped close tag");
214            }
215            return true;
216
217        } else if (token instanceof Character) {
218            throw x.syntaxError("Misshaped tag");
219
220            // Open tag <
221
222        } else {
223            tagName = (String) token;
224            token = null;
225            jsonobject = new JSONObject();
226            for (;;) {
227                if (token == null) {
228                    token = x.nextToken();
229                }
230
231                // attribute = value
232                if (token instanceof String) {
233                    string = (String) token;
234                    token = x.nextToken();
235                    if (token == EQ) {
236                        token = x.nextToken();
237                        if (!(token instanceof String)) {
238                            throw x.syntaxError("Missing value");
239                        }
240                        jsonobject.accumulate(string,
241                                JSONObject.stringToValue((String) token));
242                        token = null;
243                    } else {
244                        jsonobject.accumulate(string, "");
245                    }
246
247
248                } else if (token == SLASH) {
249                    // Empty tag <.../>
250                    if (x.nextToken() != GT) {
251                        throw x.syntaxError("Misshaped tag");
252                    }
253                    if (jsonobject.length() > 0) {
254                        context.accumulate(tagName, jsonobject);
255                    } else {
256                        context.accumulate(tagName, "");
257                    }
258                    return false;
259
260                } else if (token == GT) {
261                    // Content, between <...> and </...>
262                    for (;;) {
263                        token = x.nextContent();
264                        if (token == null) {
265                            if (tagName != null) {
266                                throw x.syntaxError("Unclosed tag " + tagName);
267                            }
268                            return false;
269                        } else if (token instanceof String) {
270                            string = (String) token;
271                            if (string.length() > 0) {
272                                jsonobject.accumulate("content",
273                                        JSONObject.stringToValue(string));
274                            }
275
276                        } else if (token == LT) {
277                            // Nested element
278                            if (parse(x, jsonobject, tagName)) {
279                                if (jsonobject.length() == 0) {
280                                    context.accumulate(tagName, "");
281                                } else if (jsonobject.length() == 1
282                                        && jsonobject.opt("content") != null) {
283                                    context.accumulate(tagName,
284                                            jsonobject.opt("content"));
285                                } else {
286                                    context.accumulate(tagName, jsonobject);
287                                }
288                                return false;
289                            }
290                        }
291                    }
292                } else {
293                    throw x.syntaxError("Misshaped tag");
294                }
295            }
296        }
297    }
298    
299    /**
300     * This method has been deprecated in favor of the
301     * {@link JSONObject.stringToValue(String)} method. Use it instead.
302     * 
303     * @deprecated Use {@link JSONObject#stringToValue(String)} instead.
304     * @param string
305     * @return JSON value of this string or the string
306     */
307    public static Object stringToValue(String string) {
308        return JSONObject.stringToValue(string);
309    }
310
311    /**
312     * Convert a well-formed (but not necessarily valid) XML string into a
313     * JSONObject. Some information may be lost in this transformation because
314     * JSON is a data format and XML is a document format. XML uses elements,
315     * attributes, and content text, while JSON uses unordered collections of
316     * name/value pairs and arrays of values. JSON does not does not like to
317     * distinguish between elements and attributes. Sequences of similar
318     * elements are represented as JSONArrays. Content text may be placed in a
319     * "content" member. Comments, prologs, DTDs, and <code>&lt;[ [ ]]></code>
320     * are ignored.
321     * 
322     * @param string
323     *            The source string.
324     * @return A JSONObject containing the structured data from the XML string.
325     * @throws JSONException
326     */
327    public static JSONObject toJSONObject(String string) throws JSONException {
328        JSONObject jo = new JSONObject();
329        XMLTokener x = new XMLTokener(string);
330        while (x.more() && x.skipPast("<")) {
331            parse(x, jo, null);
332        }
333        return jo;
334    }
335
336    /**
337     * Convert a JSONObject into a well-formed, element-normal XML string.
338     * 
339     * @param object
340     *            A JSONObject.
341     * @return A string.
342     * @throws JSONException
343     */
344    public static String toString(Object object) throws JSONException {
345        return toString(object, null);
346    }
347
348    /**
349     * Convert a JSONObject into a well-formed, element-normal XML string.
350     * 
351     * @param object
352     *            A JSONObject.
353     * @param tagName
354     *            The optional name of the enclosing tag.
355     * @return A string.
356     * @throws JSONException
357     */
358    public static String toString(Object object, String tagName)
359            throws JSONException {
360        StringBuilder sb = new StringBuilder();
361        JSONArray ja;
362        JSONObject jo;
363        String key;
364        Iterator<String> keys;
365        String string;
366        Object value;
367
368        if (object instanceof JSONObject) {
369
370            // Emit <tagName>
371            if (tagName != null) {
372                sb.append('<');
373                sb.append(tagName);
374                sb.append('>');
375            }
376
377            // Loop thru the keys.
378            jo = (JSONObject) object;
379            keys = jo.keys();
380            while (keys.hasNext()) {
381                key = keys.next();
382                value = jo.opt(key);
383                if (value == null) {
384                    value = "";
385                } else if (value.getClass().isArray()) {
386                    value = new JSONArray(value);
387                }
388                string = value instanceof String ? (String) value : null;
389
390                // Emit content in body
391                if ("content".equals(key)) {
392                    if (value instanceof JSONArray) {
393                        ja = (JSONArray) value;
394                        int i = 0;
395                        for (Object val : ja) {
396                            if (i > 0) {
397                                sb.append('\n');
398                            }
399                            sb.append(escape(val.toString()));
400                            i++;
401                        }
402                    } else {
403                        sb.append(escape(value.toString()));
404                    }
405
406                    // Emit an array of similar keys
407
408                } else if (value instanceof JSONArray) {
409                    ja = (JSONArray) value;
410                    for (Object val : ja) {
411                        if (val instanceof JSONArray) {
412                            sb.append('<');
413                            sb.append(key);
414                            sb.append('>');
415                            sb.append(toString(val));
416                            sb.append("</");
417                            sb.append(key);
418                            sb.append('>');
419                        } else {
420                            sb.append(toString(val, key));
421                        }
422                    }
423                } else if ("".equals(value)) {
424                    sb.append('<');
425                    sb.append(key);
426                    sb.append("/>");
427
428                    // Emit a new tag <k>
429
430                } else {
431                    sb.append(toString(value, key));
432                }
433            }
434            if (tagName != null) {
435
436                // Emit the </tagname> close tag
437                sb.append("</");
438                sb.append(tagName);
439                sb.append('>');
440            }
441            return sb.toString();
442
443        }
444
445        if (object != null) {
446            if (object.getClass().isArray()) {
447                object = new JSONArray(object);
448            }
449
450            if (object instanceof JSONArray) {
451                ja = (JSONArray) object;
452                for (Object val : ja) {
453                    // XML does not have good support for arrays. If an array
454                    // appears in a place where XML is lacking, synthesize an
455                    // <array> element.
456                    sb.append(toString(val, tagName == null ? "array" : tagName));
457                }
458                return sb.toString();
459            }
460        }
461
462        string = (object == null) ? "null" : escape(object.toString());
463        return (tagName == null) ? "\"" + string + "\""
464                : (string.length() == 0) ? "<" + tagName + "/>" : "<" + tagName
465                        + ">" + string + "</" + tagName + ">";
466
467    }
468}