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
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.Iterator;
039import 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 */
076public 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}