/* Copyright 2010-2014 MongoDB Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace MongoDB.Bson.IO { /// /// Represents a BSON writer to a TextWriter (in JSON format). /// public class JsonWriter : BsonWriter { // private fields private TextWriter _textWriter; private JsonWriterSettings _jsonWriterSettings; // same value as in base class just declared as derived class private JsonWriterContext _context; // constructors /// /// Initializes a new instance of the JsonWriter class. /// /// A TextWriter. /// Optional JsonWriter settings. public JsonWriter(TextWriter writer, JsonWriterSettings settings) : base(settings) { if (writer == null) { throw new ArgumentNullException("writer"); } _textWriter = writer; _jsonWriterSettings = settings; // already frozen by base class _context = new JsonWriterContext(null, ContextType.TopLevel, ""); State = BsonWriterState.Initial; } // public methods /// /// Closes the writer. /// public override void Close() { // Close can be called on Disposed objects if (State != BsonWriterState.Closed) { Flush(); if (_jsonWriterSettings.CloseOutput) { _textWriter.Close(); } _context = null; State = BsonWriterState.Closed; } } /// /// Flushes any pending data to the output destination. /// public override void Flush() { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } _textWriter.Flush(); } /// /// Writes BSON binary data to the writer. /// /// The binary data. public override void WriteBinaryData(BsonBinaryData binaryData) { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Value && State != BsonWriterState.Initial) { ThrowInvalidState("WriteBinaryData", BsonWriterState.Value, BsonWriterState.Initial); } var subType = binaryData.SubType; var bytes = binaryData.Bytes; var guidRepresentation = binaryData.GuidRepresentation; WriteNameHelper(Name); switch (_jsonWriterSettings.OutputMode) { case JsonOutputMode.Strict: _textWriter.Write("{{ \"$binary\" : \"{0}\", \"$type\" : \"{1}\" }}", Convert.ToBase64String(bytes), ((int)subType).ToString("x2")); break; case JsonOutputMode.Shell: default: switch (subType) { case BsonBinarySubType.UuidLegacy: case BsonBinarySubType.UuidStandard: _textWriter.Write(GuidToString(subType, bytes, guidRepresentation)); break; default: _textWriter.Write("new BinData({0}, \"{1}\")", (int)subType, Convert.ToBase64String(bytes)); break; } break; } State = GetNextState(); } /// /// Writes a BSON Boolean to the writer. /// /// The Boolean value. public override void WriteBoolean(bool value) { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Value && State != BsonWriterState.Initial) { ThrowInvalidState("WriteBoolean", BsonWriterState.Value, BsonWriterState.Initial); } WriteNameHelper(Name); _textWriter.Write(value ? "true" : "false"); State = GetNextState(); } /// /// Writes BSON binary data to the writer. /// /// The bytes. public override void WriteBytes(byte[] bytes) { WriteBinaryData(new BsonBinaryData(bytes, BsonBinarySubType.Binary)); } /// /// Writes a BSON DateTime to the writer. /// /// The number of milliseconds since the Unix epoch. public override void WriteDateTime(long value) { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Value && State != BsonWriterState.Initial) { ThrowInvalidState("WriteDateTime", BsonWriterState.Value, BsonWriterState.Initial); } WriteNameHelper(Name); switch (_jsonWriterSettings.OutputMode) { case JsonOutputMode.Strict: _textWriter.Write("{{ \"$date\" : {0} }}", value); break; case JsonOutputMode.Shell: default: // use ISODate for values that fall within .NET's DateTime range, and "new Date" for all others if (value >= BsonConstants.DateTimeMinValueMillisecondsSinceEpoch && value <= BsonConstants.DateTimeMaxValueMillisecondsSinceEpoch) { var utcDateTime = BsonUtils.ToDateTimeFromMillisecondsSinceEpoch(value); _textWriter.Write("ISODate(\"{0}\")", utcDateTime.ToString("yyyy-MM-ddTHH:mm:ss.FFFZ")); } else { _textWriter.Write("new Date({0})", value); } break; } State = GetNextState(); } /// /// Writes a BSON Double to the writer. /// /// The Double value. public override void WriteDouble(double value) { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Value && State != BsonWriterState.Initial) { ThrowInvalidState("WriteDouble", BsonWriterState.Value, BsonWriterState.Initial); } // if string representation looks like an integer add ".0" so that it looks like a double var stringRepresentation = value.ToString("R", NumberFormatInfo.InvariantInfo); if (Regex.IsMatch(stringRepresentation, @"^[+-]?\d+$")) { stringRepresentation += ".0"; } WriteNameHelper(Name); _textWriter.Write(stringRepresentation); State = GetNextState(); } /// /// Writes the end of a BSON array to the writer. /// public override void WriteEndArray() { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Value) { ThrowInvalidState("WriteEndArray", BsonWriterState.Value); } base.WriteEndArray(); _textWriter.Write("]"); _context = _context.ParentContext; State = GetNextState(); } /// /// Writes the end of a BSON document to the writer. /// public override void WriteEndDocument() { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Name) { ThrowInvalidState("WriteEndDocument", BsonWriterState.Name); } base.WriteEndDocument(); if (_jsonWriterSettings.Indent && _context.HasElements) { _textWriter.Write(_jsonWriterSettings.NewLineChars); if (_context.ParentContext != null) { _textWriter.Write(_context.ParentContext.Indentation); } _textWriter.Write("}"); } else { _textWriter.Write(" }"); } if (_context.ContextType == ContextType.ScopeDocument) { _context = _context.ParentContext; WriteEndDocument(); } else { _context = _context.ParentContext; } if (_context == null) { State = BsonWriterState.Done; } else { State = GetNextState(); } } /// /// Writes a BSON Int32 to the writer. /// /// The Int32 value. public override void WriteInt32(int value) { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Value && State != BsonWriterState.Initial) { ThrowInvalidState("WriteInt32", BsonWriterState.Value, BsonWriterState.Initial); } WriteNameHelper(Name); _textWriter.Write(value); State = GetNextState(); } /// /// Writes a BSON Int64 to the writer. /// /// The Int64 value. public override void WriteInt64(long value) { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Value && State != BsonWriterState.Initial) { ThrowInvalidState("WriteInt64", BsonWriterState.Value, BsonWriterState.Initial); } WriteNameHelper(Name); switch (_jsonWriterSettings.OutputMode) { case JsonOutputMode.Strict: _textWriter.Write(value); break; case JsonOutputMode.Shell: default: if (value >= int.MinValue && value <= int.MaxValue) { _textWriter.Write("NumberLong({0})", value); } else { _textWriter.Write("NumberLong(\"{0}\")", value); } break; } State = GetNextState(); } /// /// Writes a BSON JavaScript to the writer. /// /// The JavaScript code. public override void WriteJavaScript(string code) { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Value && State != BsonWriterState.Initial) { ThrowInvalidState("WriteJavaScript", BsonWriterState.Value, BsonWriterState.Initial); } WriteNameHelper(Name); _textWriter.Write("{{ \"$code\" : \"{0}\" }}", EscapedString(code)); State = GetNextState(); } /// /// Writes a BSON JavaScript to the writer (call WriteStartDocument to start writing the scope). /// /// The JavaScript code. public override void WriteJavaScriptWithScope(string code) { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Value && State != BsonWriterState.Initial) { ThrowInvalidState("WriteJavaScriptWithScope", BsonWriterState.Value, BsonWriterState.Initial); } WriteStartDocument(); WriteString("$code", code); WriteName("$scope"); State = BsonWriterState.ScopeDocument; } /// /// Writes a BSON MaxKey to the writer. /// public override void WriteMaxKey() { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Value && State != BsonWriterState.Initial) { ThrowInvalidState("WriteMaxKey", BsonWriterState.Value, BsonWriterState.Initial); } WriteNameHelper(Name); switch (_jsonWriterSettings.OutputMode) { case JsonOutputMode.Strict: _textWriter.Write("{ \"$maxKey\" : 1 }"); break; case JsonOutputMode.Shell: default: _textWriter.Write("MaxKey"); break; } State = GetNextState(); } /// /// Writes a BSON MinKey to the writer. /// public override void WriteMinKey() { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Value && State != BsonWriterState.Initial) { ThrowInvalidState("WriteMinKey", BsonWriterState.Value, BsonWriterState.Initial); } WriteNameHelper(Name); switch (_jsonWriterSettings.OutputMode) { case JsonOutputMode.Strict: _textWriter.Write("{ \"$minKey\" : 1 }"); break; case JsonOutputMode.Shell: default: _textWriter.Write("MinKey"); break; } State = GetNextState(); } /// /// Writes a BSON null to the writer. /// public override void WriteNull() { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Value && State != BsonWriterState.Initial) { ThrowInvalidState("WriteNull", BsonWriterState.Value, BsonWriterState.Initial); } WriteNameHelper(Name); _textWriter.Write("null"); State = GetNextState(); } /// /// Writes a BSON ObjectId to the writer. /// /// The ObjectId. public override void WriteObjectId(ObjectId objectId) { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Value && State != BsonWriterState.Initial) { ThrowInvalidState("WriteObjectId", BsonWriterState.Value, BsonWriterState.Initial); } var bytes = ObjectId.Pack(objectId.Timestamp, objectId.Machine, objectId.Pid, objectId.Increment); WriteNameHelper(Name); switch (_jsonWriterSettings.OutputMode) { case JsonOutputMode.Strict: _textWriter.Write("{{ \"$oid\" : \"{0}\" }}", BsonUtils.ToHexString(bytes)); break; case JsonOutputMode.Shell: default: _textWriter.Write("ObjectId(\"{0}\")", BsonUtils.ToHexString(bytes)); break; } State = GetNextState(); } /// /// Writes a BSON regular expression to the writer. /// /// A BsonRegularExpression. public override void WriteRegularExpression(BsonRegularExpression regex) { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Value && State != BsonWriterState.Initial) { ThrowInvalidState("WriteRegularExpression", BsonWriterState.Value, BsonWriterState.Initial); } var pattern = regex.Pattern; var options = regex.Options; WriteNameHelper(Name); switch (_jsonWriterSettings.OutputMode) { case JsonOutputMode.Strict: _textWriter.Write("{{ \"$regex\" : \"{0}\", \"$options\" : \"{1}\" }}", EscapedString(pattern), EscapedString(options)); break; case JsonOutputMode.Shell: default: var escapedPattern = (pattern == "") ? "(?:)" : pattern.Replace("/", @"\/"); _textWriter.Write("/{0}/{1}", escapedPattern, options); break; } State = GetNextState(); } /// /// Writes the start of a BSON array to the writer. /// public override void WriteStartArray() { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Value && State != BsonWriterState.Initial) { ThrowInvalidState("WriteStartArray", BsonWriterState.Value, BsonWriterState.Initial); } base.WriteStartArray(); WriteNameHelper(Name); _textWriter.Write("["); _context = new JsonWriterContext(_context, ContextType.Array, _jsonWriterSettings.IndentChars); State = BsonWriterState.Value; } /// /// Writes the start of a BSON document to the writer. /// public override void WriteStartDocument() { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Value && State != BsonWriterState.Initial && State != BsonWriterState.ScopeDocument) { ThrowInvalidState("WriteStartDocument", BsonWriterState.Value, BsonWriterState.Initial, BsonWriterState.ScopeDocument); } base.WriteStartDocument(); if (State == BsonWriterState.Value || State == BsonWriterState.ScopeDocument) { WriteNameHelper(Name); } _textWriter.Write("{"); var contextType = (State == BsonWriterState.ScopeDocument) ? ContextType.ScopeDocument : ContextType.Document; _context = new JsonWriterContext(_context, contextType, _jsonWriterSettings.IndentChars); State = BsonWriterState.Name; } /// /// Writes a BSON String to the writer. /// /// The String value. public override void WriteString(string value) { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Value && State != BsonWriterState.Initial) { ThrowInvalidState("WriteString", BsonWriterState.Value, BsonWriterState.Initial); } WriteNameHelper(Name); WriteQuotedString(value); State = GetNextState(); } /// /// Writes a BSON Symbol to the writer. /// /// The symbol. public override void WriteSymbol(string value) { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Value && State != BsonWriterState.Initial) { ThrowInvalidState("WriteSymbol", BsonWriterState.Value, BsonWriterState.Initial); } WriteNameHelper(Name); _textWriter.Write("{{ \"$symbol\" : \"{0}\" }}", EscapedString(value)); State = GetNextState(); } /// /// Writes a BSON timestamp to the writer. /// /// The combined timestamp/increment value. public override void WriteTimestamp(long value) { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Value && State != BsonWriterState.Initial) { ThrowInvalidState("WriteTimestamp", BsonWriterState.Value, BsonWriterState.Initial); } var secondsSinceEpoch = (int)((value >> 32) & 0xffffffff); var increment = (int)(value & 0xffffffff); WriteNameHelper(Name); switch (_jsonWriterSettings.OutputMode) { case JsonOutputMode.Strict: _textWriter.Write("{{ \"$timestamp\" : {{ \"t\" : {0}, \"i\" : {1} }} }}", secondsSinceEpoch, increment); break; case JsonOutputMode.Shell: default: _textWriter.Write("Timestamp({0}, {1})", secondsSinceEpoch, increment); break; } State = GetNextState(); } /// /// Writes a BSON undefined to the writer. /// public override void WriteUndefined() { if (Disposed) { throw new ObjectDisposedException("JsonWriter"); } if (State != BsonWriterState.Value && State != BsonWriterState.Initial) { ThrowInvalidState("WriteUndefined", BsonWriterState.Value, BsonWriterState.Initial); } WriteNameHelper(Name); switch (_jsonWriterSettings.OutputMode) { case JsonOutputMode.Strict: _textWriter.Write("{ \"$undefined\" : true }"); break; case JsonOutputMode.Shell: default: _textWriter.Write("undefined"); break; } State = GetNextState(); } // protected methods /// /// Disposes of any resources used by the writer. /// /// True if called from Dispose. protected override void Dispose(bool disposing) { if (disposing) { Close(); _textWriter.Dispose(); } base.Dispose(disposing); } // private methods private string EscapedString(string value) { if (value.All(c => !NeedsEscaping(c))) { return value; } var sb = new StringBuilder(value.Length); foreach (char c in value) { switch (c) { case '"': sb.Append("\\\""); break; case '\\': sb.Append("\\\\"); break; case '\b': sb.Append("\\b"); break; case '\f': sb.Append("\\f"); break; case '\n': sb.Append("\\n"); break; case '\r': sb.Append("\\r"); break; case '\t': sb.Append("\\t"); break; default: switch (char.GetUnicodeCategory(c)) { case UnicodeCategory.UppercaseLetter: case UnicodeCategory.LowercaseLetter: case UnicodeCategory.TitlecaseLetter: case UnicodeCategory.OtherLetter: case UnicodeCategory.DecimalDigitNumber: case UnicodeCategory.LetterNumber: case UnicodeCategory.OtherNumber: case UnicodeCategory.SpaceSeparator: case UnicodeCategory.ConnectorPunctuation: case UnicodeCategory.DashPunctuation: case UnicodeCategory.OpenPunctuation: case UnicodeCategory.ClosePunctuation: case UnicodeCategory.InitialQuotePunctuation: case UnicodeCategory.FinalQuotePunctuation: case UnicodeCategory.OtherPunctuation: case UnicodeCategory.MathSymbol: case UnicodeCategory.CurrencySymbol: case UnicodeCategory.ModifierSymbol: case UnicodeCategory.OtherSymbol: sb.Append(c); break; default: sb.AppendFormat("\\u{0:x4}", (int)c); break; } break; } } return sb.ToString(); } private BsonWriterState GetNextState() { if (_context.ContextType == ContextType.Array) { return BsonWriterState.Value; } else { return BsonWriterState.Name; } } private string GuidToString(BsonBinarySubType subType, byte[] bytes, GuidRepresentation guidRepresentation) { if (bytes.Length != 16) { var message = string.Format("Length of binary subtype {0} must be 16, not {1}.", subType, bytes.Length); throw new ArgumentException(message); } if (subType == BsonBinarySubType.UuidLegacy && guidRepresentation == GuidRepresentation.Standard) { throw new ArgumentException("GuidRepresentation for binary subtype UuidLegacy must not be Standard."); } if (subType == BsonBinarySubType.UuidStandard && guidRepresentation != GuidRepresentation.Standard) { var message = string.Format("GuidRepresentation for binary subtype UuidStandard must be Standard, not {0}.", guidRepresentation); throw new ArgumentException(message); } if (guidRepresentation == GuidRepresentation.Unspecified) { var s = BsonUtils.ToHexString(bytes); var parts = new string[] { s.Substring(0, 8), s.Substring(8, 4), s.Substring(12, 4), s.Substring(16, 4), s.Substring(20, 12) }; return string.Format("HexData({0}, \"{1}\")", (int)subType, string.Join("-", parts)); } else { string uuidConstructorName; switch (guidRepresentation) { case GuidRepresentation.CSharpLegacy: uuidConstructorName = "CSUUID"; break; case GuidRepresentation.JavaLegacy: uuidConstructorName = "JUUID"; break; case GuidRepresentation.PythonLegacy: uuidConstructorName = "PYUUID"; break; case GuidRepresentation.Standard: uuidConstructorName = "UUID"; break; default: throw new BsonInternalException("Unexpected GuidRepresentation"); } var guid = GuidConverter.FromBytes(bytes, guidRepresentation); return string.Format("{0}(\"{1}\")", uuidConstructorName, guid.ToString()); } } private bool NeedsEscaping(char c) { switch (c) { case '"': case '\\': case '\b': case '\f': case '\n': case '\r': case '\t': return true; default: switch (char.GetUnicodeCategory(c)) { case UnicodeCategory.UppercaseLetter: case UnicodeCategory.LowercaseLetter: case UnicodeCategory.TitlecaseLetter: case UnicodeCategory.OtherLetter: case UnicodeCategory.DecimalDigitNumber: case UnicodeCategory.LetterNumber: case UnicodeCategory.OtherNumber: case UnicodeCategory.SpaceSeparator: case UnicodeCategory.ConnectorPunctuation: case UnicodeCategory.DashPunctuation: case UnicodeCategory.OpenPunctuation: case UnicodeCategory.ClosePunctuation: case UnicodeCategory.InitialQuotePunctuation: case UnicodeCategory.FinalQuotePunctuation: case UnicodeCategory.OtherPunctuation: case UnicodeCategory.MathSymbol: case UnicodeCategory.CurrencySymbol: case UnicodeCategory.ModifierSymbol: case UnicodeCategory.OtherSymbol: return false; default: return true; } } } private void WriteNameHelper(string name) { switch (_context.ContextType) { case ContextType.Array: // don't write Array element names in Json if (_context.HasElements) { _textWriter.Write(", "); } break; case ContextType.Document: case ContextType.ScopeDocument: if (_context.HasElements) { _textWriter.Write(","); } if (_jsonWriterSettings.Indent) { _textWriter.Write(_jsonWriterSettings.NewLineChars); _textWriter.Write(_context.Indentation); } else { _textWriter.Write(" "); } WriteQuotedString(name); _textWriter.Write(" : "); break; case ContextType.TopLevel: break; default: throw new BsonInternalException("Invalid ContextType."); } _context.HasElements = true; } private void WriteQuotedString(string value) { _textWriter.Write("\""); _textWriter.Write(EscapedString(value)); _textWriter.Write("\""); } } }