001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH (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, 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 = new Character('&');
067
068    /** The Character '''. */
069    public static final Character APOS = new Character('\'');
070
071    /** The Character '!'. */
072    public static final Character BANG = new Character('!');
073
074    /** The Character '='. */
075    public static final Character EQ = new Character('=');
076
077    /** The Character '>'. */
078    public static final Character GT = new Character('>');
079
080    /** The Character '<'. */
081    public static final Character LT = new Character('<');
082
083    /** The Character '?'. */
084    public static final Character QUEST = new Character('?');
085
086    /** The Character '"'. */
087    public static final Character QUOT = new Character('"');
088
089    /** The Character '/'. */
090    public static final Character SLASH = new Character('/');
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) ? "\"" + s + "\"" : (s.length() == 0) ? "<" + tagName + "/>" : "<"
277                + tagName
278                + ">"
279                + s
280                + "</"
281                + tagName
282                + ">";
283        }
284    }
285
286    /**
287     * Scan the content following the named tag, attaching it to the context.<p>
288     * 
289     * @param x       the XMLTokener containing the source string
290     * @param context the JSONObject that will include the new material
291     * @param name    the tag name
292     * @return true if the close tag is processed
293     * @throws JSONException if something goes wrong
294     */
295    private static boolean parse(XMLTokener x, JSONObject context, String name) throws JSONException {
296
297        char c;
298        int i;
299        String n;
300        JSONObject o = null;
301        String s;
302        Object t;
303
304        // Test for and skip past these forms:
305        //      <!-- ... -->
306        //      <!   ...   >
307        //      <![  ... ]]>
308        //      <?   ...  ?>
309        // Report errors for these forms:
310        //      <>
311        //      <=
312        //      <<
313
314        t = x.nextToken();
315
316        // <!
317
318        if (t == BANG) {
319            c = x.next();
320            if (c == '-') {
321                if (x.next() == '-') {
322                    x.skipPast("-->");
323                    return false;
324                }
325                x.back();
326            } else if (c == '[') {
327                t = x.nextToken();
328                if (t.equals("CDATA")) {
329                    if (x.next() == '[') {
330                        s = x.nextCDATA();
331                        if (s.length() > 0) {
332                            context.accumulate("content", s);
333                        }
334                        return false;
335                    }
336                }
337                throw x.syntaxError("Expected 'CDATA['");
338            }
339            i = 1;
340            do {
341                t = x.nextMeta();
342                if (t == null) {
343                    throw x.syntaxError("Missing '>' after '<!'.");
344                } else if (t == LT) {
345                    i += 1;
346                } else if (t == GT) {
347                    i -= 1;
348                }
349            } while (i > 0);
350            return false;
351        } else if (t == QUEST) {
352
353            // <?
354
355            x.skipPast("?>");
356            return false;
357        } else if (t == SLASH) {
358
359            // Close tag </
360
361            t = x.nextToken();
362            if (name == null) {
363                throw x.syntaxError("Mismatched close tag" + t);
364            }
365            if (!t.equals(name)) {
366                throw x.syntaxError("Mismatched " + name + " and " + t);
367            }
368            if (x.nextToken() != GT) {
369                throw x.syntaxError("Misshaped close tag");
370            }
371            return true;
372
373        } else if (t instanceof Character) {
374            throw x.syntaxError("Misshaped tag");
375
376            // Open tag <
377
378        } else {
379            n = (String)t;
380            t = null;
381            o = new JSONObject();
382            for (;;) {
383                if (t == null) {
384                    t = x.nextToken();
385                }
386
387                // attribute = value
388
389                if (t instanceof String) {
390                    s = (String)t;
391                    t = x.nextToken();
392                    if (t == EQ) {
393                        t = x.nextToken();
394                        if (!(t instanceof String)) {
395                            throw x.syntaxError("Missing value");
396                        }
397                        o.accumulate(s, t);
398                        t = null;
399                    } else {
400                        o.accumulate(s, "");
401                    }
402
403                    // Empty tag <.../>
404
405                } else if (t == SLASH) {
406                    if (x.nextToken() != GT) {
407                        throw x.syntaxError("Misshaped tag");
408                    }
409                    context.accumulate(n, o);
410                    return false;
411
412                    // Content, between <...> and </...>
413
414                } else if (t == GT) {
415                    for (;;) {
416                        t = x.nextContent();
417                        if (t == null) {
418                            if (n != null) {
419                                throw x.syntaxError("Unclosed tag " + n);
420                            }
421                            return false;
422                        } else if (t instanceof String) {
423                            s = (String)t;
424                            if (s.length() > 0) {
425                                o.accumulate("content", s);
426                            }
427
428                            // Nested element
429
430                        } else if (t == LT) {
431                            if (parse(x, o, n)) {
432                                if (o.length() == 0) {
433                                    context.accumulate(n, "");
434                                } else if ((o.length() == 1) && (o.opt("content") != null)) {
435                                    context.accumulate(n, o.opt("content"));
436                                } else {
437                                    context.accumulate(n, o);
438                                }
439                                return false;
440                            }
441                        }
442                    }
443                } else {
444                    throw x.syntaxError("Misshaped tag");
445                }
446            }
447        }
448    }
449}