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}