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