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    }