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 : <code> }</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 : <code>, $scope : <scope> }</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 : <pattern>, $options : <options> }</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 }