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 }