    001    // Copyright 2007, 2008, 2010, 2011 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
    015    package 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
    037    import java.util.ArrayList;
    038    import java.util.Iterator;
    039    import java.util.List;
    040
    041    /**
    042     * A JSONArray is an ordered sequence of values. Its external text form is a string wrapped in square brackets with
    043     * commas separating the values. The internal form is an object having {@code get} and {@code opt} methods for
    044     * accessing the values by index, and {@code put} methods for adding or replacing values. The values can be any of
    045     * these types: {@code Boolean}, {@code JSONArray}, {@code JSONObject}, {@code Number},
    046     * {@code String}, or the {@code JSONObject.NULL object}.
    047     * <p/>
    048     * The constructor can convert a JSON text into a Java object. The {@code toString} method converts to JSON text.
    049     * <p/>
    050     * A {@code get} method returns a value if one can be found, and throws an exception if one cannot be found. An
    051     * {@code opt} method returns a default value instead of throwing an exception, and so is useful for obtaining
    052     * optional values.
    053     * <p/>
    054     * The generic {@code get()} and {@code opt()} methods return an object which you can cast or query for type.
    055     * There are also typed {@code get} and {@code opt} methods that do type checking and type coersion for you.
    056     * <p/>
    057     * The texts produced by the {@code toString} methods strictly conform to JSON syntax rules. The constructors are
    058     * more forgiving in the texts they will accept:
    059     * <ul>
    060     * <li>An extra {@code ,}&nbsp;<small>(comma)</small> may appear just before the closing bracket.</li>
    061     * <li>The {@code null} value will be inserted when there is {@code ,}&nbsp;<small>(comma)</small> elision.</li>
    062     * <li>Strings may be quoted with {@code '}&nbsp;<small>(single quote)</small>.</li>
    063     * <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
    064     * contain leading or trailing spaces, and if they do not contain any of these characters:
    065     * {@code { } [ ] / \ : , = ; #} and if they do not look like numbers and if they are not the reserved words
    066     * {@code true}, {@code false}, or {@code null}.</li>
    067     * <li>Values can be separated by {@code ;} <small>(semicolon)</small> as well as by {@code ,}
    068     * <small>(comma)</small>.</li>
    069     * <li>Numbers may have the {@code 0-} <small>(octal)</small> or {@code 0x-} <small>(hex)</small> prefix.</li>
    070     * <li>Comments written in the slashshlash, slashstar, and hash conventions will be ignored.</li>
    071     * </ul>
    072     *
    073     * @author JSON.org
    074     * @version 2
    075     */
    076    public final class JSONArray extends JSONCollection implements Iterable<Object>
    077    {
    078
    079        /**
    080         * The arrayList where the JSONArray's properties are kept.
    081         */
    082        private final List<Object> list = new ArrayList<Object>();
    083
    084        /**
    085         * Construct an empty JSONArray.
    086         */
    087        public JSONArray()
    088        {
    089        }
    090
    091        public JSONArray(String text)
    092        {
    093            JSONTokener tokener = new JSONTokener(text);
    094
    095            parse(tokener);
    096        }
    097
    098        public JSONArray(Object... values)
    099        {
    100            for (Object value : values)
    101                put(value);
    102        }
    103
    104        public Iterator<Object> iterator()
    105        {
    106            return list.iterator();
    107        }
    108
    109        /**
    110         * Construct a JSONArray from a JSONTokener.
    111         *
    112         * @param tokenizer
    113         *            A JSONTokener
    114         * @throws RuntimeException
    115         *             If there is a syntax error.
    116         */
    117        JSONArray(JSONTokener tokenizer)
    118        {
    119            assert tokenizer != null;
    120
    121            parse(tokenizer);
    122        }
    123
    124        private void parse(JSONTokener tokenizer)
    125        {
    126            if (tokenizer.nextClean() != '[') { throw tokenizer.syntaxError("A JSONArray text must start with '['"); }
    127
    128            if (tokenizer.nextClean() == ']') { return; }
    129
    130            tokenizer.back();
    131
    132            while (true)
    133            {
    134                if (tokenizer.nextClean() == ',')
    135                {
    136                    tokenizer.back();
    137                    list.add(JSONObject.NULL);
    138                }
    139                else
    140                {
    141                    tokenizer.back();
    142                    list.add(tokenizer.nextValue());
    143                }
    144
    145                switch (tokenizer.nextClean())
    146                {
    147                    case ';':
    148                    case ',':
    149                        if (tokenizer.nextClean() == ']') { return; }
    150                        tokenizer.back();
    151                        break;
    152
    153                    case ']':
    154                        return;
    155
    156                    default:
    157                        throw tokenizer.syntaxError("Expected a ',' or ']'");
    158                }
    159            }
    160        }
    161
    162        /**
    163         * Get the object value associated with an index.
    164         *
    165         * @param index
    166         *            The index must be between 0 and length() - 1.
    167         * @return An object value.
    168         * @throws RuntimeException
    169         *             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         * Remove the object associated with the index.
    178         *
    179         * @param index
    180         *            The index must be between 0 and length() - 1.
    181         * @return An object removed.
    182         * @throws RuntimeException
    183         *             If there is no value for the index.
    184         */
    185        public Object remove(int index)
    186        {
    187            return list.remove(index);
    188        }
    189
    190        /**
    191         * Get the boolean value associated with an index. The string values "true" and "false" are converted to boolean.
    192         *
    193         * @param index
    194         *            The index must be between 0 and length() - 1.
    195         * @return The truth.
    196         * @throws RuntimeException
    197         *             If there is no value for the index or if the value is not convertable to boolean.
    198         */
    199        public boolean getBoolean(int index)
    200        {
    201            Object value = get(index);
    202
    203            if (value instanceof Boolean) { return (Boolean) value; }
    204
    205            if (value instanceof String)
    206            {
    207                String asString = (String) value;
    208
    209                if (asString.equalsIgnoreCase("false"))
    210                    return false;
    211
    212                if (asString.equalsIgnoreCase("true"))
    213                    return true;
    214            }
    215
    216            throw new RuntimeException("JSONArray[" + index + "] is not a Boolean.");
    217        }
    218
    219        /**
    220         * Get the double value associated with an index.
    221         *
    222         * @param index
    223         *            The index must be between 0 and length() - 1.
    224         * @return The value.
    225         * @throws IllegalArgumentException
    226         *             If the key is not found or if the value cannot be converted to a number.
    227         */
    228        public double getDouble(int index)
    229        {
    230            Object value = get(index);
    231
    232            try
    233            {
    234                if (value instanceof Number)
    235                    return ((Number) value).doubleValue();
    236
    237                return Double.valueOf((String) value);
    238            }
    239            catch (Exception e)
    240            {
    241                throw new IllegalArgumentException("JSONArray[" + index + "] is not a number.");
    242            }
    243        }
    244
    245        /**
    246         * Get the int 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. if the
    253         *             value cannot be converted to a number.
    254         */
    255        public int getInt(int index)
    256        {
    257            Object o = get(index);
    258            return o instanceof Number ? ((Number) o).intValue() : (int) getDouble(index);
    259        }
    260
    261        /**
    262         * Get the JSONArray associated with an index.
    263         *
    264         * @param index
    265         *            The index must be between 0 and length() - 1.
    266         * @return A JSONArray value.
    267         * @throws RuntimeException
    268         *             If there is no value for the index. or if the value is not a JSONArray
    269         */
    270        public JSONArray getJSONArray(int index)
    271        {
    272            Object o = get(index);
    273            if (o instanceof JSONArray) { return (JSONArray) o; }
    274
    275            throw new RuntimeException("JSONArray[" + index + "] is not a JSONArray.");
    276        }
    277
    278        /**
    279         * Get the JSONObject associated with an index.
    280         *
    281         * @param index
    282         *            subscript
    283         * @return A JSONObject value.
    284         * @throws RuntimeException
    285         *             If there is no value for the index or if the value is not a JSONObject
    286         */
    287        public JSONObject getJSONObject(int index)
    288        {
    289            Object o = get(index);
    290            if (o instanceof JSONObject) { return (JSONObject) o; }
    291
    292            throw new RuntimeException("JSONArray[" + index + "] is not a JSONObject.");
    293        }
    294
    295        /**
    296         * Get the long value associated with an index.
    297         *
    298         * @param index
    299         *            The index must be between 0 and length() - 1.
    300         * @return The value.
    301         * @throws IllegalArgumentException
    302         *             If the key is not found or if the value cannot be converted to a number.
    303         */
    304        public long getLong(int index)
    305        {
    306            Object o = get(index);
    307            return o instanceof Number ? ((Number) o).longValue() : (long) getDouble(index);
    308        }
    309
    310        /**
    311         * Get the string associated with an index.
    312         *
    313         * @param index
    314         *            The index must be between 0 and length() - 1.
    315         * @return A string value.
    316         * @throws RuntimeException
    317         *             If there is no value for the index.
    318         */
    319        public String getString(int index)
    320        {
    321            return get(index).toString();
    322        }
    323
    324        /**
    325         * Determine if the value is null.
    326         *
    327         * @param index
    328         *            The index must be between 0 and length() - 1.
    329         * @return true if the value at the index is null, or if there is no value.
    330         */
    331        public boolean isNull(int index)
    332        {
    333            return get(index) == JSONObject.NULL;
    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
    350         *            An object value. The value should be a Boolean, Double, Integer, JSONArray, JSONObject, JSONLiteral,
    351         *            Long, or String, or the JSONObject.NULL singleton.
    352         * @return this array
    353         */
    354        public JSONArray put(Object value)
    355        {
    356            assert value != null;
    357
    358            JSONObject.testValidity(value);
    359
    360            list.add(value);
    361
    362            return this;
    363        }
    364
    365        /**
    366         * Put or replace an object value in the JSONArray. If the index is greater than the length of the JSONArray, then
    367         * null elements will be added as necessary to pad it out.
    368         *
    369         * @param index
    370         *            The subscript.
    371         * @param value
    372         *            The value to put into the array. The value should be a Boolean, Double, Integer, JSONArray,
    373         *            JSONObject, JSONString, Long, or String, or the JSONObject.NULL singeton.
    374         * @return this array
    375         * @throws RuntimeException
    376         *             If the index is negative or if the the value is an invalid number.
    377         */
    378        public JSONArray put(int index, Object value)
    379        {
    380            assert value != null;
    381
    382            if (index < 0) { throw new RuntimeException("JSONArray[" + index + "] not found."); }
    383
    384            JSONObject.testValidity(value);
    385
    386            if (index < length())
    387            {
    388                list.set(index, value);
    389            }
    390            else
    391            {
    392                while (index != length())
    393                    list.add(JSONObject.NULL);
    394
    395                list.add(value);
    396            }
    397
    398            return this;
    399        }
    400
    401        /** Used for testing. */
    402        Object[] toArray()
    403        {
    404            return list.toArray();
    405        }
    406
    407        @Override
    408        public boolean equals(Object obj)
    409        {
    410            if (obj == null)
    411                return false;
    412
    413            if (!(obj instanceof JSONArray))
    414                return false;
    415
    416            JSONArray other = (JSONArray) obj;
    417
    418            return list.equals(other.list);
    419        }
    420
    421        void print(JSONPrintSession session)
    422        {
    423            session.printSymbol('[');
    424
    425            session.indent();
    426
    427            boolean comma = false;
    428
    429            for (Object value : list)
    430            {
    431                if (comma)
    432                    session.printSymbol(',');
    433
    434                session.newline();
    435
    436                JSONObject.printValue(session, value);
    437
    438                comma = true;
    439            }
    440
    441            session.outdent();
    442
    443            if (comma)
    444                session.newline();
    445
    446            session.printSymbol(']');
    447        }
    448    }