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    }