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