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}