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