001    /*
002     * Copyright 2015 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2015 UnboundID Corp.
007     *
008     * This program is free software; you can redistribute it and/or modify
009     * it under the terms of the GNU General Public License (GPLv2 only)
010     * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011     * as published by the Free Software Foundation.
012     *
013     * This program is distributed in the hope that it will be useful,
014     * but WITHOUT ANY WARRANTY; without even the implied warranty of
015     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016     * GNU General Public License for more details.
017     *
018     * You should have received a copy of the GNU General Public License
019     * along with this program; if not, see <http://www.gnu.org/licenses>.
020     */
021    package com.unboundid.util.json;
022    
023    
024    
025    import java.util.ArrayList;
026    import java.util.Arrays;
027    import java.util.Collections;
028    import java.util.Iterator;
029    import java.util.List;
030    
031    import com.unboundid.util.NotMutable;
032    import com.unboundid.util.ThreadSafety;
033    import com.unboundid.util.ThreadSafetyLevel;
034    
035    
036    
037    /**
038     * This class provides an implementation of a JSON value that represents an
039     * ordered collection of zero or more values.  An array can contain elements of
040     * any type, including a mix of types, and including nested arrays.  The same
041     * value may appear multiple times in an array.
042     * <BR><BR>
043     * The string representation of a JSON array is an open square bracket (U+005B)
044     * followed by a comma-delimited list of the string representations of the
045     * values in that array and a closing square bracket (U+005D).  There must not
046     * be a comma between the last item in the array and the closing square bracket.
047     * There may optionally be any amount of whitespace (where whitespace characters
048     * include the ASCII space, horizontal tab, line feed, and carriage return
049     * characters) after the open square bracket, on either or both sides of commas
050     * separating values, and before the close square bracket.
051     * <BR><BR>
052     * The string representation returned by the {@link #toString()} method (or
053     * appended to the buffer provided to the {@link #toString(StringBuilder)}
054     * method) will include one space before each value in the array and one space
055     * before the closing square bracket.  There will not be any space between a
056     * value and the comma that follows it.  The string representation of each value
057     * in the array will be obtained using that value's {@code toString} method.
058     * <BR><BR>
059     * The normalized string representation will not include any optional spaces,
060     * and the normalized string representation of each value in the array will be
061     * obtained using that value's {@code toNormalizedString} method.
062     */
063    @NotMutable()
064    @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
065    public final class JSONArray
066           extends JSONValue
067    {
068      /**
069       * A pre-allocated empty JSON array.
070       */
071      public static final JSONArray EMPTY_ARRAY = new JSONArray();
072    
073    
074    
075      /**
076       * The serial version UID for this serializable class.
077       */
078      private static final long serialVersionUID = -5493008945333225318L;
079    
080    
081    
082      // The hash code for this JSON array.
083      private Integer hashCode;
084    
085      // The list of values for this array.
086      private final List<JSONValue> values;
087    
088      // The string representation for this JSON array.
089      private String stringRepresentation;
090    
091    
092    
093      /**
094       * Creates a new JSON array with the provided values.
095       *
096       * @param  values  The set of values to include in this JSON array.  It may be
097       *                 {@code null} or empty to indicate that the array should be
098       *                 empty.
099       */
100      public JSONArray(final JSONValue... values)
101      {
102        this((values == null) ? null : Arrays.asList(values));
103      }
104    
105    
106    
107      /**
108       * Creates a new JSON array with the provided values.
109       *
110       * @param  values  The set of values to include in this JSON array.  It may be
111       *                 {@code null} or empty to indicate that the array should be
112       *                 empty.
113       */
114      public JSONArray(final List<? extends JSONValue> values)
115      {
116        if (values == null)
117        {
118          this.values = Collections.emptyList();
119        }
120        else
121        {
122          this.values =
123               Collections.unmodifiableList(new ArrayList<JSONValue>(values));
124        }
125    
126        hashCode = null;
127        stringRepresentation = null;
128      }
129    
130    
131    
132      /**
133       * Retrieves the set of values contained in this JSON array.
134       *
135       * @return  The set of values contained in this JSON array.
136       */
137      public List<JSONValue> getValues()
138      {
139        return values;
140      }
141    
142    
143    
144      /**
145       * Indicates whether this array is empty.
146       *
147       * @return  {@code true} if this array does not contain any values, or
148       *          {@code false} if this array contains at least one value.
149       */
150      public boolean isEmpty()
151      {
152        return values.isEmpty();
153      }
154    
155    
156    
157      /**
158       * Retrieves the number of values contained in this array.
159       *
160       * @return  The number of values contained in this array.
161       */
162      public int size()
163      {
164        return values.size();
165      }
166    
167    
168    
169      /**
170       * {@inheritDoc}
171       */
172      @Override()
173      public int hashCode()
174      {
175        if (hashCode == null)
176        {
177          int hc = 0;
178          for (final JSONValue v : values)
179          {
180            hc = (hc * 31) + v.hashCode();
181          }
182    
183          hashCode = hc;
184        }
185    
186        return hashCode;
187      }
188    
189    
190    
191      /**
192       * {@inheritDoc}
193       */
194      @Override()
195      public boolean equals(final Object o)
196      {
197        if (o == this)
198        {
199          return true;
200        }
201    
202        if (o instanceof JSONArray)
203        {
204          final JSONArray a = (JSONArray) o;
205          return values.equals(a.values);
206        }
207    
208        return false;
209      }
210    
211    
212    
213      /**
214       * Indicates whether this JSON array is considered equivalent to the provided
215       * array, subject to the specified constraints.
216       *
217       * @param  array                The array for which to make the determination.
218       * @param  ignoreFieldNameCase  Indicates whether to ignore differences in
219       *                              capitalization in field names for any JSON
220       *                              objects contained in the array.
221       * @param  ignoreValueCase      Indicates whether to ignore differences in
222       *                              capitalization for array elements that are
223       *                              JSON strings, as well as for the string values
224       *                              of any JSON objects and arrays contained in
225       *                              the array.
226       * @param  ignoreArrayOrder     Indicates whether to ignore differences in the
227       *                              order of elements contained in the array.
228       *
229       * @return  {@code true} if this JSON array is considered equivalent to the
230       *          provided array (subject to the specified constraints), or
231       *          {@code false} if not.
232       */
233      public boolean equals(final JSONArray array,
234                            final boolean ignoreFieldNameCase,
235                            final boolean ignoreValueCase,
236                            final boolean ignoreArrayOrder)
237      {
238        // See if we can do a straight-up List.equals.  If so, just do that.
239        if ((! ignoreFieldNameCase) && (! ignoreValueCase) && (! ignoreArrayOrder))
240        {
241          return values.equals(array.values);
242        }
243    
244        // Make sure the arrays have the same number of elements.
245        if (values.size() != array.values.size())
246        {
247          return false;
248        }
249    
250        // Optimize for the case in which the order of values is significant.
251        if (! ignoreArrayOrder)
252        {
253          final Iterator<JSONValue> thisIterator = values.iterator();
254          final Iterator<JSONValue> thatIterator = array.values.iterator();
255          while (thisIterator.hasNext())
256          {
257            final JSONValue thisValue = thisIterator.next();
258            final JSONValue thatValue = thatIterator.next();
259            if (! thisValue.equals(thatValue, ignoreFieldNameCase, ignoreValueCase,
260                 ignoreArrayOrder))
261            {
262              return false;
263            }
264          }
265    
266          return true;
267        }
268    
269    
270        // If we've gotten here, then we know that we don't care about the order.
271        // Create a new list that we can remove values from as we find matches.
272        // This is important because arrays can have duplicate values, and we don't
273        // want to keep matching the same element.
274        final ArrayList<JSONValue> thatValues =
275             new ArrayList<JSONValue>(array.values);
276        final Iterator<JSONValue> thisIterator = values.iterator();
277        while (thisIterator.hasNext())
278        {
279          final JSONValue thisValue = thisIterator.next();
280    
281          boolean found = false;
282          final Iterator<JSONValue> thatIterator = thatValues.iterator();
283          while (thatIterator.hasNext())
284          {
285            final JSONValue thatValue = thatIterator.next();
286            if (thisValue.equals(thatValue, ignoreFieldNameCase, ignoreValueCase,
287                 ignoreArrayOrder))
288            {
289              found = true;
290              thatIterator.remove();
291              break;
292            }
293          }
294    
295          if (! found)
296          {
297            return false;
298          }
299        }
300    
301        return true;
302      }
303    
304    
305    
306      /**
307       * {@inheritDoc}
308       */
309      @Override()
310      public boolean equals(final JSONValue v, final boolean ignoreFieldNameCase,
311                            final boolean ignoreValueCase,
312                            final boolean ignoreArrayOrder)
313      {
314        return ((v instanceof JSONArray) &&
315             equals((JSONArray) v, ignoreFieldNameCase, ignoreValueCase,
316                  ignoreArrayOrder));
317      }
318    
319    
320    
321      /**
322       * Indicates whether this JSON array contains an element with the specified
323       * value.
324       *
325       * @param  value                The value for which to make the determination.
326       * @param  ignoreFieldNameCase  Indicates whether to ignore differences in
327       *                              capitalization in field names for any JSON
328       *                              objects contained in the array.
329       * @param  ignoreValueCase      Indicates whether to ignore differences in
330       *                              capitalization for array elements that are
331       *                              JSON strings, as well as for the string values
332       *                              of any JSON objects and arrays contained in
333       *                              the array.
334       * @param  ignoreArrayOrder     Indicates whether to ignore differences in the
335       *                              order of elements contained in arrays.  This
336       *                              is only applicable if the provided value is
337       *                              itself an array or is a JSON object that
338       *                              contains values that are arrays.
339       * @param  recursive            Indicates whether to recursively look into any
340       *                              arrays contained inside this array.
341       *
342       * @return  {@code true} if this JSON array contains an element with the
343       *          specified value, or {@code false} if not.
344       */
345      public boolean contains(final JSONValue value,
346                              final boolean ignoreFieldNameCase,
347                              final boolean ignoreValueCase,
348                              final boolean ignoreArrayOrder,
349                              final boolean recursive)
350      {
351        for (final JSONValue v : values)
352        {
353          if (v.equals(value, ignoreFieldNameCase, ignoreValueCase,
354               ignoreArrayOrder))
355          {
356            return true;
357          }
358    
359          if (recursive && (v instanceof JSONArray) &&
360              ((JSONArray) v).contains(value, ignoreFieldNameCase, ignoreValueCase,
361                   ignoreArrayOrder, recursive))
362          {
363            return true;
364          }
365        }
366    
367        return false;
368      }
369    
370    
371    
372      /**
373       * {@inheritDoc}
374       */
375      @Override()
376      public String toString()
377      {
378        if (stringRepresentation == null)
379        {
380          final StringBuilder buffer = new StringBuilder();
381          toString(buffer);
382          stringRepresentation = buffer.toString();
383        }
384    
385        return stringRepresentation;
386      }
387    
388    
389    
390      /**
391       * {@inheritDoc}
392       */
393      @Override()
394      public void toString(final StringBuilder buffer)
395      {
396        if (stringRepresentation != null)
397        {
398          buffer.append(stringRepresentation);
399          return;
400        }
401    
402        buffer.append("[ ");
403    
404        final Iterator<JSONValue> iterator = values.iterator();
405        while (iterator.hasNext())
406        {
407          iterator.next().toString(buffer);
408          if (iterator.hasNext())
409          {
410            buffer.append(',');
411          }
412          buffer.append(' ');
413        }
414    
415        buffer.append(']');
416      }
417    
418    
419    
420      /**
421       * {@inheritDoc}
422       */
423      @Override()
424      public String toNormalizedString()
425      {
426        final StringBuilder buffer = new StringBuilder();
427        toNormalizedString(buffer);
428        return buffer.toString();
429      }
430    
431    
432    
433      /**
434       * {@inheritDoc}
435       */
436      @Override()
437      public void toNormalizedString(final StringBuilder buffer)
438      {
439        buffer.append('[');
440    
441        final Iterator<JSONValue> iterator = values.iterator();
442        while (iterator.hasNext())
443        {
444          iterator.next().toNormalizedString(buffer);
445          if (iterator.hasNext())
446          {
447            buffer.append(',');
448          }
449        }
450    
451        buffer.append(']');
452      }
453    }