001/* 002 * Copyright 2016-2023 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2016-2023 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-2023 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 private 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 the provided JSON value. This will not include a field name, so it 595 * should only be used for elements in an array. 596 * 597 * @param fieldName The name of the field. 598 * @param value The value to append. 599 */ 600 public void appendValue(@NotNull final String fieldName, 601 @NotNull final JSONValue value) 602 { 603 value.appendToJSONBuffer(fieldName, this); 604 } 605 606 607 608 /** 609 * Retrieves the byte string buffer that backs this JSON buffer. 610 * 611 * @return The byte string buffer that backs this JSON buffer. 612 */ 613 @NotNull() 614 public ByteStringBuffer getBuffer() 615 { 616 return buffer; 617 } 618 619 620 621 /** 622 * Writes the current contents of this JSON buffer to the provided output 623 * stream. Note that based on the current contents of this buffer and the way 624 * it has been used so far, it may not represent a valid JSON object. 625 * 626 * @param outputStream The output stream to which the current contents of 627 * this JSON buffer should be written. 628 * 629 * @throws IOException If a problem is encountered while writing to the 630 * provided output stream. 631 */ 632 public void writeTo(@NotNull final OutputStream outputStream) 633 throws IOException 634 { 635 buffer.write(outputStream); 636 } 637 638 639 640 /** 641 * Retrieves a string representation of the current contents of this JSON 642 * buffer. Note that based on the current contents of this buffer and the way 643 * it has been used so far, it may not represent a valid JSON object. 644 * 645 * @return A string representation of the current contents of this JSON 646 * buffer. 647 */ 648 @Override() 649 @NotNull() 650 public String toString() 651 { 652 return buffer.toString(); 653 } 654 655 656 657 /** 658 * Retrieves the current contents of this JSON buffer as a JSON object. 659 * 660 * @return The JSON object decoded from the contents of this JSON buffer. 661 * 662 * @throws JSONException If the buffer does not currently contain exactly 663 * one valid JSON object. 664 */ 665 @NotNull() 666 public JSONObject toJSONObject() 667 throws JSONException 668 { 669 return new JSONObject(buffer.toString()); 670 } 671 672 673 674 /** 675 * Adds a comma and line break to the buffer if appropriate. 676 */ 677 private void addComma() 678 { 679 if (needComma) 680 { 681 buffer.append(','); 682 if (multiLine) 683 { 684 buffer.append(StaticUtils.EOL_BYTES); 685 buffer.append(indents.getLast()); 686 } 687 else 688 { 689 buffer.append(' '); 690 } 691 } 692 } 693 694 695 696 /** 697 * Adds an indent to the set of indents of appropriate. 698 * 699 * @param size The number of spaces to indent. 700 */ 701 private void addIndent(final int size) 702 { 703 if (multiLine) 704 { 705 final char[] spaces = new char[size]; 706 Arrays.fill(spaces, ' '); 707 final String indentStr = new String(spaces); 708 709 if (indents.isEmpty()) 710 { 711 indents.add(indentStr); 712 } 713 else 714 { 715 indents.add(indents.getLast() + indentStr); 716 } 717 } 718 } 719 720 721 722 /** 723 * Removes an indent from the set of indents of appropriate. 724 */ 725 private void removeIndent() 726 { 727 if (multiLine && (! indents.isEmpty())) 728 { 729 indents.removeLast(); 730 } 731 } 732}