001 /*
002 * Copyright 2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2016 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 */
021 package com.unboundid.util.json;
022
023
024
025 import java.io.IOException;
026 import java.io.OutputStream;
027 import java.io.Serializable;
028 import java.math.BigDecimal;
029 import java.util.Arrays;
030 import java.util.LinkedList;
031
032 import com.unboundid.util.ByteStringBuffer;
033 import com.unboundid.util.Mutable;
034 import com.unboundid.util.StaticUtils;
035 import com.unboundid.util.ThreadSafety;
036 import 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)
055 public 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 }