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}