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