1 // Copyright 2007, 2008 The Apache Software Foundation
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package org.apache.tapestry5.json;
16
17 /*
18 Copyright (c) 2002 JSON.org
19
20 Permission is hereby granted, free of charge, to any person obtaining a copy
21 of this software and associated documentation files (the "Software"), to deal
22 in the Software without restriction, including without limitation the rights
23 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
24 copies of the Software, and to permit persons to whom the Software is
25 furnished to do so, subject to the following conditions:
26
27 The above copyright notice and this permission notice shall be included in all
28 copies or substantial portions of the Software.
29
30 The Software shall be used for Good, not Evil.
31
32 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38 SOFTWARE.
39 */
40
41 import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
42
43 import java.util.List;
44
45 /**
46 * A JSONArray is an ordered sequence of values. Its external text form is a string wrapped in square brackets with
47 * commas separating the values. The internal form is an object having <code>get</code> and <code>opt</code> methods for
48 * accessing the values by index, and <code>put</code> methods for adding or replacing values. The values can be any of
49 * these types: <code>Boolean</code>, <code>JSONArray</code>, <code>JSONObject</code>, <code>Number</code>,
50 * <code>String</code>, or the <code>JSONObject.NULL object</code>.
51 * <p/>
52 * The constructor can convert a JSON text into a Java object. The <code>toString</code> method converts to JSON text.
53 * <p/>
54 * A <code>get</code> method returns a value if one can be found, and throws an exception if one cannot be found. An
55 * <code>opt</code> method returns a default value instead of throwing an exception, and so is useful for obtaining
56 * optional values.
57 * <p/>
58 * The generic <code>get()</code> and <code>opt()</code> methods return an object which you can cast or query for type.
59 * There are also typed <code>get</code> and <code>opt</code> methods that do type checking and type coersion for you.
60 * <p/>
61 * The texts produced by the <code>toString</code> methods strictly conform to JSON syntax rules. The constructors are
62 * more forgiving in the texts they will accept: <ul> <li>An extra <code>,</code> <small>(comma)</small> may appear
63 * just before the closing bracket.</li> <li>The <code>null</code> value will be inserted when there is
64 * <code>,</code> <small>(comma)</small> elision.</li> <li>Strings may be quoted with
65 * <code>'</code> <small>(single quote)</small>.</li> <li>Strings do not need to be quoted at all if they do not
66 * begin with a quote or single quote, and if they do not contain leading or trailing spaces, and if they do not contain
67 * any of these characters: <code>{ } [ ] / \ : , = ; #</code> and if they do not look like numbers and if they are not
68 * the reserved words <code>true</code>, <code>false</code>, or <code>null</code>.</li> <li>Values can be separated by
69 * <code>;</code> <small>(semicolon)</small> as well as by <code>,</code> <small>(comma)</small>.</li> <li>Numbers may
70 * have the <code>0-</code> <small>(octal)</small> or <code>0x-</code> <small>(hex)</small> prefix.</li> <li>Comments
71 * written in the slashshlash, slashstar, and hash conventions will be ignored.</li> </ul>
72 *
73 * @author JSON.org
74 * @version 2
75 */
76 public final class JSONArray
77 {
78
79 /**
80 * The arrayList where the JSONArray's properties are kept.
81 */
82 private final List<Object> list = CollectionFactory.newList();
83
84 /**
85 * Construct an empty JSONArray.
86 */
87 public JSONArray()
88 {
89 }
90
91 public JSONArray(String text)
92 {
93 JSONTokener tokener = new JSONTokener(text);
94
95 parse(tokener);
96 }
97
98 public JSONArray(Object... values)
99 {
100 for (Object value : values) put(value);
101 }
102
103 /**
104 * Construct a JSONArray from a JSONTokener.
105 *
106 * @param tokenizer A JSONTokener
107 * @throws RuntimeException If there is a syntax error.
108 */
109 JSONArray(JSONTokener tokenizer)
110 {
111 assert tokenizer != null;
112
113 parse(tokenizer);
114 }
115
116 private void parse(JSONTokener tokenizer)
117 {
118 if (tokenizer.nextClean() != '[')
119 {
120 throw tokenizer
121 .syntaxError("A JSONArray text must start with '['");
122 }
123
124 if (tokenizer.nextClean() == ']')
125 {
126 return;
127 }
128
129 tokenizer.back();
130
131 while (true)
132 {
133 if (tokenizer.nextClean() == ',')
134 {
135 tokenizer.back();
136 list.add(null);
137 }
138 else
139 {
140 tokenizer.back();
141 list.add(tokenizer.nextValue());
142 }
143
144 switch (tokenizer.nextClean())
145 {
146 case ';':
147 case ',':
148 if (tokenizer.nextClean() == ']')
149 {
150 return;
151 }
152 tokenizer.back();
153 break;
154
155 case ']':
156 return;
157
158 default:
159 throw tokenizer.syntaxError("Expected a ',' or ']'");
160 }
161 }
162 }
163
164 /**
165 * Get the object value associated with an index.
166 *
167 * @param index The index must be between 0 and length() - 1.
168 * @return An object value.
169 * @throws RuntimeException If there is no value for the index.
170 */
171 public Object get(int index)
172 {
173 return list.get(index);
174 }
175
176 /**
177 * Get the boolean value associated with an index. The string values "true" and "false" are converted to boolean.
178 *
179 * @param index The index must be between 0 and length() - 1.
180 * @return The truth.
181 * @throws RuntimeException If there is no value for the index or if the value is not convertable to boolean.
182 */
183 public boolean getBoolean(int index)
184 {
185 Object value = get(index);
186
187 if (value instanceof Boolean)
188 {
189 return (Boolean) value;
190 }
191
192 if (value instanceof String)
193 {
194 String asString = (String) value;
195
196 if (asString.equalsIgnoreCase("false")) return false;
197
198 if (asString.equalsIgnoreCase("true")) return true;
199 }
200
201 throw new RuntimeException("JSONArray[" + index + "] is not a Boolean.");
202 }
203
204 /**
205 * Get the double value associated with an index.
206 *
207 * @param index The index must be between 0 and length() - 1.
208 * @return The value.
209 * @throws IllegalArgumentException If the key is not found or if the value cannot be converted to a number.
210 */
211 public double getDouble(int index)
212 {
213 Object value = get(index);
214
215 try
216 {
217 if (value instanceof Number) return ((Number) value).doubleValue();
218
219 return Double.valueOf((String) value);
220 }
221 catch (Exception e)
222 {
223 throw new IllegalArgumentException("JSONArray[" + index + "] is not a number.");
224 }
225 }
226
227 /**
228 * Get the int value associated with an index.
229 *
230 * @param index The index must be between 0 and length() - 1.
231 * @return The value.
232 * @throws IllegalArgumentException If the key is not found or if the value cannot be converted to a number. if the
233 * value cannot be converted to a number.
234 */
235 public int getInt(int index)
236 {
237 Object o = get(index);
238 return o instanceof Number ? ((Number) o).intValue() : (int) getDouble(index);
239 }
240
241 /**
242 * Get the JSONArray associated with an index.
243 *
244 * @param index The index must be between 0 and length() - 1.
245 * @return A JSONArray value.
246 * @throws RuntimeException If there is no value for the index. or if the value is not a JSONArray
247 */
248 public JSONArray getJSONArray(int index)
249 {
250 Object o = get(index);
251 if (o instanceof JSONArray)
252 {
253 return (JSONArray) o;
254 }
255
256 throw new RuntimeException("JSONArray[" + index + "] is not a JSONArray.");
257 }
258
259 /**
260 * Get the JSONObject associated with an index.
261 *
262 * @param index subscript
263 * @return A JSONObject value.
264 * @throws RuntimeException If there is no value for the index or if the value is not a JSONObject
265 */
266 public JSONObject getJSONObject(int index)
267 {
268 Object o = get(index);
269 if (o instanceof JSONObject)
270 {
271 return (JSONObject) o;
272 }
273
274 throw new RuntimeException("JSONArray[" + index + "] is not a JSONObject.");
275 }
276
277 /**
278 * Get the long value associated with an index.
279 *
280 * @param index The index must be between 0 and length() - 1.
281 * @return The value.
282 * @throws IllegalArgumentException If the key is not found or if the value cannot be converted to a number.
283 */
284 public long getLong(int index)
285 {
286 Object o = get(index);
287 return o instanceof Number ? ((Number) o).longValue() : (long) getDouble(index);
288 }
289
290 /**
291 * Get the string associated with an index.
292 *
293 * @param index The index must be between 0 and length() - 1.
294 * @return A string value.
295 * @throws RuntimeException If there is no value for the index.
296 */
297 public String getString(int index)
298 {
299 return get(index).toString();
300 }
301
302 /**
303 * Determine if the value is null.
304 *
305 * @param index The index must be between 0 and length() - 1.
306 * @return true if the value at the index is null, or if there is no value.
307 */
308 public boolean isNull(int index)
309 {
310 return get(index) == JSONObject.NULL;
311 }
312
313 /**
314 * Make a string from the contents of this JSONArray. The <code>separator</code> string is inserted between each
315 * element. Warning: This method assumes that the data structure is acyclical.
316 *
317 * @param separator A string that will be inserted between the elements.
318 * @return a string.
319 * @throws RuntimeException If the array contains an invalid number.
320 */
321 public String join(String separator)
322 {
323 int len = length();
324 StringBuilder buffer = new StringBuilder();
325
326 for (int i = 0; i < len; i += 1)
327 {
328 if (i > 0) buffer.append(separator);
329
330 buffer.append(JSONObject.valueToString(list.get(i)));
331 }
332
333 return buffer.toString();
334 }
335
336 /**
337 * Get the number of elements in the JSONArray, included nulls.
338 *
339 * @return The length (or size).
340 */
341 public int length()
342 {
343 return list.size();
344 }
345
346 /**
347 * Append an object value. This increases the array's length by one.
348 *
349 * @param value An object value. The value should be a Boolean, Double, Integer, JSONArray, JSONObject, Long, or
350 * String, or the JSONObject.NULL object.
351 * @return
352 */
353 public JSONArray put(Object value)
354 {
355 assert value != null;
356
357 JSONObject.testValidity(value);
358
359 list.add(value);
360
361 return this;
362 }
363
364 /**
365 * Put or replace an object value in the JSONArray. If the index is greater than the length of the JSONArray, then
366 * null elements will be added as necessary to pad it out.
367 *
368 * @param index The subscript.
369 * @param value The value to put into the array. The value should be a Boolean, Double, Integer, JSONArray,
370 * JSONObject, Long, or String, or the JSONObject.NULL object.
371 * @return
372 * @throws RuntimeException If the index is negative or if the the value is an invalid number.
373 */
374 public JSONArray put(int index, Object value)
375 {
376 assert value != null;
377
378 if (index < 0)
379 {
380 throw new RuntimeException("JSONArray[" + index + "] not found.");
381 }
382
383 JSONObject.testValidity(value);
384
385 if (index < length())
386 {
387 list.set(index, value);
388 }
389 else
390 {
391 while (index != length()) list.add(JSONObject.NULL);
392
393 list.add(value);
394 }
395
396 return this;
397 }
398
399 /**
400 * Make a JSON text of this JSONArray. For compactness, no unnecessary whitespace is added. If it is not possible to
401 * produce a syntactically correct JSON text then null will be returned instead. This could occur if the array
402 * contains an invalid number.
403 * <p/>
404 * Warning: This method assumes that the data structure is acyclical.
405 *
406 * @return a printable, displayable, transmittable representation of the array.
407 */
408 @Override
409 public String toString()
410 {
411 try
412 {
413 return '[' + join(",") + ']';
414 }
415 catch (Exception e)
416 {
417 return null;
418 }
419 }
420
421 Object[] toArray()
422 {
423 return list.toArray();
424 }
425
426 @Override
427 public boolean equals(Object obj)
428 {
429 if (obj == null) return false;
430
431 if (!(obj instanceof JSONArray)) return false;
432
433 JSONArray other = (JSONArray) obj;
434
435 return list.equals(other.list);
436 }
437 }