// 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();
}
}
}
}