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}