001/* 002 * Copyright 2016-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2016-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) 2016-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.io.IOException; 041import java.io.OutputStream; 042import java.io.Serializable; 043import java.math.BigDecimal; 044import java.util.Arrays; 045import java.util.LinkedList; 046 047import com.unboundid.util.ByteStringBuffer; 048import com.unboundid.util.Mutable; 049import com.unboundid.util.NotNull; 050import com.unboundid.util.Nullable; 051import com.unboundid.util.StaticUtils; 052import com.unboundid.util.ThreadSafety; 053import com.unboundid.util.ThreadSafetyLevel; 054 055 056 057/** 058 * This class provides a mechanism for constructing the string representation of 059 * one or more JSON objects by appending elements of those objects into a byte 060 * string buffer. {@code JSONBuffer} instances may be cleared and reused any 061 * number of times. They are not threadsafe and should not be accessed 062 * concurrently by multiple threads. 063 * <BR><BR> 064 * Note that the caller is responsible for proper usage to ensure that the 065 * buffer results in a valid JSON encoding. This includes ensuring that the 066 * object begins with the appropriate opening curly brace, that all objects 067 * and arrays are properly closed, that raw values are not used outside of 068 * arrays, that named fields are not added into arrays, etc. 069 */ 070@Mutable() 071@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 072public final class JSONBuffer 073 implements Serializable 074{ 075 /** 076 * The default maximum buffer size. 077 */ 078 public static final int DEFAULT_MAX_BUFFER_SIZE = 1_048_576; 079 080 081 082 /** 083 * The serial version UID for this serializable class. 084 */ 085 private static final long serialVersionUID = 5946166401452532693L; 086 087 088 089 // Indicates whether to format the JSON object across multiple lines rather 090 // than putting it all on a single line. 091 private final boolean multiLine; 092 093 // Indicates whether we need to add a comma before adding the next element. 094 private boolean needComma = false; 095 096 // The buffer to which all data will be written. 097 @NotNull private ByteStringBuffer buffer; 098 099 // The maximum buffer size that should be retained. 100 private final int maxBufferSize; 101 102 // A list of the indents that we need to use when formatting multi-line 103 // objects. 104 @NotNull private final LinkedList<String> indents; 105 106 107 108 /** 109 * Creates a new instance of this JSON buffer with the default maximum buffer 110 * size. 111 */ 112 public JSONBuffer() 113 { 114 this(DEFAULT_MAX_BUFFER_SIZE); 115 } 116 117 118 119 /** 120 * Creates a new instance of this JSON buffer with an optional maximum 121 * retained size. If a maximum size is defined, then this buffer may be used 122 * to hold elements larger than that, but when the buffer is cleared it will 123 * be shrunk to the maximum size. 124 * 125 * @param maxBufferSize The maximum buffer size that will be retained by 126 * this JSON buffer. A value less than or equal to 127 * zero indicates that no maximum size should be 128 * enforced. 129 */ 130 public JSONBuffer(final int maxBufferSize) 131 { 132 this(null, maxBufferSize, false); 133 } 134 135 136 137 /** 138 * Creates a new instance of this JSON buffer that wraps the provided byte 139 * string buffer (if provided) and that has an optional maximum retained size. 140 * If a maximum size is defined, then this buffer may be used to hold elements 141 * larger than that, but when the buffer is cleared it will be shrunk to the 142 * maximum size. 143 * 144 * @param buffer The buffer to wrap. It may be {@code null} if a new 145 * buffer should be created. 146 * @param maxBufferSize The maximum buffer size that will be retained by 147 * this JSON buffer. A value less than or equal to 148 * zero indicates that no maximum size should be 149 * enforced. 150 * @param multiLine Indicates whether to format JSON objects using a 151 * user-friendly, formatted, multi-line representation 152 * rather than constructing the entire element without 153 * any line breaks. Note that regardless of the value 154 * of this argument, there will not be an end-of-line 155 * marker at the very end of the object. 156 */ 157 public JSONBuffer(@Nullable final ByteStringBuffer buffer, 158 final int maxBufferSize, final boolean multiLine) 159 { 160 this.multiLine = multiLine; 161 this.maxBufferSize = maxBufferSize; 162 163 indents = new LinkedList<>(); 164 needComma = false; 165 166 if (buffer == null) 167 { 168 this.buffer = new ByteStringBuffer(); 169 } 170 else 171 { 172 this.buffer = buffer; 173 } 174 } 175 176 177 178 /** 179 * Clears the contents of this buffer. 180 */ 181 public void clear() 182 { 183 buffer.clear(); 184 185 if ((maxBufferSize > 0) && (buffer.capacity() > maxBufferSize)) 186 { 187 buffer.setCapacity(maxBufferSize); 188 } 189 190 needComma = false; 191 indents.clear(); 192 } 193 194 195 196 /** 197 * Replaces the underlying buffer to which the JSON object data will be 198 * written. 199 * 200 * @param buffer The underlying buffer to which the JSON object data will be 201 * written. 202 */ 203 public void setBuffer(@Nullable final ByteStringBuffer buffer) 204 { 205 if (buffer == null) 206 { 207 this.buffer = new ByteStringBuffer(); 208 } 209 else 210 { 211 this.buffer = buffer; 212 } 213 214 needComma = false; 215 indents.clear(); 216 } 217 218 219 220 /** 221 * Retrieves the current length of this buffer in bytes. 222 * 223 * @return The current length of this buffer in bytes. 224 */ 225 public int length() 226 { 227 return buffer.length(); 228 } 229 230 231 232 /** 233 * Appends the open curly brace needed to signify the beginning of a JSON 234 * object. This will not include a field name, so it should only be used to 235 * start the outermost JSON object, or to start a JSON object contained in an 236 * array. 237 */ 238 public void beginObject() 239 { 240 addComma(); 241 buffer.append("{ "); 242 needComma = false; 243 addIndent(2); 244 } 245 246 247 248 /** 249 * Begins a new JSON object that will be used as the value of the specified 250 * field. 251 * 252 * @param fieldName The name of the field 253 */ 254 public void beginObject(@NotNull final String fieldName) 255 { 256 addComma(); 257 258 final int startPos = buffer.length(); 259 JSONString.encodeString(fieldName, buffer); 260 final int fieldNameLength = buffer.length() - startPos; 261 262 buffer.append(":{ "); 263 needComma = false; 264 addIndent(fieldNameLength + 3); 265 } 266 267 268 269 /** 270 * Appends the close curly brace needed to signify the end of a JSON object. 271 */ 272 public void endObject() 273 { 274 if (needComma) 275 { 276 buffer.append(' '); 277 } 278 279 buffer.append('}'); 280 needComma = true; 281 removeIndent(); 282 } 283 284 285 286 /** 287 * Appends the open curly brace needed to signify the beginning of a JSON 288 * array. This will not include a field name, so it should only be used to 289 * start a JSON array contained in an array. 290 */ 291 public void beginArray() 292 { 293 addComma(); 294 buffer.append("[ "); 295 needComma = false; 296 addIndent(2); 297 } 298 299 300 301 /** 302 * Begins a new JSON array that will be used as the value of the specified 303 * field. 304 * 305 * @param fieldName The name of the field 306 */ 307 public void beginArray(@NotNull final String fieldName) 308 { 309 addComma(); 310 311 final int startPos = buffer.length(); 312 JSONString.encodeString(fieldName, buffer); 313 final int fieldNameLength = buffer.length() - startPos; 314 315 buffer.append(":[ "); 316 needComma = false; 317 addIndent(fieldNameLength + 3); 318 } 319 320 321 322 /** 323 * Appends the close square bracket needed to signify the end of a JSON array. 324 */ 325 public void endArray() 326 { 327 if (needComma) 328 { 329 buffer.append(' '); 330 } 331 332 buffer.append(']'); 333 needComma = true; 334 removeIndent(); 335 } 336 337 338 339 /** 340 * Appends the provided Boolean value. This will not include a field name, so 341 * it should only be used for Boolean value elements in an array. 342 * 343 * @param value The Boolean value to append. 344 */ 345 public void appendBoolean(final boolean value) 346 { 347 addComma(); 348 if (value) 349 { 350 buffer.append("true"); 351 } 352 else 353 { 354 buffer.append("false"); 355 } 356 needComma = true; 357 } 358 359 360 361 /** 362 * Appends a JSON field with the specified name and the provided Boolean 363 * value. 364 * 365 * @param fieldName The name of the field. 366 * @param value The Boolean value. 367 */ 368 public void appendBoolean(@NotNull final String fieldName, 369 final boolean value) 370 { 371 addComma(); 372 JSONString.encodeString(fieldName, buffer); 373 if (value) 374 { 375 buffer.append(":true"); 376 } 377 else 378 { 379 buffer.append(":false"); 380 } 381 382 needComma = true; 383 } 384 385 386 387 /** 388 * Appends the provided JSON null value. This will not include a field name, 389 * so it should only be used for null value elements in an array. 390 */ 391 public void appendNull() 392 { 393 addComma(); 394 buffer.append("null"); 395 needComma = true; 396 } 397 398 399 400 /** 401 * Appends a JSON field with the specified name and a null value. 402 * 403 * @param fieldName The name of the field. 404 */ 405 public void appendNull(@NotNull final String fieldName) 406 { 407 addComma(); 408 JSONString.encodeString(fieldName, buffer); 409 buffer.append(":null"); 410 needComma = true; 411 } 412 413 414 415 /** 416 * Appends the provided JSON number value. This will not include a field 417 * name, so it should only be used for number elements in an array. 418 * 419 * @param value The number to add. 420 */ 421 public void appendNumber(@NotNull final BigDecimal value) 422 { 423 addComma(); 424 buffer.append(value.toPlainString()); 425 needComma = true; 426 } 427 428 429 430 /** 431 * Appends the provided JSON number value. This will not include a field 432 * name, so it should only be used for number elements in an array. 433 * 434 * @param value The number to add. 435 */ 436 public void appendNumber(final int value) 437 { 438 addComma(); 439 buffer.append(value); 440 needComma = true; 441 } 442 443 444 445 /** 446 * Appends the provided JSON number value. This will not include a field 447 * name, so it should only be used for number elements in an array. 448 * 449 * @param value The number to add. 450 */ 451 public void appendNumber(final long value) 452 { 453 addComma(); 454 buffer.append(value); 455 needComma = true; 456 } 457 458 459 460 /** 461 * Appends the provided JSON number value. This will not include a field 462 * name, so it should only be used for number elements in an array. 463 * 464 * @param value The string representation of the number to add. It must be 465 * properly formed. 466 */ 467 public void appendNumber(@NotNull final String value) 468 { 469 addComma(); 470 buffer.append(value); 471 needComma = true; 472 } 473 474 475 476 /** 477 * Appends a JSON field with the specified name and a number value. 478 * 479 * @param fieldName The name of the field. 480 * @param value The number value. 481 */ 482 public void appendNumber(@NotNull final String fieldName, 483 @NotNull final BigDecimal value) 484 { 485 addComma(); 486 JSONString.encodeString(fieldName, buffer); 487 buffer.append(':'); 488 buffer.append(value.toPlainString()); 489 needComma = true; 490 } 491 492 493 494 /** 495 * Appends a JSON field with the specified name and a number value. 496 * 497 * @param fieldName The name of the field. 498 * @param value The number value. 499 */ 500 public void appendNumber(@NotNull final String fieldName, final int value) 501 { 502 addComma(); 503 JSONString.encodeString(fieldName, buffer); 504 buffer.append(':'); 505 buffer.append(value); 506 needComma = true; 507 } 508 509 510 511 /** 512 * Appends a JSON field with the specified name and a number value. 513 * 514 * @param fieldName The name of the field. 515 * @param value The number value. 516 */ 517 public void appendNumber(@NotNull final String fieldName, final long value) 518 { 519 addComma(); 520 JSONString.encodeString(fieldName, buffer); 521 buffer.append(':'); 522 buffer.append(value); 523 needComma = true; 524 } 525 526 527 528 /** 529 * Appends a JSON field with the specified name and a number value. 530 * 531 * @param fieldName The name of the field. 532 * @param value The string representation of the number ot add. It must 533 * be properly formed. 534 */ 535 public void appendNumber(@NotNull final String fieldName, 536 @NotNull final String value) 537 { 538 addComma(); 539 JSONString.encodeString(fieldName, buffer); 540 buffer.append(':'); 541 buffer.append(value); 542 needComma = true; 543 } 544 545 546 547 /** 548 * Appends the provided JSON string value. This will not include a field 549 * name, so it should only be used for string elements in an array. 550 * 551 * @param value The value to add. 552 */ 553 public void appendString(@NotNull final String value) 554 { 555 addComma(); 556 JSONString.encodeString(value, buffer); 557 needComma = true; 558 } 559 560 561 562 /** 563 * Appends a JSON field with the specified name and a null value. 564 * 565 * @param fieldName The name of the field. 566 * @param value The value to add. 567 */ 568 public void appendString(@NotNull final String fieldName, 569 @NotNull final String value) 570 { 571 addComma(); 572 JSONString.encodeString(fieldName, buffer); 573 buffer.append(':'); 574 JSONString.encodeString(value, buffer); 575 needComma = true; 576 } 577 578 579 580 /** 581 * Appends the provided JSON value. This will not include a field name, so it 582 * should only be used for elements in an array. 583 * 584 * @param value The value to append. 585 */ 586 public void appendValue(@NotNull final JSONValue value) 587 { 588 value.appendToJSONBuffer(this); 589 } 590 591 592 593 /** 594 * Appends a field with the given name and value. 595 * 596 * @param fieldName The name of the field. 597 * @param value The value to append. 598 */ 599 public void appendValue(@NotNull final String fieldName, 600 @NotNull final JSONValue value) 601 { 602 value.appendToJSONBuffer(fieldName, this); 603 } 604 605 606 607 /** 608 * Appends the provided JSON field. 609 * 610 * @param field The JSON field to be appended. 611 */ 612 public void appendField(@NotNull final JSONField field) 613 { 614 appendValue(field.getName(), field.getValue()); 615 } 616 617 618 619 /** 620 * Retrieves the byte string buffer that backs this JSON buffer. 621 * 622 * @return The byte string buffer that backs this JSON buffer. 623 */ 624 @NotNull() 625 public ByteStringBuffer getBuffer() 626 { 627 return buffer; 628 } 629 630 631 632 /** 633 * Writes the current contents of this JSON buffer to the provided output 634 * stream. Note that based on the current contents of this buffer and the way 635 * it has been used so far, it may not represent a valid JSON object. 636 * 637 * @param outputStream The output stream to which the current contents of 638 * this JSON buffer should be written. 639 * 640 * @throws IOException If a problem is encountered while writing to the 641 * provided output stream. 642 */ 643 public void writeTo(@NotNull final OutputStream outputStream) 644 throws IOException 645 { 646 buffer.write(outputStream); 647 } 648 649 650 651 /** 652 * Retrieves a string representation of the current contents of this JSON 653 * buffer. Note that based on the current contents of this buffer and the way 654 * it has been used so far, it may not represent a valid JSON object. 655 * 656 * @return A string representation of the current contents of this JSON 657 * buffer. 658 */ 659 @Override() 660 @NotNull() 661 public String toString() 662 { 663 return buffer.toString(); 664 } 665 666 667 668 /** 669 * Retrieves the current contents of this JSON buffer as a JSON object. 670 * 671 * @return The JSON object decoded from the contents of this JSON buffer. 672 * 673 * @throws JSONException If the buffer does not currently contain exactly 674 * one valid JSON object. 675 */ 676 @NotNull() 677 public JSONObject toJSONObject() 678 throws JSONException 679 { 680 return new JSONObject(buffer.toString()); 681 } 682 683 684 685 /** 686 * Adds a comma and line break to the buffer if appropriate. 687 */ 688 private void addComma() 689 { 690 if (needComma) 691 { 692 buffer.append(','); 693 if (multiLine) 694 { 695 buffer.append(StaticUtils.EOL_BYTES); 696 buffer.append(indents.getLast()); 697 } 698 else 699 { 700 buffer.append(' '); 701 } 702 } 703 } 704 705 706 707 /** 708 * Adds an indent to the set of indents of appropriate. 709 * 710 * @param size The number of spaces to indent. 711 */ 712 private void addIndent(final int size) 713 { 714 if (multiLine) 715 { 716 final char[] spaces = new char[size]; 717 Arrays.fill(spaces, ' '); 718 final String indentStr = new String(spaces); 719 720 if (indents.isEmpty()) 721 { 722 indents.add(indentStr); 723 } 724 else 725 { 726 indents.add(indents.getLast() + indentStr); 727 } 728 } 729 } 730 731 732 733 /** 734 * Removes an indent from the set of indents of appropriate. 735 */ 736 private void removeIndent() 737 { 738 if (multiLine && (! indents.isEmpty())) 739 { 740 indents.removeLast(); 741 } 742 } 743}