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