001    // Copyright 2007, 2008 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    
020     Permission is hereby granted, free of charge, to any person obtaining a copy
021     of this software and associated documentation files (the "Software"), to deal
022     in the Software without restriction, including without limitation the rights
023     to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
024     copies of the Software, and to permit persons to whom the Software is
025     furnished to do so, subject to the following conditions:
026    
027     The above copyright notice and this permission notice shall be included in all
028     copies or substantial portions of the Software.
029    
030     The Software shall be used for Good, not Evil.
031    
032     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
033     IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
034     FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
035     AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
036     LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
037     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
038     SOFTWARE.
039     */
040    
041    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
042    
043    import java.util.List;
044    
045    /**
046     * A JSONArray is an ordered sequence of values. Its external text form is a string wrapped in square brackets with
047     * commas separating the values. The internal form is an object having <code>get</code> and <code>opt</code> methods for
048     * accessing the values by index, and <code>put</code> methods for adding or replacing values. The values can be any of
049     * these types: <code>Boolean</code>, <code>JSONArray</code>, <code>JSONObject</code>, <code>Number</code>,
050     * <code>String</code>, or the <code>JSONObject.NULL object</code>.
051     * <p/>
052     * The constructor can convert a JSON text into a Java object. The <code>toString</code> method converts to JSON text.
053     * <p/>
054     * A <code>get</code> method returns a value if one can be found, and throws an exception if one cannot be found. An
055     * <code>opt</code> method returns a default value instead of throwing an exception, and so is useful for obtaining
056     * optional values.
057     * <p/>
058     * The generic <code>get()</code> and <code>opt()</code> methods return an object which you can cast or query for type.
059     * There are also typed <code>get</code> and <code>opt</code> methods that do type checking and type coersion for you.
060     * <p/>
061     * The texts produced by the <code>toString</code> methods strictly conform to JSON syntax rules. The constructors are
062     * more forgiving in the texts they will accept: <ul> <li>An extra <code>,</code>&nbsp;<small>(comma)</small> may appear
063     * just before the closing bracket.</li> <li>The <code>null</code> value will be inserted when there is
064     * <code>,</code>&nbsp;<small>(comma)</small> elision.</li> <li>Strings may be quoted with
065     * <code>'</code>&nbsp;<small>(single quote)</small>.</li> <li>Strings do not need to be quoted at all if they do not
066     * begin with a quote or single quote, and if they do not contain leading or trailing spaces, and if they do not contain
067     * any of these characters: <code>{ } [ ] / \ : , = ; #</code> and if they do not look like numbers and if they are not
068     * the reserved words <code>true</code>, <code>false</code>, or <code>null</code>.</li> <li>Values can be separated by
069     * <code>;</code> <small>(semicolon)</small> as well as by <code>,</code> <small>(comma)</small>.</li> <li>Numbers may
070     * have the <code>0-</code> <small>(octal)</small> or <code>0x-</code> <small>(hex)</small> prefix.</li> <li>Comments
071     * written in the slashshlash, slashstar, and hash conventions will be ignored.</li> </ul>
072     *
073     * @author JSON.org
074     * @version 2
075     */
076    public final class JSONArray
077    {
078    
079        /**
080         * The arrayList where the JSONArray's properties are kept.
081         */
082        private final List<Object> list = CollectionFactory.newList();
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) put(value);
101        }
102    
103        /**
104         * Construct a JSONArray from a JSONTokener.
105         *
106         * @param tokenizer A JSONTokener
107         * @throws RuntimeException If there is a syntax error.
108         */
109        JSONArray(JSONTokener tokenizer)
110        {
111            assert tokenizer != null;
112    
113            parse(tokenizer);
114        }
115    
116        private void parse(JSONTokener tokenizer)
117        {
118            if (tokenizer.nextClean() != '[')
119            {
120                throw tokenizer
121                        .syntaxError("A JSONArray text must start with '['");
122            }
123    
124            if (tokenizer.nextClean() == ']')
125            {
126                return;
127            }
128    
129            tokenizer.back();
130    
131            while (true)
132            {
133                if (tokenizer.nextClean() == ',')
134                {
135                    tokenizer.back();
136                    list.add(JSONObject.NULL);
137                }
138                else
139                {
140                    tokenizer.back();
141                    list.add(tokenizer.nextValue());
142                }
143    
144                switch (tokenizer.nextClean())
145                {
146                    case ';':
147                    case ',':
148                        if (tokenizer.nextClean() == ']')
149                        {
150                            return;
151                        }
152                        tokenizer.back();
153                        break;
154    
155                    case ']':
156                        return;
157    
158                    default:
159                        throw tokenizer.syntaxError("Expected a ',' or ']'");
160                }
161            }
162        }
163    
164        /**
165         * Get the object value associated with an index.
166         *
167         * @param index The index must be between 0 and length() - 1.
168         * @return An object value.
169         * @throws RuntimeException 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         * Get the boolean value associated with an index. The string values "true" and "false" are converted to boolean.
178         *
179         * @param index The index must be between 0 and length() - 1.
180         * @return The truth.
181         * @throws RuntimeException If there is no value for the index or if the value is not convertable to boolean.
182         */
183        public boolean getBoolean(int index)
184        {
185            Object value = get(index);
186    
187            if (value instanceof Boolean)
188            {
189                return (Boolean) value;
190            }
191    
192            if (value instanceof String)
193            {
194                String asString = (String) value;
195    
196                if (asString.equalsIgnoreCase("false")) return false;
197    
198                if (asString.equalsIgnoreCase("true")) return true;
199            }
200    
201            throw new RuntimeException("JSONArray[" + index + "] is not a Boolean.");
202        }
203    
204        /**
205         * Get the double value associated with an index.
206         *
207         * @param index The index must be between 0 and length() - 1.
208         * @return The value.
209         * @throws IllegalArgumentException If the key is not found or if the value cannot be converted to a number.
210         */
211        public double getDouble(int index)
212        {
213            Object value = get(index);
214    
215            try
216            {
217                if (value instanceof Number) return ((Number) value).doubleValue();
218    
219                return Double.valueOf((String) value);
220            }
221            catch (Exception e)
222            {
223                throw new IllegalArgumentException("JSONArray[" + index + "] is not a number.");
224            }
225        }
226    
227        /**
228         * Get the int value associated with an index.
229         *
230         * @param index The index must be between 0 and length() - 1.
231         * @return The value.
232         * @throws IllegalArgumentException If the key is not found or if the value cannot be converted to a number. if the
233         *                                  value cannot be converted to a number.
234         */
235        public int getInt(int index)
236        {
237            Object o = get(index);
238            return o instanceof Number ? ((Number) o).intValue() : (int) getDouble(index);
239        }
240    
241        /**
242         * Get the JSONArray associated with an index.
243         *
244         * @param index The index must be between 0 and length() - 1.
245         * @return A JSONArray value.
246         * @throws RuntimeException If there is no value for the index. or if the value is not a JSONArray
247         */
248        public JSONArray getJSONArray(int index)
249        {
250            Object o = get(index);
251            if (o instanceof JSONArray)
252            {
253                return (JSONArray) o;
254            }
255    
256            throw new RuntimeException("JSONArray[" + index + "] is not a JSONArray.");
257        }
258    
259        /**
260         * Get the JSONObject associated with an index.
261         *
262         * @param index subscript
263         * @return A JSONObject value.
264         * @throws RuntimeException If there is no value for the index or if the value is not a JSONObject
265         */
266        public JSONObject getJSONObject(int index)
267        {
268            Object o = get(index);
269            if (o instanceof JSONObject)
270            {
271                return (JSONObject) o;
272            }
273    
274            throw new RuntimeException("JSONArray[" + index + "] is not a JSONObject.");
275        }
276    
277        /**
278         * Get the long value associated with an index.
279         *
280         * @param index The index must be between 0 and length() - 1.
281         * @return The value.
282         * @throws IllegalArgumentException If the key is not found or if the value cannot be converted to a number.
283         */
284        public long getLong(int index)
285        {
286            Object o = get(index);
287            return o instanceof Number ? ((Number) o).longValue() : (long) getDouble(index);
288        }
289    
290        /**
291         * Get the string associated with an index.
292         *
293         * @param index The index must be between 0 and length() - 1.
294         * @return A string value.
295         * @throws RuntimeException If there is no value for the index.
296         */
297        public String getString(int index)
298        {
299            return get(index).toString();
300        }
301    
302        /**
303         * Determine if the value is null.
304         *
305         * @param index The index must be between 0 and length() - 1.
306         * @return true if the value at the index is null, or if there is no value.
307         */
308        public boolean isNull(int index)
309        {
310            return get(index) == JSONObject.NULL;
311        }
312    
313        /**
314         * Make a string from the contents of this JSONArray. The <code>separator</code> string is inserted between each
315         * element. Warning: This method assumes that the data structure is acyclical.
316         *
317         * @param separator A string that will be inserted between the elements.
318         * @return a string.
319         * @throws RuntimeException If the array contains an invalid number.
320         */
321        public String join(String separator)
322        {
323            int len = length();
324            StringBuilder buffer = new StringBuilder();
325    
326            for (int i = 0; i < len; i += 1)
327            {
328                if (i > 0) buffer.append(separator);
329    
330                buffer.append(JSONObject.valueToString(list.get(i)));
331            }
332    
333            return buffer.toString();
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 An object value. The value should be a Boolean, Double, Integer, JSONArray, JSONObject, JSONLiteral,
350         *              Long, or String, or the JSONObject.NULL singleton.
351         * @return
352         */
353        public JSONArray put(Object value)
354        {
355            assert value != null;
356    
357            JSONObject.testValidity(value);
358    
359            list.add(value);
360    
361            return this;
362        }
363    
364        /**
365         * Put or replace an object value in the JSONArray. If the index is greater than the length of the JSONArray, then
366         * null elements will be added as necessary to pad it out.
367         *
368         * @param index The subscript.
369         * @param value The value to put into the array. The value should be a Boolean, Double, Integer, JSONArray,
370         *              JSONObject, JSONString, Long, or String, or the JSONObject.NULL singeton.
371         * @return
372         * @throws RuntimeException If the index is negative or if the the value is an invalid number.
373         */
374        public JSONArray put(int index, Object value)
375        {
376            assert value != null;
377    
378            if (index < 0)
379            {
380                throw new RuntimeException("JSONArray[" + index + "] not found.");
381            }
382    
383            JSONObject.testValidity(value);
384    
385            if (index < length())
386            {
387                list.set(index, value);
388            }
389            else
390            {
391                while (index != length()) list.add(JSONObject.NULL);
392    
393                list.add(value);
394            }
395    
396            return this;
397        }
398    
399        /**
400         * Make a JSON text of this JSONArray. For compactness, no unnecessary whitespace is added. If it is not possible to
401         * produce a syntactically correct JSON text then null will be returned instead. This could occur if the array
402         * contains an invalid number.
403         * <p/>
404         * Warning: This method assumes that the data structure is acyclical.
405         *
406         * @return a printable, displayable, transmittable representation of the array.
407         */
408        @Override
409        public String toString()
410        {
411            try
412            {
413                return '[' + join(",") + ']';
414            }
415            catch (Exception e)
416            {
417                return null;
418            }
419        }
420    
421        Object[] toArray()
422        {
423            return list.toArray();
424        }
425    
426        @Override
427        public boolean equals(Object obj)
428        {
429            if (obj == null) return false;
430    
431            if (!(obj instanceof JSONArray)) return false;
432    
433            JSONArray other = (JSONArray) obj;
434    
435            return list.equals(other.list);
436        }
437    }