// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. namespace LightJson.Serialization { using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using ErrorType = JsonSerializationException.ErrorType; /// /// Represents a writer that can write string representations of JsonValues. /// internal sealed class JsonWriter : IDisposable { private int indent; private bool isNewLine; private TextWriter writer; /// /// A set of containing all the collection objects (JsonObject/JsonArray) being rendered. /// It is used to prevent circular references; since collections that contain themselves /// will never finish rendering. /// private HashSet> renderingCollections; /// /// Initializes a new instance of the class. /// public JsonWriter() : this(false) { } /// /// Initializes a new instance of the class. /// /// /// A value indicating whether the output of the writer should be human-readable. /// public JsonWriter(bool pretty) { if (pretty) { this.IndentString = "\t"; this.SpacingString = " "; this.NewLineString = "\n"; } } /// /// Gets or sets the string representing a indent in the output. /// /// /// The string representing a indent in the output. /// public string IndentString { get; set; } /// /// Gets or sets the string representing a space in the output. /// /// /// The string representing a space in the output. /// public string SpacingString { get; set; } /// /// Gets or sets the string representing a new line on the output. /// /// /// The string representing a new line on the output. /// public string NewLineString { get; set; } /// /// Gets or sets a value indicating whether JsonObject properties should be written in a deterministic order. /// /// /// A value indicating whether JsonObject properties should be written in a deterministic order. /// public bool SortObjects { get; set; } /// /// Returns a string representation of the given JsonValue. /// /// The JsonValue to serialize. /// The serialized value. public string Serialize(JsonValue jsonValue) { this.Initialize(); this.Render(jsonValue); return this.writer.ToString(); } /// /// Releases all the resources used by this object. /// public void Dispose() { if (this.writer != null) { this.writer.Dispose(); } } private static bool IsValidNumber(double number) { return !(double.IsNaN(number) || double.IsInfinity(number)); } private void Initialize() { this.indent = 0; this.isNewLine = true; this.writer = new StringWriter(); this.renderingCollections = new HashSet>(); } private void Write(string text) { if (this.isNewLine) { this.isNewLine = false; this.WriteIndentation(); } this.writer.Write(text); } private void WriteEncodedJsonValue(JsonValue value) { switch (value.Type) { case JsonValueType.Null: this.Write("null"); break; case JsonValueType.Boolean: this.Write(value.AsString); break; case JsonValueType.Number: this.Write(((double)value).ToString(CultureInfo.InvariantCulture)); break; default: Debug.Assert(value.Type == JsonValueType.String, "value.Type == JsonValueType.String"); this.WriteEncodedString((string)value); break; } } private void WriteEncodedString(string text) { this.Write("\""); for (int i = 0; i < text.Length; i += 1) { var currentChar = text[i]; // Encoding special characters. switch (currentChar) { case '\\': this.writer.Write("\\\\"); break; case '\"': this.writer.Write("\\\""); break; case '/': this.writer.Write("\\/"); break; case '\b': this.writer.Write("\\b"); break; case '\f': this.writer.Write("\\f"); break; case '\n': this.writer.Write("\\n"); break; case '\r': this.writer.Write("\\r"); break; case '\t': this.writer.Write("\\t"); break; default: this.writer.Write(currentChar); break; } } this.writer.Write("\""); } private void WriteIndentation() { for (var i = 0; i < this.indent; i += 1) { this.Write(this.IndentString); } } private void WriteSpacing() { this.Write(this.SpacingString); } private void WriteLine() { this.Write(this.NewLineString); this.isNewLine = true; } private void WriteLine(string line) { this.Write(line); this.WriteLine(); } private void AddRenderingCollection(IEnumerable value) { if (!this.renderingCollections.Add(value)) { throw new JsonSerializationException(ErrorType.CircularReference); } } private void RemoveRenderingCollection(IEnumerable value) { this.renderingCollections.Remove(value); } private void Render(JsonValue value) { switch (value.Type) { case JsonValueType.Null: case JsonValueType.Boolean: case JsonValueType.Number: case JsonValueType.String: this.WriteEncodedJsonValue(value); break; case JsonValueType.Object: this.Render((JsonObject)value); break; case JsonValueType.Array: this.Render((JsonArray)value); break; default: throw new JsonSerializationException(ErrorType.InvalidValueType); } } private void Render(JsonArray value) { this.AddRenderingCollection(value); this.WriteLine("["); this.indent += 1; using (var enumerator = value.GetEnumerator()) { var hasNext = enumerator.MoveNext(); while (hasNext) { this.Render(enumerator.Current); hasNext = enumerator.MoveNext(); if (hasNext) { this.WriteLine(","); } else { this.WriteLine(); } } } this.indent -= 1; this.Write("]"); this.RemoveRenderingCollection(value); } private void Render(JsonObject value) { this.AddRenderingCollection(value); this.WriteLine("{"); this.indent += 1; using (var enumerator = this.GetJsonObjectEnumerator(value)) { var hasNext = enumerator.MoveNext(); while (hasNext) { this.WriteEncodedString(enumerator.Current.Key); this.Write(":"); this.WriteSpacing(); this.Render(enumerator.Current.Value); hasNext = enumerator.MoveNext(); if (hasNext) { this.WriteLine(","); } else { this.WriteLine(); } } } this.indent -= 1; this.Write("}"); this.RemoveRenderingCollection(value); } /// /// Gets an JsonObject enumerator based on the configuration of this JsonWriter. /// If JsonWriter.SortObjects is set to true, then a ordered enumerator is returned. /// Otherwise, a faster non-deterministic enumerator is returned. /// /// The JsonObject for which to get an enumerator. /// An enumerator for the properties in a . private IEnumerator> GetJsonObjectEnumerator(JsonObject jsonObject) { if (this.SortObjects) { var sortedDictionary = new SortedDictionary(StringComparer.Ordinal); foreach (var item in jsonObject) { sortedDictionary.Add(item.Key, item.Value); } return sortedDictionary.GetEnumerator(); } else { return jsonObject.GetEnumerator(); } } } }