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