package org.emfjson.mongo.bson.codecs; import org.bson.*; import org.bson.json.JsonMode; import org.bson.json.JsonWriterSettings; import org.bson.types.ObjectId; import java.io.IOException; import java.io.Writer; import static javax.xml.bind.DatatypeConverter.printBase64Binary; public class JsonWriter extends org.bson.json.JsonWriter { private final JsonWriterSettings settings; public JsonWriter(Writer writer) { this(writer, new JsonWriterSettings()); } public JsonWriter(final Writer writer, final JsonWriterSettings settings) { super(writer, settings); this.settings = settings; if (settings.getOutputMode().equals(JsonMode.SHELL)) throw new IllegalArgumentException("JsonMode must not be SHELL"); setContext(new Context(null, BsonContextType.TOP_LEVEL, "")); } @Override protected void doWriteStartDocument() { try { if (getState() == State.VALUE || getState() == State.SCOPE_DOCUMENT) { writeNameHelper(getName()); } getWriter().write("{"); BsonContextType contextType = (getState() == State.SCOPE_DOCUMENT) ? BsonContextType.SCOPE_DOCUMENT : BsonContextType.DOCUMENT; setContext(new Context(getContext(), contextType, settings.getIndentCharacters())); } catch (IOException e) { throwBSONException(e); } } @Override protected void doWriteStartArray() { try { writeNameHelper(getName()); getWriter().write("["); setContext(new Context(getContext(), BsonContextType.ARRAY, settings.getIndentCharacters())); } catch (IOException e) { throwBSONException(e); } } @Override protected void doWriteBinaryData(final BsonBinary binary) { writeStartDocument(); writeString("$binary", printBase64Binary(binary.getData())); writeString("$type", Integer.toHexString(binary.getType() & 0xFF)); writeEndDocument(); } @Override public void doWriteBoolean(final boolean value) { try { writeNameHelper(getName()); getWriter().write(value ? "true": "false"); } catch (IOException e) { throwBSONException(e); } } @Override protected void doWriteDateTime(final long value) { try { writeNameHelper(getName()); getWriter().write(Long.toString(value)); } catch (IOException e) { throwBSONException(e); } } @Override protected void doWriteDBPointer(final BsonDbPointer value) { writeStartDocument(); writeString("$ref", value.getNamespace()); writeObjectId("$id", value.getId()); writeEndDocument(); } @Override protected void doWriteDouble(final double value) { try { writeNameHelper(getName()); getWriter().write(Double.toString(value)); setState(getNextState()); } catch (IOException e) { throwBSONException(e); } } @Override protected void doWriteInt32(final int value) { try { writeNameHelper(getName()); getWriter().write(Integer.toString(value)); } catch (IOException e) { throwBSONException(e); } } @Override protected void doWriteInt64(final long value) { try { writeNameHelper(getName()); getWriter().write(Long.toString(value)); } catch (IOException e) { throwBSONException(e); } } @Override protected void doWriteJavaScript(final String code) { writeStartDocument(); writeString("$code", code); writeEndDocument(); } @Override protected void doWriteJavaScriptWithScope(final String code) { writeStartDocument(); writeString("$code", code); writeName("$scope"); } @Override protected void doWriteMaxKey() { writeStartDocument(); writeInt32("$maxKey", 1); writeEndDocument(); } @Override protected void doWriteMinKey() { writeStartDocument(); writeInt32("$minKey", 1); writeEndDocument(); } @Override public void doWriteNull() { try { writeNameHelper(getName()); getWriter().write("null"); } catch (IOException e) { throwBSONException(e); } } @Override public void doWriteObjectId(final ObjectId objectId) { writeStartDocument(); writeString("$oid", objectId.toString()); writeEndDocument(); } @Override public void doWriteRegularExpression(final BsonRegularExpression regularExpression) { try { writeNameHelper(getName()); getWriter().write("/"); String escaped = (regularExpression.getPattern().equals("")) ? "(?:)" : regularExpression.getPattern().replace("/", "\\/"); getWriter().write(escaped); getWriter().write("/"); getWriter().write(regularExpression.getOptions()); } catch (IOException e) { throwBSONException(e); } } @Override public void doWriteString(final String value) { try { writeNameHelper(getName()); writeStringHelper(value); } catch (IOException e) { throwBSONException(e); } } @Override public void doWriteSymbol(final String value) { writeStartDocument(); writeString("$symbol", value); writeEndDocument(); } @Override public void doWriteTimestamp(final BsonTimestamp value) { writeStartDocument(); writeStartDocument("$timestamp"); writeInt32("t", value.getTime()); writeInt32("i", value.getInc()); writeEndDocument(); writeEndDocument(); } @Override public void doWriteUndefined() { try { writeNameHelper(getName()); getWriter().write("undefined"); } catch (IOException e) { throwBSONException(e); } } private void throwBSONException(final IOException e) { throw new BSONException("Wrapping IOException", e); } private void writeNameHelper(final String name) throws IOException { switch (getContext().getContextType()) { case ARRAY: // don't write Array element names in JSON if (getContext().hasElements()) { getWriter().write(", "); } break; case DOCUMENT: case SCOPE_DOCUMENT: if (getContext().hasElements()) { getWriter().write(","); } if (settings.isIndent()) { getWriter().write(settings.getNewLineCharacters()); getWriter().write(getContext().getIndentation()); } else { getWriter().write(" "); } writeStringHelper(name); getWriter().write(" : "); break; case TOP_LEVEL: break; default: throw new BSONException("Invalid contextType."); } getContext().hasElements(true); } private void writeStringHelper(final String str) throws IOException { getWriter().write('"'); for (final char c : str.toCharArray()) { switch (c) { case '"': getWriter().write("\\\""); break; case '\\': getWriter().write("\\\\"); break; case '\b': getWriter().write("\\b"); break; case '\f': getWriter().write("\\f"); break; case '\n': getWriter().write("\\n"); break; case '\r': getWriter().write("\\r"); break; case '\t': getWriter().write("\\t"); break; default: switch (Character.getType(c)) { case Character.UPPERCASE_LETTER: case Character.LOWERCASE_LETTER: case Character.TITLECASE_LETTER: case Character.OTHER_LETTER: case Character.DECIMAL_DIGIT_NUMBER: case Character.LETTER_NUMBER: case Character.OTHER_NUMBER: case Character.SPACE_SEPARATOR: case Character.CONNECTOR_PUNCTUATION: case Character.DASH_PUNCTUATION: case Character.START_PUNCTUATION: case Character.END_PUNCTUATION: case Character.INITIAL_QUOTE_PUNCTUATION: case Character.FINAL_QUOTE_PUNCTUATION: case Character.OTHER_PUNCTUATION: case Character.MATH_SYMBOL: case Character.CURRENCY_SYMBOL: case Character.MODIFIER_SYMBOL: case Character.OTHER_SYMBOL: getWriter().write(c); break; default: getWriter().write("\\u"); getWriter().write(Integer.toHexString((c & 0xf000) >> 12)); getWriter().write(Integer.toHexString((c & 0x0f00) >> 8)); getWriter().write(Integer.toHexString((c & 0x00f0) >> 4)); getWriter().write(Integer.toHexString(c & 0x000f)); break; } break; } } getWriter().write('"'); } @Override protected Context getContext() { return (Context) super.getContext(); } /** * The context for the writer, inheriting all the values from * {@link org.bson.AbstractBsonWriter.Context}, and additionally providing * settings for the indentation level and whether there are any child * elements at this level. */ public class Context extends org.bson.json.JsonWriter.Context { private final String indentation; public String getIndentation() { return indentation; } private boolean hasElements; public boolean hasElements() { return hasElements; } public void hasElements(boolean hasElements) { this.hasElements = hasElements; } /** * Creates a new context. * * @param parentContext the parent context that can be used for going back up to * the parent level * @param contextType the type of this context * @param indentChars the String to use for indentation at this level. */ public Context(final Context parentContext, final BsonContextType contextType, final String indentChars) { super(parentContext, contextType, indentChars); this.indentation = (parentContext == null) ? indentChars: parentContext.indentation + indentChars; } } }