View Javadoc
1   /*
2    * #%L
3    * JsonSerializationVisitor.java - mongodb-async-driver - Allanbank Consulting, Inc.
4    * %%
5    * Copyright (C) 2011 - 2014 Allanbank Consulting, Inc.
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   * 
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   * 
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  
21  package com.allanbank.mongodb.bson.element;
22  
23  import java.io.IOException;
24  import java.io.Writer;
25  import java.text.SimpleDateFormat;
26  import java.util.Date;
27  import java.util.List;
28  import java.util.TimeZone;
29  import java.util.regex.Pattern;
30  
31  import com.allanbank.mongodb.bson.Document;
32  import com.allanbank.mongodb.bson.Element;
33  import com.allanbank.mongodb.bson.Visitor;
34  import com.allanbank.mongodb.error.JsonException;
35  import com.allanbank.mongodb.util.IOUtils;
36  
37  /**
38   * JsonSerializationVisitor provides a BSON Visitor that generates a JSON
39   * document.
40   * 
41   * @api.no This class is <b>NOT</b> part of the drivers API. This class may be
42   *         mutated in incompatible ways between any two releases of the driver.
43   * @copyright 2012-2013, Allanbank Conublic sulting, Inc., All Rights Reserved
44   */
45  public class JsonSerializationVisitor implements Visitor {
46  
47      /** The platforms new line string. */
48      public static final String NL = System.getProperty("line.separator", "\n");
49  
50      /** A pattern to detect valid "symbol" names. */
51      public static final Pattern SYMBOL_PATTERN = Pattern
52              .compile("\\p{Alpha}\\p{Alnum}*");
53  
54      /** The default time zone. */
55      public static final TimeZone UTC = TimeZone.getTimeZone("UTC");
56  
57      /** The current indent level. */
58      private int myIndentLevel = 0;
59  
60      /** If true then the visitor will write the document to 1 line. */
61      private final boolean myOneLine;
62  
63      /** The Writer to write to. */
64      private final Writer mySink;
65  
66      /**
67       * If true then the names of the elements should be suppressed because we
68       * are in an array.
69       */
70      private boolean mySuppressNames = false;
71  
72      /**
73       * Creates a new JsonSerializationVisitor.
74       * 
75       * @param sink
76       *            The Writer to write to.
77       * @param oneLine
78       *            If true then the visitor will write the document to 1 line,
79       *            otherwise the visitor will write the document accross multiple
80       *            lines with indenting.
81       */
82      public JsonSerializationVisitor(final Writer sink, final boolean oneLine) {
83          mySink = sink;
84          myOneLine = oneLine;
85          myIndentLevel = 0;
86      }
87  
88      /**
89       * {@inheritDoc}
90       * <p>
91       * Overridden to create a JSON representation of the document's elements to
92       * the writer provided when this object was created.
93       * </p>
94       */
95      @Override
96      public void visit(final List<Element> elements) {
97          try {
98              if (elements.isEmpty()) {
99                  mySink.write("{}");
100             }
101             else if ((elements.size() == 1)
102                     && !(elements.get(0) instanceof DocumentElement)
103                     && !(elements.get(0) instanceof ArrayElement)) {
104                 mySink.write("{ ");
105 
106                 final boolean oldSuppress = mySuppressNames;
107                 mySuppressNames = false;
108 
109                 elements.get(0).accept(this);
110 
111                 mySuppressNames = oldSuppress;
112                 mySink.write(" }");
113             }
114             else {
115                 mySink.write('{');
116                 myIndentLevel += 1;
117                 final boolean oldSuppress = mySuppressNames;
118                 mySuppressNames = false;
119 
120                 boolean first = true;
121                 for (final Element element : elements) {
122                     if (!first) {
123                         mySink.write(",");
124                     }
125                     nl();
126                     element.accept(this);
127                     first = false;
128                 }
129 
130                 mySuppressNames = oldSuppress;
131                 myIndentLevel -= 1;
132                 nl();
133                 mySink.write('}');
134             }
135             mySink.flush();
136         }
137         catch (final IOException ioe) {
138             throw new JsonException(ioe);
139         }
140     }
141 
142     /**
143      * {@inheritDoc}
144      * <p>
145      * Overridden to append a JSON representation of the array's elements to the
146      * writer provided when this object was created.
147      * </p>
148      */
149     @Override
150     public void visitArray(final String name, final List<Element> elements) {
151         try {
152             writeName(name);
153             if (elements.isEmpty()) {
154                 mySink.write("[]");
155             }
156             else if ((elements.size() == 1)
157                     && !(elements.get(0) instanceof DocumentElement)
158                     && !(elements.get(0) instanceof ArrayElement)) {
159                 mySink.write("[ ");
160                 final boolean oldSuppress = mySuppressNames;
161                 mySuppressNames = true;
162 
163                 elements.get(0).accept(this);
164 
165                 mySuppressNames = oldSuppress;
166                 mySink.write(" ]");
167             }
168             else {
169                 mySink.write("[");
170                 myIndentLevel += 1;
171                 final boolean oldSuppress = mySuppressNames;
172                 mySuppressNames = true;
173 
174                 boolean first = true;
175                 for (final Element element : elements) {
176                     if (!first) {
177                         mySink.write(", ");
178                     }
179                     nl();
180                     element.accept(this);
181                     first = false;
182                 }
183 
184                 mySuppressNames = oldSuppress;
185                 myIndentLevel -= 1;
186                 nl();
187                 mySink.append(']');
188             }
189             mySink.flush();
190         }
191         catch (final IOException ioe) {
192             throw new JsonException(ioe);
193         }
194     }
195 
196     /**
197      * {@inheritDoc}
198      * <p>
199      * Overridden to append a JSON representation of the binary element to the
200      * writer provided when this object was created. This method generates the
201      * MongoDB standard BinData(...) JSON extension.
202      * </p>
203      */
204     @Override
205     public void visitBinary(final String name, final byte subType,
206             final byte[] data) {
207         try {
208             writeName(name);
209             mySink.write("BinData( ");
210             mySink.write(Integer.toString(subType));
211             mySink.write(", '");
212             mySink.write(IOUtils.toBase64(data));
213             mySink.write("' )");
214             mySink.flush();
215         }
216         catch (final IOException ioe) {
217             throw new JsonException(ioe);
218         }
219     }
220 
221     /**
222      * {@inheritDoc}
223      * <p>
224      * Overridden to append a JSON representation of the boolean element to the
225      * writer provided when this object was created.
226      * </p>
227      */
228     @Override
229     public void visitBoolean(final String name, final boolean value) {
230         try {
231             writeName(name);
232             mySink.write(Boolean.toString(value));
233             mySink.flush();
234         }
235         catch (final IOException ioe) {
236             throw new JsonException(ioe);
237         }
238     }
239 
240     /**
241      * {@inheritDoc}
242      * <p>
243      * Overridden to append a JSON representation of the DBPointer element to
244      * the writer provided when this object was created. This method generates
245      * the non-standard DBPointer(...) JSON extension.
246      * </p>
247      */
248     @Override
249     public void visitDBPointer(final String name, final String databaseName,
250             final String collectionName, final ObjectId id) {
251         try {
252             writeName(name);
253             mySink.write("DBPointer( ");
254             writeQuotedString(databaseName);
255             mySink.write(", ");
256             writeQuotedString(collectionName);
257             mySink.write(", ");
258             writeObjectId(id);
259             mySink.write(" )");
260             mySink.flush();
261         }
262         catch (final IOException ioe) {
263             throw new JsonException(ioe);
264         }
265     }
266 
267     /**
268      * {@inheritDoc}
269      * <p>
270      * Overridden to append a JSON representation of the sub-document element to
271      * the writer provided when this object was created.
272      * </p>
273      */
274     @Override
275     public void visitDocument(final String name, final List<Element> elements) {
276         try {
277             writeName(name);
278             visit(elements);
279         }
280         catch (final IOException ioe) {
281             throw new JsonException(ioe);
282         }
283     }
284 
285     /**
286      * {@inheritDoc}
287      * <p>
288      * Overridden to append a JSON representation of the double element to the
289      * writer provided when this object was created.
290      * </p>
291      */
292     @Override
293     public void visitDouble(final String name, final double value) {
294         try {
295             writeName(name);
296             mySink.write(Double.toString(value));
297             mySink.flush();
298         }
299         catch (final IOException ioe) {
300             throw new JsonException(ioe);
301         }
302     }
303 
304     /**
305      * {@inheritDoc}
306      * <p>
307      * Overridden to append a JSON representation of the integer element to the
308      * writer provided when this object was created.
309      * </p>
310      */
311     @Override
312     public void visitInteger(final String name, final int value) {
313         try {
314             writeName(name);
315             mySink.write(Integer.toString(value));
316             mySink.flush();
317         }
318         catch (final IOException ioe) {
319             throw new JsonException(ioe);
320         }
321     }
322 
323     /**
324      * {@inheritDoc}
325      * <p>
326      * Overridden to append a JSON representation of the JavaScript element to
327      * the writer provided when this object was created. This method writes the
328      * elements as a <code>{ $code : &lt;code&gt; }</code> sub-document.
329      * </p>
330      */
331     @Override
332     public void visitJavaScript(final String name, final String code) {
333         try {
334             writeName(name);
335             mySink.write("{ $code : ");
336             writeQuotedString(code);
337             mySink.write(" }");
338             mySink.flush();
339         }
340         catch (final IOException ioe) {
341             throw new JsonException(ioe);
342         }
343     }
344 
345     /**
346      * {@inheritDoc}
347      * <p>
348      * Overridden to append a JSON representation of the JavaScript element to
349      * the writer provided when this object was created. This method writes the
350      * elements as a
351      * <code>{ $code : &lt;code&gt;, $scope : &lt;scope&gt; }</code>
352      * sub-document.
353      * </p>
354      */
355     @Override
356     public void visitJavaScript(final String name, final String code,
357             final Document scope) {
358         try {
359             writeName(name);
360             mySink.write("{ $code : ");
361             writeQuotedString(code);
362             mySink.write(", $scope : ");
363             scope.accept(this);
364             mySink.write(" }");
365             mySink.flush();
366         }
367         catch (final IOException ioe) {
368             throw new JsonException(ioe);
369         }
370     }
371 
372     /**
373      * {@inheritDoc}
374      * <p>
375      * Overridden to append a JSON representation of the binary element to the
376      * writer provided when this object was created. This method generates the
377      * MongoDB standard NumberLong(...) JSON extension.
378      * </p>
379      */
380     @Override
381     public void visitLong(final String name, final long value) {
382         try {
383             writeName(name);
384             mySink.write("NumberLong('");
385             mySink.write(Long.toString(value));
386             mySink.write("')");
387             mySink.flush();
388         }
389         catch (final IOException ioe) {
390             throw new JsonException(ioe);
391         }
392     }
393 
394     /**
395      * {@inheritDoc}
396      * <p>
397      * Overridden to append a JSON representation of the DBPointer element to
398      * the writer provided when this object was created. This method generates
399      * the non-standard MaxKey() JSON extension.
400      * </p>
401      */
402     @Override
403     public void visitMaxKey(final String name) {
404         try {
405             writeName(name);
406             mySink.write("MaxKey()");
407             mySink.flush();
408         }
409         catch (final IOException ioe) {
410             throw new JsonException(ioe);
411         }
412     }
413 
414     /**
415      * {@inheritDoc}
416      * <p>
417      * Overridden to append a JSON representation of the DBPointer element to
418      * the writer provided when this object was created. This method generates
419      * the non-standard MinKey() JSON extension.
420      * </p>
421      */
422     @Override
423     public void visitMinKey(final String name) {
424         try {
425             writeName(name);
426             mySink.write("MinKey()");
427             mySink.flush();
428         }
429         catch (final IOException ioe) {
430             throw new JsonException(ioe);
431         }
432     }
433 
434     /**
435      * {@inheritDoc}
436      * <p>
437      * Overridden to append a JSON representation of the binary element to the
438      * writer provided when this object was created. This method generates the
439      * MongoDB standard Timestamp(...) JSON extension.
440      * </p>
441      */
442     @Override
443     public void visitMongoTimestamp(final String name, final long value) {
444         try {
445             final long time = (value >> Integer.SIZE) & 0xFFFFFFFFL;
446             final long increment = value & 0xFFFFFFFFL;
447 
448             writeName(name);
449             mySink.write("Timestamp(");
450             mySink.write(Long.toString(time * 1000));
451             mySink.write(", ");
452             mySink.write(Long.toString(increment));
453             mySink.write(')');
454             mySink.flush();
455         }
456         catch (final IOException ioe) {
457             throw new JsonException(ioe);
458         }
459     }
460 
461     /**
462      * {@inheritDoc}
463      * <p>
464      * Overridden to append a JSON representation of the null element to the
465      * writer provided when this object was created.
466      * </p>
467      */
468     @Override
469     public void visitNull(final String name) {
470         try {
471             writeName(name);
472             mySink.write("null");
473             mySink.flush();
474         }
475         catch (final IOException ioe) {
476             throw new JsonException(ioe);
477         }
478     }
479 
480     /**
481      * {@inheritDoc}
482      * <p>
483      * Overridden to append a JSON representation of the binary element to the
484      * writer provided when this object was created. This method generates the
485      * MongoDB standard ObjectId(...) JSON extension.
486      * </p>
487      */
488     @Override
489     public void visitObjectId(final String name, final ObjectId id) {
490         try {
491             writeName(name);
492             writeObjectId(id);
493             mySink.flush();
494         }
495         catch (final IOException ioe) {
496             throw new JsonException(ioe);
497         }
498     }
499 
500     /**
501      * {@inheritDoc}
502      * <p>
503      * Overridden to append a JSON representation of the JavaScript element to
504      * the writer provided when this object was created. This method writes the
505      * elements as a
506      * <code>{ $regex : &lt;pattern&gt;, $options : &lt;options&gt; }</code>
507      * sub-document.
508      * </p>
509      */
510     @Override
511     public void visitRegularExpression(final String name, final String pattern,
512             final String options) {
513         try {
514             writeName(name);
515             mySink.write("{ $regex : '");
516             mySink.write(pattern);
517             if (options.isEmpty()) {
518                 mySink.write("' }");
519             }
520             else {
521                 mySink.write("', $options : '");
522                 mySink.write(options);
523                 mySink.write("' }");
524             }
525             mySink.flush();
526         }
527         catch (final IOException ioe) {
528             throw new JsonException(ioe);
529         }
530     }
531 
532     /**
533      * {@inheritDoc}
534      * <p>
535      * Overridden to append a JSON representation of the string element to the
536      * writer provided when this object was created.
537      * </p>
538      */
539     @Override
540     public void visitString(final String name, final String value) {
541         try {
542             writeName(name);
543             writeQuotedString(value);
544             mySink.flush();
545         }
546         catch (final IOException ioe) {
547             throw new JsonException(ioe);
548         }
549     }
550 
551     /**
552      * {@inheritDoc}
553      * <p>
554      * Overridden to append a JSON representation of the symbol element to the
555      * writer provided when this object was created.
556      * </p>
557      */
558     @Override
559     public void visitSymbol(final String name, final String symbol) {
560         try {
561             writeName(name);
562             if (SYMBOL_PATTERN.matcher(symbol).matches()) {
563                 mySink.write(symbol);
564             }
565             else {
566                 writeQuotedString(symbol);
567             }
568             mySink.flush();
569         }
570         catch (final IOException ioe) {
571             throw new JsonException(ioe);
572         }
573 
574     }
575 
576     /**
577      * {@inheritDoc}
578      * <p>
579      * Overridden to append a JSON representation of the binary element to the
580      * writer provided when this object was created. This method generates the
581      * MongoDB standard ISODate(...) JSON extension.
582      * </p>
583      */
584     @Override
585     public void visitTimestamp(final String name, final long timestamp) {
586         final SimpleDateFormat sdf = new SimpleDateFormat(
587                 "yyyy-MM-dd'T'HH:mm:ss.SSSZ");
588         sdf.setTimeZone(UTC);
589 
590         try {
591             writeName(name);
592             mySink.write("ISODate('");
593             mySink.write(sdf.format(new Date(timestamp)));
594             mySink.write("')");
595             mySink.flush();
596         }
597         catch (final IOException ioe) {
598             throw new JsonException(ioe);
599         }
600 
601     }
602 
603     /**
604      * Returns if the visitor is currently suppressing the names of elements.
605      * This is true when serializing an array.
606      * 
607      * @return If the visitor is currently suppressing the names of elements.
608      *         This is true when serializing an array.
609      */
610     protected boolean isSuppressNames() {
611         return mySuppressNames;
612     }
613 
614     /**
615      * Writes a new line if {@link #myOneLine} is false and indents to the
616      * {@link #myIndentLevel}.
617      * 
618      * @throws IOException
619      *             On a failure to write the new line.
620      */
621     protected void nl() throws IOException {
622         if (!myOneLine) {
623             mySink.write(NL);
624             for (int i = 0; i < myIndentLevel; ++i) {
625                 mySink.write("  ");
626             }
627         }
628         else {
629             mySink.write(' ');
630         }
631     }
632 
633     /**
634      * Sets the value for if the visitor is currently suppressing the names of
635      * elements. This is true, for example, when serializing an array.
636      * 
637      * @param suppressNames
638      *            The new value for if names should be suppressed.
639      */
640     protected void setSuppressNames(final boolean suppressNames) {
641         mySuppressNames = suppressNames;
642     }
643 
644     /**
645      * Writes the name if {@link #mySuppressNames} is false.
646      * 
647      * @param name
648      *            The name to write, if not suppressed.
649      * @throws IOException
650      *             On a failure to write the new line.
651      */
652     protected void writeName(final String name) throws IOException {
653         if (!mySuppressNames) {
654             if (SYMBOL_PATTERN.matcher(name).matches()) {
655                 mySink.write(name);
656             }
657             else {
658                 writeQuotedString(name);
659             }
660             mySink.write(" : ");
661         }
662     }
663 
664     /**
665      * Writes the {@link ObjectId}.
666      * 
667      * @param id
668      *            The {@link ObjectId} to write.
669      * @throws IOException
670      *             On a failure writing to the sink.
671      */
672     protected void writeObjectId(final ObjectId id) throws IOException {
673         mySink.write("ObjectId('");
674 
675         String hex = Integer.toHexString(id.getTimestamp());
676         mySink.write("00000000".substring(hex.length()));
677         mySink.write(hex);
678 
679         hex = Long.toHexString(id.getMachineId());
680         mySink.write("0000000000000000".substring(hex.length()));
681         mySink.write(hex);
682 
683         mySink.write("')");
684     }
685 
686     /**
687      * Writes the {@code string} as a quoted string.
688      * 
689      * @param string
690      *            The String to write.
691      * @throws IOException
692      *             On a failure writing the String.
693      */
694     protected void writeQuotedString(final String string) throws IOException {
695         if (string.indexOf('\'') < 0) {
696             mySink.write('\'');
697             mySink.write(string);
698             mySink.write('\'');
699         }
700         else if (string.indexOf('"') < 0) {
701             mySink.write('"');
702             mySink.write(string);
703             mySink.write('"');
704         }
705         else {
706             mySink.write('\'');
707             // Escape any embedded single quotes.
708             mySink.write(string.replaceAll("'", "\\\\'"));
709             mySink.write('\'');
710         }
711     }
712 }