001// Copyright 2007, 2008, 2010, 2011, 2012 The Apache Software Foundation
002//
003// Licensed under the Apache License, Version 2.0 (the "License");
004// you may not use this file except in compliance with the License.
005// You may obtain a copy of the License at
006//
007// http://www.apache.org/licenses/LICENSE-2.0
008//
009// Unless required by applicable law or agreed to in writing, software
010// distributed under the License is distributed on an "AS IS" BASIS,
011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012// See the License for the specific language governing permissions and
013// limitations under the License.
014
015package org.apache.tapestry5.json;
016
017/*
018 * Copyright (c) 2002 JSON.org
019 * Permission is hereby granted, free of charge, to any person obtaining a copy
020 * of this software and associated documentation files (the "Software"), to deal
021 * in the Software without restriction, including without limitation the rights
022 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
023 * copies of the Software, and to permit persons to whom the Software is
024 * furnished to do so, subject to the following conditions:
025 * The above copyright notice and this permission notice shall be included in all
026 * copies or substantial portions of the Software.
027 * The Software shall be used for Good, not Evil.
028 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
029 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
030 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
031 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
032 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
033 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
034 * SOFTWARE.
035 */
036
037import java.util.ArrayList;
038import java.util.Collections;
039import java.util.Iterator;
040import java.util.List;
041
042/**
043 * A JSONArray is an ordered sequence of values. Its external text form is a string wrapped in square brackets with
044 * commas separating the values. The internal form is an object having {@code get} and {@code opt} methods for
045 * accessing the values by index, and {@code put} methods for adding or replacing values. The values can be any of
046 * these types: {@code Boolean}, {@code JSONArray}, {@code JSONObject}, {@code Number},
047 * {@code String}, or the {@code JSONObject.NULL object}.
048 * <p/>
049 * The constructor can convert a JSON text into a Java object. The {@code toString} method converts to JSON text.
050 * <p/>
051 * A {@code get} method returns a value if one can be found, and throws an exception if one cannot be found. An
052 * {@code opt} method returns a default value instead of throwing an exception, and so is useful for obtaining
053 * optional values.
054 * <p/>
055 * The generic {@code get()} and {@code opt()} methods return an object which you can cast or query for type.
056 * There are also typed {@code get} and {@code opt} methods that do type checking and type coersion for you.
057 * <p/>
058 * The texts produced by the {@code toString} methods strictly conform to JSON syntax rules. The constructors are
059 * more forgiving in the texts they will accept:
060 * <ul>
061 * <li>An extra {@code ,}&nbsp;<small>(comma)</small> may appear just before the closing bracket.</li>
062 * <li>The {@code null} value will be inserted when there is {@code ,}&nbsp;<small>(comma)</small> elision.</li>
063 * <li>Strings may be quoted with {@code '}&nbsp;<small>(single quote)</small>.</li>
064 * <li>Strings do not need to be quoted at all if they do not begin with a quote or single quote, and if they do not
065 * contain leading or trailing spaces, and if they do not contain any of these characters:
066 * {@code { } [ ] / \ : , = ; #} and if they do not look like numbers and if they are not the reserved words
067 * {@code true}, {@code false}, or {@code null}.</li>
068 * <li>Values can be separated by {@code ;} <small>(semicolon)</small> as well as by {@code ,}
069 * <small>(comma)</small>.</li>
070 * <li>Numbers may have the {@code 0-} <small>(octal)</small> or {@code 0x-} <small>(hex)</small> prefix.</li>
071 * <li>Comments written in the slashshlash, slashstar, and hash conventions will be ignored.</li>
072 * </ul>
073 *
074 * @author JSON.org
075 * @version 2
076 */
077public final class JSONArray extends JSONCollection implements Iterable<Object>
078{
079
080    /**
081     * The arrayList where the JSONArray's properties are kept.
082     */
083    private final List<Object> list = new ArrayList<Object>();
084
085    /**
086     * Construct an empty JSONArray.
087     */
088    public JSONArray()
089    {
090    }
091
092    public JSONArray(String text)
093    {
094        JSONTokener tokener = new JSONTokener(text);
095
096        parse(tokener);
097    }
098
099    public JSONArray(Object... values)
100    {
101        for (Object value : values)
102            put(value);
103    }
104
105    /**
106     * Create a new array, and adds all values fro the iterable to the array (using {@link #putAll(Iterable)}.
107     * <p/>
108     * This is implemented as a static method so as not to break the semantics of the existing {@link #JSONArray(Object...)} constructor.
109     * Adding a constructor of type Iterable would change the meaning of <code>new JSONArray(new JSONArray())</code>.
110     *
111     * @param iterable
112     *         collection ot value to include, or null
113     * @since 5.4
114     */
115    public static JSONArray from(Iterable<?> iterable)
116    {
117        return new JSONArray().putAll(iterable);
118    }
119
120    public Iterator<Object> iterator()
121    {
122        return list.iterator();
123    }
124
125    /**
126     * Construct a JSONArray from a JSONTokener.
127     *
128     * @param tokenizer
129     *         A JSONTokener
130     * @throws RuntimeException
131     *         If there is a syntax error.
132     */
133    JSONArray(JSONTokener tokenizer)
134    {
135        assert tokenizer != null;
136
137        parse(tokenizer);
138    }
139
140    private void parse(JSONTokener tokenizer)
141    {
142        if (tokenizer.nextClean() != '[')
143        {
144            throw tokenizer.syntaxError("A JSONArray text must start with '['");
145        }
146
147        if (tokenizer.nextClean() == ']')
148        {
149            return;
150        }
151
152        tokenizer.back();
153
154        while (true)
155        {
156            if (tokenizer.nextClean() == ',')
157            {
158                tokenizer.back();
159                list.add(JSONObject.NULL);
160            } else
161            {
162                tokenizer.back();
163                list.add(tokenizer.nextValue());
164            }
165
166            switch (tokenizer.nextClean())
167            {
168                case ';':
169                case ',':
170                    if (tokenizer.nextClean() == ']')
171                    {
172                        return;
173                    }
174                    tokenizer.back();
175                    break;
176
177                case ']':
178                    return;
179
180                default:
181                    throw tokenizer.syntaxError("Expected a ',' or ']'");
182            }
183        }
184    }
185
186    /**
187     * Get the object value associated with an index.
188     *
189     * @param index
190     *         The index must be between 0 and length() - 1.
191     * @return An object value.
192     * @throws RuntimeException
193     *         If there is no value for the index.
194     */
195    public Object get(int index)
196    {
197        return list.get(index);
198    }
199
200    /**
201     * Remove the object associated with the index.
202     *
203     * @param index
204     *         The index must be between 0 and length() - 1.
205     * @return An object removed.
206     * @throws RuntimeException
207     *         If there is no value for the index.
208     */
209    public Object remove(int index)
210    {
211        return list.remove(index);
212    }
213
214    /**
215     * Get the boolean value associated with an index. The string values "true" and "false" are converted to boolean.
216     *
217     * @param index
218     *         The index must be between 0 and length() - 1.
219     * @return The truth.
220     * @throws RuntimeException
221     *         If there is no value for the index or if the value is not convertable to boolean.
222     */
223    public boolean getBoolean(int index)
224    {
225        Object value = get(index);
226
227        if (value instanceof Boolean)
228        {
229            return (Boolean) value;
230        }
231
232        if (value instanceof String)
233        {
234            String asString = (String) value;
235
236            if (asString.equalsIgnoreCase("false"))
237                return false;
238
239            if (asString.equalsIgnoreCase("true"))
240                return true;
241        }
242
243        throw new RuntimeException("JSONArray[" + index + "] is not a Boolean.");
244    }
245
246    /**
247     * Get the double value associated with an index.
248     *
249     * @param index
250     *         The index must be between 0 and length() - 1.
251     * @return The value.
252     * @throws IllegalArgumentException
253     *         If the key is not found or if the value cannot be converted to a number.
254     */
255    public double getDouble(int index)
256    {
257        Object value = get(index);
258
259        try
260        {
261            if (value instanceof Number)
262                return ((Number) value).doubleValue();
263
264            return Double.valueOf((String) value);
265        } catch (Exception e)
266        {
267            throw new IllegalArgumentException("JSONArray[" + index + "] is not a number.");
268        }
269    }
270
271    /**
272     * Get the int value associated with an index.
273     *
274     * @param index
275     *         The index must be between 0 and length() - 1.
276     * @return The value.
277     * @throws IllegalArgumentException
278     *         If the key is not found or if the value cannot be converted to a number. if the
279     *         value cannot be converted to a number.
280     */
281    public int getInt(int index)
282    {
283        Object o = get(index);
284        return o instanceof Number ? ((Number) o).intValue() : (int) getDouble(index);
285    }
286
287    /**
288     * Get the JSONArray associated with an index.
289     *
290     * @param index
291     *         The index must be between 0 and length() - 1.
292     * @return A JSONArray value.
293     * @throws RuntimeException
294     *         If there is no value for the index. or if the value is not a JSONArray
295     */
296    public JSONArray getJSONArray(int index)
297    {
298        Object o = get(index);
299        if (o instanceof JSONArray)
300        {
301            return (JSONArray) o;
302        }
303
304        throw new RuntimeException("JSONArray[" + index + "] is not a JSONArray.");
305    }
306
307    /**
308     * Get the JSONObject associated with an index.
309     *
310     * @param index
311     *         subscript
312     * @return A JSONObject value.
313     * @throws RuntimeException
314     *         If there is no value for the index or if the value is not a JSONObject
315     */
316    public JSONObject getJSONObject(int index)
317    {
318        Object o = get(index);
319        if (o instanceof JSONObject)
320        {
321            return (JSONObject) o;
322        }
323
324        throw new RuntimeException("JSONArray[" + index + "] is not a JSONObject.");
325    }
326
327    /**
328     * Get the long value associated with an index.
329     *
330     * @param index
331     *         The index must be between 0 and length() - 1.
332     * @return The value.
333     * @throws IllegalArgumentException
334     *         If the key is not found or if the value cannot be converted to a number.
335     */
336    public long getLong(int index)
337    {
338        Object o = get(index);
339        return o instanceof Number ? ((Number) o).longValue() : (long) getDouble(index);
340    }
341
342    /**
343     * Get the string associated with an index.
344     *
345     * @param index
346     *         The index must be between 0 and length() - 1.
347     * @return A string value.
348     * @throws RuntimeException
349     *         If there is no value for the index.
350     */
351    public String getString(int index)
352    {
353        return get(index).toString();
354    }
355
356    /**
357     * Determine if the value is null.
358     *
359     * @param index
360     *         The index must be between 0 and length() - 1.
361     * @return true if the value at the index is null, or if there is no value.
362     */
363    public boolean isNull(int index)
364    {
365        return get(index) == JSONObject.NULL;
366    }
367
368    /**
369     * Get the number of elements in the JSONArray, included nulls.
370     *
371     * @return The length (or size).
372     */
373    public int length()
374    {
375        return list.size();
376    }
377
378    /**
379     * Append an object value. This increases the array's length by one.
380     *
381     * @param value
382     *         An object value. The value should be a Boolean, Double, Integer, JSONArray, JSONObject, JSONLiteral,
383     *         Long, or String, or the JSONObject.NULL singleton.
384     * @return this array
385     */
386    public JSONArray put(Object value)
387    {
388        assert value != null;
389
390        JSONObject.testValidity(value);
391
392        list.add(value);
393
394        return this;
395    }
396
397    /**
398     * Put or replace an object value in the JSONArray. If the index is greater than the length of the JSONArray, then
399     * null elements will be added as necessary to pad it out.
400     *
401     * @param index
402     *         The subscript.
403     * @param value
404     *         The value to put into the array. The value should be a Boolean, Double, Integer, JSONArray,
405     *         JSONObject, JSONString, Long, or String, or the JSONObject.NULL singeton.
406     * @return this array
407     * @throws RuntimeException
408     *         If the index is negative or if the the value is an invalid number.
409     */
410    public JSONArray put(int index, Object value)
411    {
412        assert value != null;
413
414        if (index < 0)
415        {
416            throw new RuntimeException("JSONArray[" + index + "] not found.");
417        }
418
419        JSONObject.testValidity(value);
420
421        if (index < length())
422        {
423            list.set(index, value);
424        } else
425        {
426            while (index != length())
427                list.add(JSONObject.NULL);
428
429            list.add(value);
430        }
431
432        return this;
433    }
434
435    @Override
436    public boolean equals(Object obj)
437    {
438        if (obj == null)
439            return false;
440
441        if (!(obj instanceof JSONArray))
442            return false;
443
444        JSONArray other = (JSONArray) obj;
445
446        return list.equals(other.list);
447    }
448
449    void print(JSONPrintSession session)
450    {
451        session.printSymbol('[');
452
453        session.indent();
454
455        boolean comma = false;
456
457        for (Object value : list)
458        {
459            if (comma)
460                session.printSymbol(',');
461
462            session.newline();
463
464            JSONObject.printValue(session, value);
465
466            comma = true;
467        }
468
469        session.outdent();
470
471        if (comma)
472            session.newline();
473
474        session.printSymbol(']');
475    }
476
477    /**
478     * Puts all objects from the collection into this JSONArray, using {@link #put(Object)}.
479     *
480     * @param collection
481     *         List, array, JSONArray, or other iterable object, or null
482     * @return this JSONArray
483     * @since 5.4
484     */
485    public JSONArray putAll(Iterable<?> collection)
486    {
487        if (collection != null)
488        {
489            for (Object o : collection)
490            {
491                put(o);
492            }
493        }
494
495        return this;
496    }
497
498    /**
499     * Returns an unmodifiable list of the contents of the array. This is a wrapper around the list's internal
500     * storage and is live (changes to the JSONArray affect the returned List).
501     *
502     * @return unmodifiable list of array contents
503     * @since 5.4
504     */
505    public List<Object> toList()
506    {
507        return Collections.unmodifiableList(list);
508    }
509}