    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    @Override
    121    public Iterator<Object> iterator()
    122    {
    123        return list.iterator();
    124    }
    125
    126    /**
    127     * Construct a JSONArray from a JSONTokener.
    128     *
    129     * @param tokenizer
    130     *         A JSONTokener
    131     * @throws RuntimeException
    132     *         If there is a syntax error.
    133     */
    134    JSONArray(JSONTokener tokenizer)
    135    {
    136        assert tokenizer != null;
    137
    138        parse(tokenizer);
    139    }
    140
    141    private void parse(JSONTokener tokenizer)
    142    {
    143        if (tokenizer.nextClean() != '[')
    144        {
    145            throw tokenizer.syntaxError("A JSONArray text must start with '['");
    146        }
    147
    148        if (tokenizer.nextClean() == ']')
    149        {
    150            return;
    151        }
    152
    153        tokenizer.back();
    154
    155        while (true)
    156        {
    157            if (tokenizer.nextClean() == ',')
    158            {
    159                tokenizer.back();
    160                list.add(JSONObject.NULL);
    161            } else
    162            {
    163                tokenizer.back();
    164                list.add(tokenizer.nextValue());
    165            }
    166
    167            switch (tokenizer.nextClean())
    168            {
    169                case ';':
    170                case ',':
    171                    if (tokenizer.nextClean() == ']')
    172                    {
    173                        return;
    174                    }
    175                    tokenizer.back();
    176                    break;
    177
    178                case ']':
    179                    return;
    180
    181                default:
    182                    throw tokenizer.syntaxError("Expected a ',' or ']'");
    183            }
    184        }
    185    }
    186
    187    /**
    188     * Get the object value associated with an index.
    189     *
    190     * @param index
    191     *         The index must be between 0 and length() - 1.
    192     * @return An object value.
    193     * @throws RuntimeException
    194     *         If there is no value for the index.
    195     */
    196    public Object get(int index)
    197    {
    198        return list.get(index);
    199    }
    200
    201    /**
    202     * Remove the object associated with the index.
    203     *
    204     * @param index
    205     *         The index must be between 0 and length() - 1.
    206     * @return An object removed.
    207     * @throws RuntimeException
    208     *         If there is no value for the index.
    209     */
    210    public Object remove(int index)
    211    {
    212        return list.remove(index);
    213    }
    214
    215    /**
    216     * Get the boolean value associated with an index. The string values "true" and "false" are converted to boolean.
    217     *
    218     * @param index
    219     *         The index must be between 0 and length() - 1.
    220     * @return The truth.
    221     * @throws RuntimeException
    222     *         If there is no value for the index or if the value is not convertable to boolean.
    223     */
    224    public boolean getBoolean(int index)
    225    {
    226        Object value = get(index);
    227
    228        if (value instanceof Boolean)
    229        {
    230            return (Boolean) value;
    231        }
    232
    233        if (value instanceof String)
    234        {
    235            String asString = (String) value;
    236
    237            if (asString.equalsIgnoreCase("false"))
    238                return false;
    239
    240            if (asString.equalsIgnoreCase("true"))
    241                return true;
    242        }
    243
    244        throw new RuntimeException("JSONArray[" + index + "] is not a Boolean.");
    245    }
    246
    247    /**
    248     * Get the double value associated with an index.
    249     *
    250     * @param index
    251     *         The index must be between 0 and length() - 1.
    252     * @return The value.
    253     * @throws IllegalArgumentException
    254     *         If the key is not found or if the value cannot be converted to a number.
    255     */
    256    public double getDouble(int index)
    257    {
    258        Object value = get(index);
    259
    260        try
    261        {
    262            if (value instanceof Number)
    263                return ((Number) value).doubleValue();
    264
    265            return Double.valueOf((String) value);
    266        } catch (Exception e)
    267        {
    268            throw new IllegalArgumentException("JSONArray[" + index + "] is not a number.");
    269        }
    270    }
    271
    272    /**
    273     * Get the int value associated with an index.
    274     *
    275     * @param index
    276     *         The index must be between 0 and length() - 1.
    277     * @return The value.
    278     * @throws IllegalArgumentException
    279     *         If the key is not found or if the value cannot be converted to a number. if the
    280     *         value cannot be converted to a number.
    281     */
    282    public int getInt(int index)
    283    {
    284        Object o = get(index);
    285        return o instanceof Number ? ((Number) o).intValue() : (int) getDouble(index);
    286    }
    287
    288    /**
    289     * Get the JSONArray associated with an index.
    290     *
    291     * @param index
    292     *         The index must be between 0 and length() - 1.
    293     * @return A JSONArray value.
    294     * @throws RuntimeException
    295     *         If there is no value for the index. or if the value is not a JSONArray
    296     */
    297    public JSONArray getJSONArray(int index)
    298    {
    299        Object o = get(index);
    300        if (o instanceof JSONArray)
    301        {
    302            return (JSONArray) o;
    303        }
    304
    305        throw new RuntimeException("JSONArray[" + index + "] is not a JSONArray.");
    306    }
    307
    308    /**
    309     * Get the JSONObject associated with an index.
    310     *
    311     * @param index
    312     *         subscript
    313     * @return A JSONObject value.
    314     * @throws RuntimeException
    315     *         If there is no value for the index or if the value is not a JSONObject
    316     */
    317    public JSONObject getJSONObject(int index)
    318    {
    319        Object o = get(index);
    320        if (o instanceof JSONObject)
    321        {
    322            return (JSONObject) o;
    323        }
    324
    325        throw new RuntimeException("JSONArray[" + index + "] is not a JSONObject.");
    326    }
    327
    328    /**
    329     * Get the long value associated with an index.
    330     *
    331     * @param index
    332     *         The index must be between 0 and length() - 1.
    333     * @return The value.
    334     * @throws IllegalArgumentException
    335     *         If the key is not found or if the value cannot be converted to a number.
    336     */
    337    public long getLong(int index)
    338    {
    339        Object o = get(index);
    340        return o instanceof Number ? ((Number) o).longValue() : (long) getDouble(index);
    341    }
    342
    343    /**
    344     * Get the string associated with an index.
    345     *
    346     * @param index
    347     *         The index must be between 0 and length() - 1.
    348     * @return A string value.
    349     * @throws RuntimeException
    350     *         If there is no value for the index.
    351     */
    352    public String getString(int index)
    353    {
    354        return get(index).toString();
    355    }
    356
    357    /**
    358     * Determine if the value is null.
    359     *
    360     * @param index
    361     *         The index must be between 0 and length() - 1.
    362     * @return true if the value at the index is null, or if there is no value.
    363     */
    364    public boolean isNull(int index)
    365    {
    366        return get(index) == JSONObject.NULL;
    367    }
    368
    369    /**
    370     * Get the number of elements in the JSONArray, included nulls.
    371     *
    372     * @return The length (or size).
    373     */
    374    public int length()
    375    {
    376        return list.size();
    377    }
    378
    379    /**
    380     * Append an object value. This increases the array's length by one.
    381     *
    382     * @param value
    383     *         An object value. The value should be a Boolean, Double, Integer, JSONArray, JSONObject, JSONLiteral,
    384     *         Long, or String, or the JSONObject.NULL singleton.
    385     * @return this array
    386     */
    387    public JSONArray put(Object value)
    388    {
    389        // now testValidity checks for null values.
    390        // assert value != null;
    391
    392        JSONObject.testValidity(value);
    393
    394        list.add(value);
    395
    396        return this;
    397    }
    398
    399    /**
    400     * Put or replace an object value in the JSONArray. If the index is greater than the length of the JSONArray, then
    401     * null elements will be added as necessary to pad it out.
    402     *
    403     * @param index
    404     *         The subscript.
    405     * @param value
    406     *         The value to put into the array. The value should be a Boolean, Double, Integer, JSONArray,
    407     *         JSONObject, JSONString, Long, or String, or the JSONObject.NULL singeton.
    408     * @return this array
    409     * @throws RuntimeException
    410     *         If the index is negative or if the the value is an invalid number.
    411     */
    412    public JSONArray put(int index, Object value)
    413    {
    414        assert value != null;
    415
    416        if (index < 0)
    417        {
    418            throw new RuntimeException("JSONArray[" + index + "] not found.");
    419        }
    420
    421        JSONObject.testValidity(value);
    422
    423        if (index < length())
    424        {
    425            list.set(index, value);
    426        } else
    427        {
    428            while (index != length())
    429                list.add(JSONObject.NULL);
    430
    431            list.add(value);
    432        }
    433
    434        return this;
    435    }
    436
    437    @Override
    438    public boolean equals(Object obj)
    439    {
    440        if (obj == null)
    441            return false;
    442
    443        if (!(obj instanceof JSONArray))
    444            return false;
    445
    446        JSONArray other = (JSONArray) obj;
    447
    448        return list.equals(other.list);
    449    }
    450
    451    @Override
    452    void print(JSONPrintSession session)
    453    {
    454        session.printSymbol('[');
    455
    456        session.indent();
    457
    458        boolean comma = false;
    459
    460        for (Object value : list)
    461        {
    462            if (comma)
    463                session.printSymbol(',');
    464
    465            session.newline();
    466
    467            JSONObject.printValue(session, value);
    468
    469            comma = true;
    470        }
    471
    472        session.outdent();
    473
    474        if (comma)
    475            session.newline();
    476
    477        session.printSymbol(']');
    478    }
    479
    480    /**
    481     * Puts all objects from the collection into this JSONArray, using {@link #put(Object)}.
    482     *
    483     * @param collection
    484     *         List, array, JSONArray, or other iterable object, or null
    485     * @return this JSONArray
    486     * @since 5.4
    487     */
    488    public JSONArray putAll(Iterable<?> collection)
    489    {
    490        if (collection != null)
    491        {
    492            for (Object o : collection)
    493            {
    494                put(o);
    495            }
    496        }
    497
    498        return this;
    499    }
    500
    501    /**
    502     * Returns an unmodifiable list of the contents of the array. This is a wrapper around the list's internal
    503     * storage and is live (changes to the JSONArray affect the returned List).
    504     *
    505     * @return unmodifiable list of array contents
    506     * @since 5.4
    507     */
    508    public List<Object> toList()
    509    {
    510        return Collections.unmodifiableList(list);
    511    }
    512}