001/* Copyright 2016-2017 Clifton Labs
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 * http://www.apache.org/licenses/LICENSE-2.0
006 * Unless required by applicable law or agreed to in writing, software
007 * distributed under the License is distributed on an "AS IS" BASIS,
008 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
009 * See the License for the specific language governing permissions and
010 * limitations under the License. */
011package org.json.simple;
012
013import java.io.IOException;
014import java.io.StringWriter;
015import java.io.Writer;
016import java.math.BigDecimal;
017import java.util.ArrayList;
018import java.util.Collection;
019import java.util.Iterator;
020import java.util.Map;
021
022/** JsonArray is a common non-thread safe data format for a collection of data. The contents of a JsonArray are only
023 * validated as JSON values on serialization. Meaning all values added to a JsonArray must be recognized by the Jsoner
024 * for it to be a true 'JsonArray', so it is really a JsonableArrayList that will serialize to a JsonArray if all of
025 * its contents are valid JSON.
026 * @see Jsoner
027 * @since 2.0.0 */
028public class JsonArray extends ArrayList<Object> implements Jsonable{
029        /** The serialization version this class is compatible
030         * with. This value doesn't need to be incremented if and only if the only changes to occur were updating comments,
031         * updating javadocs, adding new
032         * fields to the class, changing the fields from static to non-static, or changing the fields from transient to non
033         * transient. All other changes require this number be incremented. */
034        private static final long serialVersionUID = 1L;
035
036        /** Instantiates an empty JsonArray. */
037        public JsonArray(){
038                super();
039        }
040
041        /** Instantiate a new JsonArray using ArrayList's constructor of the same type.
042         * @param collection represents the elements to produce the JsonArray with. */
043        public JsonArray(final Collection<?> collection){
044                super(collection);
045        }
046
047        /** A convenience method that assumes every element of the JsonArray is castable to T before adding it to a
048         * collection of Ts.
049         * @param <T> represents the type that all of the elements of the JsonArray should be cast to and the type the
050         *        collection will contain.
051         * @param destination represents where all of the elements of the JsonArray are added to after being cast to the
052         *        generic type
053         *        provided.
054         * @throws ClassCastException if the unchecked cast of an element to T fails. */
055        @SuppressWarnings("unchecked")
056        public <T> void asCollection(final Collection<T> destination){
057                for(final Object o : this){
058                        destination.add((T)o);
059                }
060        }
061
062        /** A convenience method that assumes there is a BigDecimal, Number, or String at the given index. If a Number or
063         * String is there it is used to construct a new BigDecimal.
064         * @param index representing where the value is expected to be at.
065         * @return the value stored at the key or the default provided if the key doesn't exist.
066         * @throws ClassCastException if there was a value but didn't match the assumed return types.
067         * @throws IndexOutOfBoundsException if the index is outside of the range of element indexes in the JsonArray.
068         * @throws NumberFormatException if a String isn't a valid representation of a BigDecimal.
069         * @see BigDecimal
070         * @see Number#doubleValue() */
071        public BigDecimal getBigDecimal(final int index){
072                Object returnable = this.get(index);
073                if(returnable instanceof BigDecimal){
074                        /* Success there was a BigDecimal. */
075                }else if(returnable instanceof Number){
076                        /* A number can be used to construct a BigDecimal. */
077                        returnable = new BigDecimal(returnable.toString());
078                }else if(returnable instanceof String){
079                        /* A number can be used to construct a BigDecimal. */
080                        returnable = new BigDecimal((String)returnable);
081                }
082                return (BigDecimal)returnable;
083        }
084
085        /** A convenience method that assumes there is a Boolean or String value at the given index.
086         * @param index represents where the value is expected to be at.
087         * @return the value at the index provided cast to a boolean.
088         * @throws ClassCastException if there was a value but didn't match the assumed return type.
089         * @throws IndexOutOfBoundsException if the index is outside of the range of element indexes in the JsonArray. */
090        public Boolean getBoolean(final int index){
091                Object returnable = this.get(index);
092                if(returnable instanceof String){
093                        returnable = Boolean.valueOf((String)returnable);
094                }
095                return (Boolean)returnable;
096        }
097
098        /** A convenience method that assumes there is a Number or String value at the given index.
099         * @param index represents where the value is expected to be at.
100         * @return the value at the index provided cast to a byte.
101         * @throws ClassCastException if there was a value but didn't match the assumed return type.
102         * @throws NumberFormatException if a String isn't a valid representation of a BigDecimal or if the Number
103         *         represents the double or float Infinity or NaN.
104         * @throws IndexOutOfBoundsException if the index is outside of the range of element indexes in the JsonArray.
105         * @see Number */
106        public Byte getByte(final int index){
107                Object returnable = this.get(index);
108                if(returnable == null){
109                        return null;
110                }
111                if(returnable instanceof String){
112                        /* A String can be used to construct a BigDecimal. */
113                        returnable = new BigDecimal((String)returnable);
114                }
115                return ((Number)returnable).byteValue();
116        }
117
118        /** A convenience method that assumes there is a Collection value at the given index.
119         * @param <T> the kind of collection to expect at the index. Note unless manually added, collection values will be a
120         *        JsonArray.
121         * @param index represents where the value is expected to be at.
122         * @return the value at the index provided cast to a Collection.
123         * @throws ClassCastException if there was a value but didn't match the assumed return type.
124         * @throws IndexOutOfBoundsException if the index is outside of the range of element indexes in the JsonArray.
125         * @see Collection */
126        @SuppressWarnings("unchecked")
127        public <T extends Collection<?>> T getCollection(final int index){
128                /* The unchecked warning is suppressed because there is no way of guaranteeing at compile time the cast will
129                 * work. */
130                return (T)this.get(index);
131        }
132
133        /** A convenience method that assumes there is a Number or String value at the given index.
134         * @param index represents where the value is expected to be at.
135         * @return the value at the index provided cast to a double.
136         * @throws ClassCastException if there was a value but didn't match the assumed return type.
137         * @throws NumberFormatException if a String isn't a valid representation of a BigDecimal or if the Number
138         *         represents the double or float Infinity or NaN.
139         * @throws IndexOutOfBoundsException if the index is outside of the range of element indexes in the JsonArray.
140         * @see Number */
141        public Double getDouble(final int index){
142                Object returnable = this.get(index);
143                if(returnable == null){
144                        return null;
145                }
146                if(returnable instanceof String){
147                        /* A String can be used to construct a BigDecimal. */
148                        returnable = new BigDecimal((String)returnable);
149                }
150                return ((Number)returnable).doubleValue();
151        }
152
153        /** A convenience method that assumes there is a String value at the given index representing a fully qualified name
154         * in dot notation of an enum.
155         * @param index representing where the value is expected to be at.
156         * @param <T> the Enum type the value at the index is expected to belong to.
157         * @return the enum based on the string found at the index, or null if the value at the index was null.
158         * @throws ClassNotFoundException if the element was a String but the declaring enum type couldn't be determined
159         *         with it.
160         * @throws ClassCastException if the element at the index was not a String or if the fully qualified enum name is of
161         *         the wrong type.
162         * @throws IllegalArgumentException if an enum type was dynamically determined but it doesn't define an enum with
163         *         the dynamically determined name.
164         * @throws IndexOutOfBoundsException if the index is outside of the range of element indexes in the JsonArray.
165         * @see Enum#valueOf(Class, String)
166         * @deprecated 2.3.0 Jsoner deprecated automatically serializing enums as Strings. */
167        @Deprecated
168        @SuppressWarnings("unchecked")
169        public <T extends Enum<T>> T getEnum(final int index) throws ClassNotFoundException{
170                /* Supressing the unchecked warning because the returnType is dynamically identified and could lead to a
171                 * ClassCastException when returnType is cast to Class<T>, which is expected by the method's contract. */
172                T returnable;
173                final String element;
174                final String[] splitValues;
175                final int numberOfValues;
176                final StringBuilder returnTypeName;
177                final StringBuilder enumName;
178                final Class<T> returnType;
179                /* Make sure the element at the index is a String. */
180                element = this.getString(index);
181                if(element == null){
182                        return null;
183                }
184                /* Get the package, class, and enum names. */
185                splitValues = element.split("\\.");
186                numberOfValues = splitValues.length;
187                returnTypeName = new StringBuilder();
188                enumName = new StringBuilder();
189                for(int i = 0; i < numberOfValues; i++){
190                        if(i == (numberOfValues - 1)){
191                                /* If it is the last split value then it should be the name of the Enum since dots are not allowed in
192                                 * enum names. */
193                                enumName.append(splitValues[i]);
194                        }else if(i == (numberOfValues - 2)){
195                                /* If it is the penultimate split value then it should be the end of the package/enum type and not need
196                                 * a dot appended to it. */
197                                returnTypeName.append(splitValues[i]);
198                        }else{
199                                /* Must be part of the package/enum type and will need a dot appended to it since they got removed in
200                                 * the split. */
201                                returnTypeName.append(splitValues[i]);
202                                returnTypeName.append(".");
203                        }
204                }
205                /* Use the package/class and enum names to get the Enum<T>. */
206                returnType = (Class<T>)Class.forName(returnTypeName.toString());
207                returnable = Enum.valueOf(returnType, enumName.toString());
208                return returnable;
209        }
210
211        /** A convenience method that assumes there is a Number or String value at the given index.
212         * @param index represents where the value is expected to be at.
213         * @return the value at the index provided cast to a float.
214         * @throws ClassCastException if there was a value but didn't match the assumed return type.
215         * @throws NumberFormatException if a String isn't a valid representation of a BigDecimal or if the Number
216         *         represents the double or float Infinity or NaN.
217         * @throws IndexOutOfBoundsException if the index is outside of the range of element indexes in the JsonArray.
218         * @see Number */
219        public Float getFloat(final int index){
220                Object returnable = this.get(index);
221                if(returnable == null){
222                        return null;
223                }
224                if(returnable instanceof String){
225                        /* A String can be used to construct a BigDecimal. */
226                        returnable = new BigDecimal((String)returnable);
227                }
228                return ((Number)returnable).floatValue();
229        }
230
231        /** A convenience method that assumes there is a Number or String value at the given index.
232         * @param index represents where the value is expected to be at.
233         * @return the value at the index provided cast to a int.
234         * @throws ClassCastException if there was a value but didn't match the assumed return type.
235         * @throws NumberFormatException if a String isn't a valid representation of a BigDecimal or if the Number
236         *         represents the double or float Infinity or NaN.
237         * @throws IndexOutOfBoundsException if the index is outside of the range of element indexes in the JsonArray.
238         * @see Number */
239        public Integer getInteger(final int index){
240                Object returnable = this.get(index);
241                if(returnable == null){
242                        return null;
243                }
244                if(returnable instanceof String){
245                        /* A String can be used to construct a BigDecimal. */
246                        returnable = new BigDecimal((String)returnable);
247                }
248                return ((Number)returnable).intValue();
249        }
250
251        /** A convenience method that assumes there is a Number or String value at the given index.
252         * @param index represents where the value is expected to be at.
253         * @return the value at the index provided cast to a long.
254         * @throws ClassCastException if there was a value but didn't match the assumed return type.
255         * @throws NumberFormatException if a String isn't a valid representation of a BigDecimal or if the Number
256         *         represents the double or float Infinity or NaN.
257         * @throws IndexOutOfBoundsException if the index is outside of the range of element indexes in the JsonArray.
258         * @see Number */
259        public Long getLong(final int index){
260                Object returnable = this.get(index);
261                if(returnable == null){
262                        return null;
263                }
264                if(returnable instanceof String){
265                        /* A String can be used to construct a BigDecimal. */
266                        returnable = new BigDecimal((String)returnable);
267                }
268                return ((Number)returnable).longValue();
269        }
270
271        /** A convenience method that assumes there is a Map value at the given index.
272         * @param <T> the kind of map to expect at the index. Note unless manually added, Map values will be a JsonObject.
273         * @param index represents where the value is expected to be at.
274         * @return the value at the index provided cast to a Map.
275         * @throws ClassCastException if there was a value but didn't match the assumed return type.
276         * @throws IndexOutOfBoundsException if the index is outside of the range of element indexes in the JsonArray.
277         * @see Map */
278        @SuppressWarnings("unchecked")
279        public <T extends Map<?, ?>> T getMap(final int index){
280                /* The unchecked warning is suppressed because there is no way of guaranteeing at compile time the cast will
281                 * work. */
282                return (T)this.get(index);
283        }
284
285        /** A convenience method that assumes there is a Number or String value at the given index.
286         * @param index represents where the value is expected to be at.
287         * @return the value at the index provided cast to a short.
288         * @throws ClassCastException if there was a value but didn't match the assumed return type.
289         * @throws NumberFormatException if a String isn't a valid representation of a BigDecimal or if the Number
290         *         represents the double or float Infinity or NaN.
291         * @throws IndexOutOfBoundsException if the index is outside of the range of element indexes in the JsonArray.
292         * @see Number */
293        public Short getShort(final int index){
294                Object returnable = this.get(index);
295                if(returnable == null){
296                        return null;
297                }
298                if(returnable instanceof String){
299                        /* A String can be used to construct a BigDecimal. */
300                        returnable = new BigDecimal((String)returnable);
301                }
302                return ((Number)returnable).shortValue();
303        }
304
305        /** A convenience method that assumes there is a Boolean, Number, or String value at the given index.
306         * @param index represents where the value is expected to be at.
307         * @return the value at the index provided cast to a String.
308         * @throws ClassCastException if there was a value but didn't match the assumed return type.
309         * @throws IndexOutOfBoundsException if the index is outside of the range of element indexes in the JsonArray. */
310        public String getString(final int index){
311                Object returnable = this.get(index);
312                if(returnable instanceof Boolean){
313                        returnable = returnable.toString();
314                }else if(returnable instanceof Number){
315                        returnable = returnable.toString();
316                }
317                return (String)returnable;
318        }
319
320        /* (non-Javadoc)
321         * @see org.json.simple.Jsonable#asJsonString() */
322        @Override
323        public String toJson(){
324                final StringWriter writable = new StringWriter();
325                try{
326                        this.toJson(writable);
327                }catch(final IOException caught){
328                        /* See java.io.StringWriter. */
329                }
330                return writable.toString();
331        }
332
333        /* (non-Javadoc)
334         * @see org.json.simple.Jsonable#toJsonString(java.io.Writer) */
335        @Override
336        public void toJson(final Writer writable) throws IOException{
337                boolean isFirstElement = true;
338                final Iterator<Object> elements = this.iterator();
339                writable.write('[');
340                while(elements.hasNext()){
341                        if(isFirstElement){
342                                isFirstElement = false;
343                        }else{
344                                writable.write(',');
345                        }
346                        writable.write(Jsoner.serialize(elements.next()));
347                }
348                writable.write(']');
349        }
350}