    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}