using System;
using System.Collections.Generic;
using System.IO;
using Google.ProtocolBuffers.Descriptors;
namespace Google.ProtocolBuffers.Serialization
{
/// <summary>
/// JsonFormatWriter is a .NET 2.0 friendly json formatter for proto buffer messages. For .NET 3.5
/// you may also use the XmlFormatWriter with an XmlWriter created by the
/// <see cref="System.Runtime.Serialization.Json.JsonReaderWriterFactory">JsonReaderWriterFactory</see>.
/// </summary>
public class JsonFormatWriter : AbstractTextWriter
{
private readonly char[] _buffer;
private readonly TextWriter _output;
private readonly List<int> _counter;
private bool _isArray;
int _bufferPos;
/// <summary>
/// Constructs a JsonFormatWriter to output to a new instance of a StringWriter, use
/// the ToString() member to extract the final Json on completion.
/// </summary>
public JsonFormatWriter() : this(new StringWriter()) { }
/// <summary>
/// Constructs a JsonFormatWriter to output to the given text writer
/// </summary>
public JsonFormatWriter(TextWriter output)
{
_buffer = new char[4096];
_bufferPos = 0;
_output = output;
_counter = new List<int>();
_counter.Add(0);
}
private void WriteToOutput(string format, params object[] args)
{ WriteToOutput(String.Format(format, args)); }
private void WriteToOutput(string text)
{ WriteToOutput(text.ToCharArray(), 0, text.Length); }
private void WriteToOutput(char[] chars, int offset, int len)
{
if (_bufferPos + len >= _buffer.Length)
Flush();
if (len < _buffer.Length)
{
if (len <= 12)
{
int stop = offset + len;
for (int i = offset; i < stop; i++)
_buffer[_bufferPos++] = chars[i];
}
else
{
Buffer.BlockCopy(chars, offset << 1, _buffer, _bufferPos << 1, len << 1);
_bufferPos += len;
}
}
else
_output.Write(chars, offset, len);
}
private void WriteToOutput(char ch)
{
if (_bufferPos >= _buffer.Length)
Flush();
_buffer[_bufferPos++] = ch;
}
public override void Flush()
{
if (_bufferPos > 0)
{
_output.Write(_buffer, 0, _bufferPos);
_bufferPos = 0;
}
base.Flush();
}
/// <summary>
/// Returns the output of TextWriter.ToString() where TextWriter is the ctor argument.
/// </summary>
public override string ToString()
{ Flush(); return _output.ToString(); }
/// <summary> Sets the output formatting to use Environment.NewLine with 4-character indentions </summary>
public JsonFormatWriter Formatted()
{
NewLine = Environment.NewLine;
Indent = " ";
Whitespace = " ";
return this;
}
/// <summary> Gets or sets the characters to use for the new-line, default = empty </summary>
public string NewLine { get; set; }
/// <summary> Gets or sets the text to use for indenting, default = empty </summary>
public string Indent { get; set; }
/// <summary> Gets or sets the whitespace to use to separate the text, default = empty </summary>
public string Whitespace { get; set; }
private void Seperator()
{
if (_counter.Count == 0)
throw new InvalidOperationException("Missmatched open/close in Json writer.");
int index = _counter.Count - 1;
if (_counter[index] > 0)
WriteToOutput(',');
WriteLine(String.Empty);
_counter[index] = _counter[index] + 1;
}
private void WriteLine(string content)
{
if (!String.IsNullOrEmpty(NewLine))
{
WriteToOutput(NewLine);
for (int i = 1; i < _counter.Count; i++)
WriteToOutput(Indent);
}
else if(!String.IsNullOrEmpty(Whitespace))
WriteToOutput(Whitespace);
WriteToOutput(content);
}
private void WriteName(string field)
{
Seperator();
if (!String.IsNullOrEmpty(field))
{
WriteToOutput('"');
WriteToOutput(field);
WriteToOutput('"');
WriteToOutput(':');
if (!String.IsNullOrEmpty(Whitespace))
WriteToOutput(Whitespace);
}
}
private void EncodeText(string value)
{
char[] text = value.ToCharArray();
int len = text.Length;
int pos = 0;
while (pos < len)
{
int next = pos;
while (next < len && text[next] >= 32 && text[next] < 127 && text[next] != '\\' && text[next] != '/' && text[next] != '"')
next++;
WriteToOutput(text, pos, next - pos);
if (next < len)
{
switch (text[next])
{
case '"': WriteToOutput(@"\"""); break;
case '\\': WriteToOutput(@"\\"); break;
//odd at best to escape '/', most Json implementations don't, but it is defined in the rfc-4627
case '/': WriteToOutput(@"\/"); break;
case '\b': WriteToOutput(@"\b"); break;
case '\f': WriteToOutput(@"\f"); break;
case '\n': WriteToOutput(@"\n"); break;
case '\r': WriteToOutput(@"\r"); break;
case '\t': WriteToOutput(@"\t"); break;
default: WriteToOutput(@"\u{0:x4}", (int)text[next]); break;
}
next++;
}
pos = next;
}
}
/// <summary>
/// Writes a String value
/// </summary>
protected override void WriteAsText(string field, string textValue, object typedValue)
{
WriteName(field);
if(typedValue is bool || typedValue is int || typedValue is uint || typedValue is long || typedValue is ulong || typedValue is double || typedValue is float)
WriteToOutput(textValue);
else
{
WriteToOutput('"');
if (typedValue is string)
EncodeText(textValue);
else
WriteToOutput(textValue);
WriteToOutput('"');
}
}
/// <summary>
/// Writes a Double value
/// </summary>
protected override void Write(string field, double value)
{
if (double.IsNaN(value) || double.IsNegativeInfinity(value) || double.IsPositiveInfinity(value))
throw new InvalidOperationException("This format does not support NaN, Infinity, or -Infinity");
base.Write(field, value);
}
/// <summary>
/// Writes a Single value
/// </summary>
protected override void Write(string field, float value)
{
if (float.IsNaN(value) || float.IsNegativeInfinity(value) || float.IsPositiveInfinity(value))
throw new InvalidOperationException("This format does not support NaN, Infinity, or -Infinity");
base.Write(field, value);
}
// Treat enum as string
protected override void WriteEnum(string field, int number, string name)
{
Write(field, name);
}
/// <summary>
/// Writes an array of field values
/// </summary>
protected override void WriteArray(FieldType type, string field, System.Collections.IEnumerable items)
{
System.Collections.IEnumerator enumerator = items.GetEnumerator();
try { if (!enumerator.MoveNext()) return; }
finally { if (enumerator is IDisposable) ((IDisposable)enumerator).Dispose(); }
WriteName(field);
WriteToOutput("[");
_counter.Add(0);
base.WriteArray(type, String.Empty, items);
_counter.RemoveAt(_counter.Count - 1);
WriteLine("]");
}
/// <summary>
/// Writes a message
/// </summary>
protected override void WriteMessageOrGroup(string field, IMessageLite message)
{
WriteName(field);
WriteMessage(message);
}
/// <summary>
/// Writes the message to the the formatted stream.
/// </summary>
public override void WriteMessage(IMessageLite message)
{
if (_isArray) Seperator();
WriteToOutput("{");
_counter.Add(0);
message.WriteTo(this);
_counter.RemoveAt(_counter.Count - 1);
WriteLine("}");
Flush();
}
/// <summary>
/// Used in streaming arrays of objects to the writer
/// </summary>
/// <example>
/// <code>
/// using(writer.StartArray())
/// foreach(IMessageLite m in messages)
/// writer.WriteMessage(m);
/// </code>
/// </example>
public sealed class JsonArray : IDisposable
{
JsonFormatWriter _writer;
internal JsonArray(JsonFormatWriter writer)
{
_writer = writer;
_writer.WriteToOutput("[");
_writer._counter.Add(0);
}
/// <summary>
/// Causes the end of the array character to be written.
/// </summary>
void EndArray()
{
if (_writer != null)
{
_writer._counter.RemoveAt(_writer._counter.Count - 1);
_writer.WriteLine("]");
_writer.Flush();
}
_writer = null;
}
void IDisposable.Dispose() { EndArray(); }
}
/// <summary>
/// Used to write an array of messages as the output rather than a single message.
/// </summary>
/// <example>
/// <code>
/// using(writer.StartArray())
/// foreach(IMessageLite m in messages)
/// writer.WriteMessage(m);
/// </code>
/// </example>
public JsonArray StartArray()
{
if (_isArray) Seperator();
_isArray = true;
return new JsonArray(this);
}
}
}