001/*
002 * Copyright 2015-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2015-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2015-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.util.json;
037
038
039
040import java.util.ArrayList;
041import java.util.Arrays;
042import java.util.Collections;
043import java.util.Iterator;
044import java.util.List;
045
046import com.unboundid.util.NotMutable;
047import com.unboundid.util.NotNull;
048import com.unboundid.util.Nullable;
049import com.unboundid.util.ThreadSafety;
050import com.unboundid.util.ThreadSafetyLevel;
051
052
053
054/**
055 * This class provides an implementation of a JSON value that represents an
056 * ordered collection of zero or more values.  An array can contain elements of
057 * any type, including a mix of types, and including nested arrays.  The same
058 * value may appear multiple times in an array.
059 * <BR><BR>
060 * The string representation of a JSON array is an open square bracket (U+005B)
061 * followed by a comma-delimited list of the string representations of the
062 * values in that array and a closing square bracket (U+005D).  There must not
063 * be a comma between the last item in the array and the closing square bracket.
064 * There may optionally be any amount of whitespace (where whitespace characters
065 * include the ASCII space, horizontal tab, line feed, and carriage return
066 * characters) after the open square bracket, on either or both sides of commas
067 * separating values, and before the close square bracket.
068 * <BR><BR>
069 * The string representation returned by the {@link #toString()} method (or
070 * appended to the buffer provided to the {@link #toString(StringBuilder)}
071 * method) will include one space before each value in the array and one space
072 * before the closing square bracket.  There will not be any space between a
073 * value and the comma that follows it.  The string representation of each value
074 * in the array will be obtained using that value's {@code toString} method.
075 * <BR><BR>
076 * The normalized string representation will not include any optional spaces,
077 * and the normalized string representation of each value in the array will be
078 * obtained using that value's {@code toNormalizedString} method.
079 */
080@NotMutable()
081@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
082public final class JSONArray
083       extends JSONValue
084{
085  /**
086   * A pre-allocated empty JSON array.
087   */
088  @NotNull public static final JSONArray EMPTY_ARRAY = new JSONArray();
089
090
091
092  /**
093   * The serial version UID for this serializable class.
094   */
095  private static final long serialVersionUID = -5493008945333225318L;
096
097
098
099  // The hash code for this JSON array.
100  @Nullable private Integer hashCode;
101
102  // The list of values for this array.
103  @NotNull private final List<JSONValue> values;
104
105  // The string representation for this JSON array.
106  @Nullable private String stringRepresentation;
107
108
109
110  /**
111   * Creates a new JSON array with the provided values.
112   *
113   * @param  values  The set of values to include in this JSON array.  It may be
114   *                 {@code null} or empty to indicate that the array should be
115   *                 empty.
116   */
117  public JSONArray(@Nullable final JSONValue... values)
118  {
119    this((values == null) ? null : Arrays.asList(values));
120  }
121
122
123
124  /**
125   * Creates a new JSON array with the provided values.
126   *
127   * @param  values  The set of values to include in this JSON array.  It may be
128   *                 {@code null} or empty to indicate that the array should be
129   *                 empty.
130   */
131  public JSONArray(@Nullable final List<? extends JSONValue> values)
132  {
133    if (values == null)
134    {
135      this.values = Collections.emptyList();
136    }
137    else
138    {
139      this.values =
140           Collections.unmodifiableList(new ArrayList<>(values));
141    }
142
143    hashCode = null;
144    stringRepresentation = null;
145  }
146
147
148
149  /**
150   * Retrieves the set of values contained in this JSON array.
151   *
152   * @return  The set of values contained in this JSON array.
153   */
154  @NotNull()
155  public List<JSONValue> getValues()
156  {
157    return values;
158  }
159
160
161
162  /**
163   * Indicates whether this array is empty.
164   *
165   * @return  {@code true} if this array does not contain any values, or
166   *          {@code false} if this array contains at least one value.
167   */
168  public boolean isEmpty()
169  {
170    return values.isEmpty();
171  }
172
173
174
175  /**
176   * Retrieves the number of values contained in this array.
177   *
178   * @return  The number of values contained in this array.
179   */
180  public int size()
181  {
182    return values.size();
183  }
184
185
186
187  /**
188   * {@inheritDoc}
189   */
190  @Override()
191  public int hashCode()
192  {
193    if (hashCode == null)
194    {
195      int hc = 0;
196      for (final JSONValue v : values)
197      {
198        hc = (hc * 31) + v.hashCode();
199      }
200
201      hashCode = hc;
202    }
203
204    return hashCode;
205  }
206
207
208
209  /**
210   * {@inheritDoc}
211   */
212  @Override()
213  public boolean equals(@Nullable final Object o)
214  {
215    if (o == this)
216    {
217      return true;
218    }
219
220    if (o instanceof JSONArray)
221    {
222      final JSONArray a = (JSONArray) o;
223      return values.equals(a.values);
224    }
225
226    return false;
227  }
228
229
230
231  /**
232   * Indicates whether this JSON array is considered equivalent to the provided
233   * array, subject to the specified constraints.
234   *
235   * @param  array                The array for which to make the determination.
236   * @param  ignoreFieldNameCase  Indicates whether to ignore differences in
237   *                              capitalization in field names for any JSON
238   *                              objects contained in the array.
239   * @param  ignoreValueCase      Indicates whether to ignore differences in
240   *                              capitalization for array elements that are
241   *                              JSON strings, as well as for the string values
242   *                              of any JSON objects and arrays contained in
243   *                              the array.
244   * @param  ignoreArrayOrder     Indicates whether to ignore differences in the
245   *                              order of elements contained in the array.
246   *
247   * @return  {@code true} if this JSON array is considered equivalent to the
248   *          provided array (subject to the specified constraints), or
249   *          {@code false} if not.
250   */
251  public boolean equals(@NotNull final JSONArray array,
252                        final boolean ignoreFieldNameCase,
253                        final boolean ignoreValueCase,
254                        final boolean ignoreArrayOrder)
255  {
256    // See if we can do a straight-up List.equals.  If so, just do that.
257    if ((! ignoreFieldNameCase) && (! ignoreValueCase) && (! ignoreArrayOrder))
258    {
259      return values.equals(array.values);
260    }
261
262    // Make sure the arrays have the same number of elements.
263    if (values.size() != array.values.size())
264    {
265      return false;
266    }
267
268    // Optimize for the case in which the order of values is significant.
269    if (! ignoreArrayOrder)
270    {
271      final Iterator<JSONValue> thisIterator = values.iterator();
272      final Iterator<JSONValue> thatIterator = array.values.iterator();
273      while (thisIterator.hasNext())
274      {
275        final JSONValue thisValue = thisIterator.next();
276        final JSONValue thatValue = thatIterator.next();
277        if (! thisValue.equals(thatValue, ignoreFieldNameCase, ignoreValueCase,
278             ignoreArrayOrder))
279        {
280          return false;
281        }
282      }
283
284      return true;
285    }
286
287
288    // If we've gotten here, then we know that we don't care about the order.
289    // Create a new list that we can remove values from as we find matches.
290    // This is important because arrays can have duplicate values, and we don't
291    // want to keep matching the same element.
292    final ArrayList<JSONValue> thatValues = new ArrayList<>(array.values);
293    final Iterator<JSONValue> thisIterator = values.iterator();
294    while (thisIterator.hasNext())
295    {
296      final JSONValue thisValue = thisIterator.next();
297
298      boolean found = false;
299      final Iterator<JSONValue> thatIterator = thatValues.iterator();
300      while (thatIterator.hasNext())
301      {
302        final JSONValue thatValue = thatIterator.next();
303        if (thisValue.equals(thatValue, ignoreFieldNameCase, ignoreValueCase,
304             ignoreArrayOrder))
305        {
306          found = true;
307          thatIterator.remove();
308          break;
309        }
310      }
311
312      if (! found)
313      {
314        return false;
315      }
316    }
317
318    return true;
319  }
320
321
322
323  /**
324   * {@inheritDoc}
325   */
326  @Override()
327  public boolean equals(@NotNull final JSONValue v,
328                        final boolean ignoreFieldNameCase,
329                        final boolean ignoreValueCase,
330                        final boolean ignoreArrayOrder)
331  {
332    return ((v instanceof JSONArray) &&
333         equals((JSONArray) v, ignoreFieldNameCase, ignoreValueCase,
334              ignoreArrayOrder));
335  }
336
337
338
339  /**
340   * Indicates whether this JSON array contains an element with the specified
341   * value.
342   *
343   * @param  value                The value for which to make the determination.
344   * @param  ignoreFieldNameCase  Indicates whether to ignore differences in
345   *                              capitalization in field names for any JSON
346   *                              objects contained in the array.
347   * @param  ignoreValueCase      Indicates whether to ignore differences in
348   *                              capitalization for array elements that are
349   *                              JSON strings, as well as for the string values
350   *                              of any JSON objects and arrays contained in
351   *                              the array.
352   * @param  ignoreArrayOrder     Indicates whether to ignore differences in the
353   *                              order of elements contained in arrays.  This
354   *                              is only applicable if the provided value is
355   *                              itself an array or is a JSON object that
356   *                              contains values that are arrays.
357   * @param  recursive            Indicates whether to recursively look into any
358   *                              arrays contained inside this array.
359   *
360   * @return  {@code true} if this JSON array contains an element with the
361   *          specified value, or {@code false} if not.
362   */
363  public boolean contains(@NotNull final JSONValue value,
364                          final boolean ignoreFieldNameCase,
365                          final boolean ignoreValueCase,
366                          final boolean ignoreArrayOrder,
367                          final boolean recursive)
368  {
369    for (final JSONValue v : values)
370    {
371      if (v.equals(value, ignoreFieldNameCase, ignoreValueCase,
372           ignoreArrayOrder))
373      {
374        return true;
375      }
376
377      if (recursive && (v instanceof JSONArray) &&
378          ((JSONArray) v).contains(value, ignoreFieldNameCase, ignoreValueCase,
379               ignoreArrayOrder, recursive))
380      {
381        return true;
382      }
383    }
384
385    return false;
386  }
387
388
389
390  /**
391   * Retrieves a string representation of this array as it should appear in a
392   * JSON object, including the surrounding square brackets.  Appropriate
393   * encoding will also be used for all elements in the array.    If the object
394   * containing this array was decoded from a string, then this method will use
395   * the same string representation as in that original object.  Otherwise, the
396   * string representation will be constructed.
397   *
398   * @return  A string representation of this array as it should appear in a
399   *          JSON object, including the surrounding square brackets.
400   */
401  @Override()
402  @NotNull()
403  public String toString()
404  {
405    if (stringRepresentation == null)
406    {
407      final StringBuilder buffer = new StringBuilder();
408      toString(buffer);
409      stringRepresentation = buffer.toString();
410    }
411
412    return stringRepresentation;
413  }
414
415
416
417  /**
418   * Appends a string representation of this value as it should appear in a
419   * JSON object, including the surrounding square brackets,. to the provided
420   * buffer.  Appropriate encoding will also be used for all elements in the
421   * array.    If the object containing this array was decoded from a string,
422   * then this method will use the same string representation as in that
423   * original object.  Otherwise, the string representation will be constructed.
424   *
425   * @param  buffer  The buffer to which the information should be appended.
426   */
427  @Override()
428  public void toString(@NotNull final StringBuilder buffer)
429  {
430    if (stringRepresentation != null)
431    {
432      buffer.append(stringRepresentation);
433      return;
434    }
435
436    buffer.append("[ ");
437
438    final Iterator<JSONValue> iterator = values.iterator();
439    while (iterator.hasNext())
440    {
441      iterator.next().toString(buffer);
442      if (iterator.hasNext())
443      {
444        buffer.append(',');
445      }
446      buffer.append(' ');
447    }
448
449    buffer.append(']');
450  }
451
452
453
454  /**
455   * Retrieves a single-line string representation of this array as it should
456   * appear in a JSON object, including the surrounding square brackets.
457   * Appropriate encoding will also be used for all elements in the array.
458   *
459   * @return  A string representation of this array as it should appear in a
460   *          JSON object, including the surrounding square brackets.
461   */
462  @Override()
463  @NotNull()
464  public String toSingleLineString()
465  {
466    final StringBuilder buffer = new StringBuilder();
467    toSingleLineString(buffer);
468    return buffer.toString();
469  }
470
471
472
473  /**
474   * Appends a single-line string representation of this array as it should
475   * appear in a JSON object, including the surrounding square brackets, to the
476   * provided buffer.  Appropriate encoding will also be used for all elements
477   * in the array.
478   *
479   * @param  buffer  The buffer to which the information should be appended.
480   */
481  @Override()
482  public void toSingleLineString(@NotNull final StringBuilder buffer)
483  {
484    buffer.append("[ ");
485
486    final Iterator<JSONValue> iterator = values.iterator();
487    while (iterator.hasNext())
488    {
489      iterator.next().toSingleLineString(buffer);
490      if (iterator.hasNext())
491      {
492        buffer.append(',');
493      }
494      buffer.append(' ');
495    }
496
497    buffer.append(']');
498  }
499
500
501
502  /**
503   * Retrieves a normalized string representation of this array.  The normalized
504   * representation will not contain any line breaks, will not include any
505   * spaces around the enclosing brackets or around commas used to separate the
506   * elements, and it will use the normalized representations of those elements.
507   * The order of elements in an array is considered significant, and will not
508   * be affected by the normalization process.
509   *
510   * @return  A normalized string representation of this array.
511   */
512  @Override()
513  @NotNull()
514  public String toNormalizedString()
515  {
516    final StringBuilder buffer = new StringBuilder();
517    toNormalizedString(buffer);
518    return buffer.toString();
519  }
520
521
522
523  /**
524   * Appends a normalized string representation of this array to the provided
525   * buffer.  The normalized representation will not contain any line breaks,
526   * will not include any spaces around the enclosing brackets or around commas
527   * used to separate the elements, and it will use the normalized
528   * representations of those elements. The order of elements in an array is
529   * considered significant, and will not be affected by the normalization
530   * process.
531   *
532   * @param  buffer  The buffer to which the information should be appended.
533   */
534  @Override()
535  public void toNormalizedString(@NotNull final StringBuilder buffer)
536  {
537    toNormalizedString(buffer, false, true, false);
538  }
539
540
541
542  /**
543   * Retrieves a normalized string representation of this array.  The normalized
544   * representation will not contain any line breaks, will not include any
545   * spaces around the enclosing brackets or around commas used to separate the
546   * elements, and it will use the normalized representations of those elements.
547   * The order of elements in an array is considered significant, and will not
548   * be affected by the normalization process.
549   *
550   * @param  ignoreFieldNameCase  Indicates whether field names should be
551   *                              treated in a case-sensitive (if {@code false})
552   *                              or case-insensitive (if {@code true}) manner.
553   * @param  ignoreValueCase      Indicates whether string field values should
554   *                              be treated in a case-sensitive (if
555   *                              {@code false}) or case-insensitive (if
556   *                              {@code true}) manner.
557   * @param  ignoreArrayOrder     Indicates whether the order of elements in an
558   *                              array should be considered significant (if
559   *                              {@code false}) or insignificant (if
560   *                              {@code true}).
561   *
562   * @return  A normalized string representation of this array.
563   */
564  @Override()
565  @NotNull()
566  public String toNormalizedString(final boolean ignoreFieldNameCase,
567                                   final boolean ignoreValueCase,
568                                   final boolean ignoreArrayOrder)
569  {
570    final StringBuilder buffer = new StringBuilder();
571    toNormalizedString(buffer, ignoreFieldNameCase, ignoreValueCase,
572         ignoreArrayOrder);
573    return buffer.toString();
574  }
575
576
577
578  /**
579   * Appends a normalized string representation of this array to the provided
580   * buffer.  The normalized representation will not contain any line breaks,
581   * will not include any spaces around the enclosing brackets or around commas
582   * used to separate the elements, and it will use the normalized
583   * representations of those elements. The order of elements in an array is
584   * considered significant, and will not be affected by the normalization
585   * process.
586   *
587   * @param  buffer               The buffer to which the information should be
588   *                              appended.
589   * @param  ignoreFieldNameCase  Indicates whether field names should be
590   *                              treated in a case-sensitive (if {@code false})
591   *                              or case-insensitive (if {@code true}) manner.
592   * @param  ignoreValueCase      Indicates whether string field values should
593   *                              be treated in a case-sensitive (if
594   *                              {@code false}) or case-insensitive (if
595   *                              {@code true}) manner.
596   * @param  ignoreArrayOrder     Indicates whether the order of elements in an
597   *                              array should be considered significant (if
598   *                              {@code false}) or insignificant (if
599   *                              {@code true}).
600   */
601  @Override()
602  public void toNormalizedString(@NotNull final StringBuilder buffer,
603                                 final boolean ignoreFieldNameCase,
604                                 final boolean ignoreValueCase,
605                                 final boolean ignoreArrayOrder)
606  {
607    final List<String> normalizedValues = new ArrayList<>(values.size());
608    for (final JSONValue v : values)
609    {
610      normalizedValues.add(v.toNormalizedString(ignoreFieldNameCase,
611           ignoreValueCase, ignoreArrayOrder));
612    }
613
614    if (ignoreArrayOrder)
615    {
616      Collections.sort(normalizedValues);
617    }
618
619    buffer.append('[');
620
621    final Iterator<String> iterator = normalizedValues.iterator();
622    while (iterator.hasNext())
623    {
624      buffer.append(iterator.next());
625      if (iterator.hasNext())
626      {
627        buffer.append(',');
628      }
629    }
630
631    buffer.append(']');
632  }
633
634
635
636  /**
637   * {@inheritDoc}
638   */
639  @Override()
640  @NotNull()
641  public JSONArray toNormalizedValue(final boolean ignoreFieldNameCase,
642                                     final boolean ignoreValueCase,
643                                     final boolean ignoreArrayOrder)
644  {
645    final List<JSONValue> normalizedValues = new ArrayList<>(values.size());
646    for (final JSONValue v : values)
647    {
648      normalizedValues.add(v.toNormalizedValue(ignoreFieldNameCase,
649           ignoreValueCase, ignoreArrayOrder));
650    }
651
652    if (ignoreArrayOrder)
653    {
654      Collections.sort(normalizedValues);
655    }
656
657    return new JSONArray(normalizedValues);
658  }
659
660
661
662  /**
663   * {@inheritDoc}
664   */
665  @Override()
666  public void appendToJSONBuffer(@NotNull final JSONBuffer buffer)
667  {
668    buffer.beginArray();
669
670    for (final JSONValue value : values)
671    {
672      value.appendToJSONBuffer(buffer);
673    }
674
675    buffer.endArray();
676  }
677
678
679
680  /**
681   * {@inheritDoc}
682   */
683  @Override()
684  public void appendToJSONBuffer(@NotNull final String fieldName,
685                                 @NotNull final JSONBuffer buffer)
686  {
687    buffer.beginArray(fieldName);
688
689    for (final JSONValue value : values)
690    {
691      value.appendToJSONBuffer(buffer);
692    }
693
694    buffer.endArray();
695  }
696}