Commit 2cf6e1b0 authored by csharptest's avatar csharptest Committed by rogerk

Last change for http support, adding a simple reader for query strings and/or

url-encoded form data to support restful apis.  Also exposed the mime to reader
and mime to writer via dictionaries in the MessageFormatOptions structure.
parent c86b5044
using System;
using System.IO;
using System.Text;
namespace Google.ProtocolBuffers.Serialization.Http
{
/// <summary>
/// Allows reading messages from a name/value dictionary
/// </summary>
public class FormUrlEncodedReader : AbstractTextReader
{
private readonly TextReader _input;
private string _fieldName, _fieldValue;
private bool _ready;
/// <summary>
/// Creates a dictionary reader from an enumeration of KeyValuePair data, like an IDictionary
/// </summary>
FormUrlEncodedReader(TextReader input)
{
_input = input;
int ch = input.Peek();
if (ch == '?')
{
input.Read();
}
_ready = ReadNext();
}
#region CreateInstance overloads
/// <summary>
/// Constructs a FormUrlEncodedReader to parse form data, or url query text into a message.
/// </summary>
public static FormUrlEncodedReader CreateInstance(Stream stream)
{
return new FormUrlEncodedReader(new StreamReader(stream, Encoding.UTF8, false));
}
/// <summary>
/// Constructs a FormUrlEncodedReader to parse form data, or url query text into a message.
/// </summary>
public static FormUrlEncodedReader CreateInstance(byte[] bytes)
{
return new FormUrlEncodedReader(new StreamReader(new MemoryStream(bytes, false), Encoding.UTF8, false));
}
/// <summary>
/// Constructs a FormUrlEncodedReader to parse form data, or url query text into a message.
/// </summary>
public static FormUrlEncodedReader CreateInstance(string text)
{
return new FormUrlEncodedReader(new StringReader(text));
}
/// <summary>
/// Constructs a FormUrlEncodedReader to parse form data, or url query text into a message.
/// </summary>
public static FormUrlEncodedReader CreateInstance(TextReader input)
{
return new FormUrlEncodedReader(input);
}
#endregion
private bool ReadNext()
{
StringBuilder field = new StringBuilder(32);
StringBuilder value = new StringBuilder(64);
int ch;
while (-1 != (ch = _input.Read()) && ch != '=' && ch != '&')
{
field.Append((char)ch);
}
if (ch != -1 && ch != '&')
{
while (-1 != (ch = _input.Read()) && ch != '&')
{
value.Append((char)ch);
}
}
_fieldName = field.ToString();
_fieldValue = Uri.UnescapeDataString(value.Replace('+', ' ').ToString());
return !String.IsNullOrEmpty(_fieldName);
}
/// <summary>
/// No-op
/// </summary>
public override void ReadMessageStart()
{ }
/// <summary>
/// No-op
/// </summary>
public override void ReadMessageEnd()
{ }
/// <summary>
/// Merges the contents of stream into the provided message builder
/// </summary>
public override TBuilder Merge<TBuilder>(TBuilder builder, ExtensionRegistry registry)
{
builder.WeakMergeFrom(this, registry);
return builder;
}
/// <summary>
/// Causes the reader to skip past this field
/// </summary>
protected override void Skip()
{
_ready = ReadNext();
}
/// <summary>
/// Peeks at the next field in the input stream and returns what information is available.
/// </summary>
/// <remarks>
/// This may be called multiple times without actually reading the field. Only after the field
/// is either read, or skipped, should PeekNext return a different value.
/// </remarks>
protected override bool PeekNext(out string field)
{
field = _ready ? _fieldName : null;
return field != null;
}
/// <summary>
/// Returns true if it was able to read a String from the input
/// </summary>
protected override bool ReadAsText(ref string value, Type typeInfo)
{
if (_ready)
{
value = _fieldValue;
_ready = ReadNext();
return true;
}
return false;
}
/// <summary>
/// It's unlikely this will work for anything but text data as bytes UTF8 are transformed to text and back to bytes
/// </summary>
protected override ByteString DecodeBytes(string bytes)
{ return ByteString.CopyFromUtf8(bytes); }
/// <summary>
/// Not Supported
/// </summary>
public override bool ReadGroup(IBuilderLite value, ExtensionRegistry registry)
{ throw new NotSupportedException(); }
/// <summary>
/// Not Supported
/// </summary>
protected override bool ReadMessage(IBuilderLite builder, ExtensionRegistry registry)
{ throw new NotSupportedException(); }
}
}
\ No newline at end of file
......@@ -19,27 +19,14 @@ namespace Google.ProtocolBuffers.Serialization.Http
/// <returns>The ICodedInputStream that can be given to the IBuilder.MergeFrom(...) method</returns>
public static ICodedInputStream CreateInputStream(MessageFormatOptions options, string contentType, Stream input)
{
FormatType inputType = ContentTypeToFormat(contentType, options.DefaultContentType);
ICodedInputStream codedInput = ContentTypeToInputStream(contentType, options, input);
ICodedInputStream codedInput;
if (inputType == FormatType.ProtoBuffer)
if (codedInput is XmlFormatReader)
{
codedInput = CodedInputStream.CreateInstance(input);
}
else if (inputType == FormatType.Json)
{
JsonFormatReader reader = JsonFormatReader.CreateInstance(input);
codedInput = reader;
}
else if (inputType == FormatType.Xml)
{
XmlFormatReader reader = XmlFormatReader.CreateInstance(input);
XmlFormatReader reader = (XmlFormatReader)codedInput;
reader.RootElementName = options.XmlReaderRootElementName;
reader.Options = options.XmlReaderOptions;
codedInput = reader;
}
else
throw new NotSupportedException();
return codedInput;
}
......@@ -69,30 +56,20 @@ namespace Google.ProtocolBuffers.Serialization.Http
/// <remarks> If you do not dispose of ICodedOutputStream some formats may yield incomplete output </remarks>
public static ICodedOutputStream CreateOutputStream(MessageFormatOptions options, string contentType, Stream output)
{
FormatType outputType = ContentTypeToFormat(contentType, options.DefaultContentType);
ICodedOutputStream codedOutput = ContentTypeToOutputStream(contentType, options, output);
ICodedOutputStream codedOutput;
if (outputType == FormatType.ProtoBuffer)
{
codedOutput = CodedOutputStream.CreateInstance(output);
}
else if (outputType == FormatType.Json)
if (codedOutput is JsonFormatWriter)
{
JsonFormatWriter writer = JsonFormatWriter.CreateInstance(output);
JsonFormatWriter writer = (JsonFormatWriter)codedOutput;
if (options.FormattedOutput)
{
writer.Formatted();
}
codedOutput = writer;
}
else if (outputType == FormatType.Xml)
else if (codedOutput is XmlFormatWriter)
{
XmlFormatWriter writer;
if (!options.FormattedOutput)
{
writer = XmlFormatWriter.CreateInstance(output);
}
else
XmlFormatWriter writer = (XmlFormatWriter)codedOutput;
if (options.FormattedOutput)
{
XmlWriterSettings settings = new XmlWriterSettings()
{
......@@ -104,14 +81,12 @@ namespace Google.ProtocolBuffers.Serialization.Http
IndentChars = " ",
NewLineChars = Environment.NewLine,
};
writer = XmlFormatWriter.CreateInstance(XmlWriter.Create(output, settings));
// Don't know how else to change xml writer options?
codedOutput = writer = XmlFormatWriter.CreateInstance(XmlWriter.Create(output, settings));
}
writer.RootElementName = options.XmlWriterRootElementName;
writer.Options = options.XmlWriterOptions;
codedOutput = writer;
}
else
throw new NotSupportedException();
return codedOutput;
}
......@@ -137,46 +112,39 @@ namespace Google.ProtocolBuffers.Serialization.Http
codedOutput.WriteMessageEnd();
}
enum FormatType { ProtoBuffer, Json, Xml };
private static ICodedInputStream ContentTypeToInputStream(string contentType, MessageFormatOptions options, Stream input)
{
contentType = (contentType ?? String.Empty).Split(';')[0].Trim();
private static FormatType ContentTypeToFormat(string contentType, string defaultType)
Converter<Stream, ICodedInputStream> factory;
if(!options.MimeInputTypesReadOnly.TryGetValue(contentType, out factory) || factory == null)
{
if(String.IsNullOrEmpty(options.DefaultContentType) ||
!options.MimeInputTypesReadOnly.TryGetValue(options.DefaultContentType, out factory) || factory == null)
{
throw new ArgumentOutOfRangeException("contentType");
}
}
return factory(input);
}
private static ICodedOutputStream ContentTypeToOutputStream(string contentType, MessageFormatOptions options, Stream output)
{
switch ((contentType ?? String.Empty).Split(';')[0].Trim().ToLower())
contentType = (contentType ?? String.Empty).Split(';')[0].Trim();
Converter<Stream, ICodedOutputStream> factory;
if (!options.MimeOutputTypesReadOnly.TryGetValue(contentType, out factory) || factory == null)
{
case "application/json":
case "application/x-json":
case "application/x-javascript":
case "text/javascript":
case "text/x-javascript":
case "text/x-json":
case "text/json":
{
return FormatType.Json;
}
case "text/xml":
case "application/xml":
{
return FormatType.Xml;
}
case "application/binary":
case "application/x-protobuf":
case "application/vnd.google.protobuf":
{
return FormatType.ProtoBuffer;
}
case "":
case null:
if (!String.IsNullOrEmpty(defaultType))
{
return ContentTypeToFormat(defaultType, null);
}
break;
if (String.IsNullOrEmpty(options.DefaultContentType) ||
!options.MimeOutputTypesReadOnly.TryGetValue(options.DefaultContentType, out factory) || factory == null)
{
throw new ArgumentOutOfRangeException("contentType");
}
}
throw new ArgumentOutOfRangeException("contentType");
return factory(output);
}
}
}
\ No newline at end of file
using System;
using System.IO;
using System.Collections.Generic;
using Google.ProtocolBuffers.Collections;
namespace Google.ProtocolBuffers.Serialization.Http
{
......@@ -22,10 +25,92 @@ namespace Google.ProtocolBuffers.Serialization.Http
/// </remarks>
public const string ContentTypeJson = "application/json";
/// <summary>The mime type for query strings and x-www-form-urlencoded content</summary>
/// <remarks>This mime type is input-only</remarks>
public const string ContentFormUrlEncoded = "application/x-www-form-urlencoded";
/// <summary>
/// Default mime-type handling for input
/// </summary>
private static readonly IDictionary<string, Converter<Stream, ICodedInputStream>> MimeInputDefaults =
new ReadOnlyDictionary<string, Converter<Stream, ICodedInputStream>>(
new Dictionary<string, Converter<Stream, ICodedInputStream>>(StringComparer.OrdinalIgnoreCase)
{
{"application/json", JsonFormatReader.CreateInstance},
{"application/x-json", JsonFormatReader.CreateInstance},
{"application/x-javascript", JsonFormatReader.CreateInstance},
{"text/javascript", JsonFormatReader.CreateInstance},
{"text/x-javascript", JsonFormatReader.CreateInstance},
{"text/x-json", JsonFormatReader.CreateInstance},
{"text/json", JsonFormatReader.CreateInstance},
{"text/xml", XmlFormatReader.CreateInstance},
{"application/xml", XmlFormatReader.CreateInstance},
{"application/binary", CodedInputStream.CreateInstance},
{"application/x-protobuf", CodedInputStream.CreateInstance},
{"application/vnd.google.protobuf", CodedInputStream.CreateInstance},
{"application/x-www-form-urlencoded", FormUrlEncodedReader.CreateInstance},
}
);
/// <summary>
/// Default mime-type handling for output
/// </summary>
private static readonly IDictionary<string, Converter<Stream, ICodedOutputStream>> MimeOutputDefaults =
new ReadOnlyDictionary<string, Converter<Stream, ICodedOutputStream>>(
new Dictionary<string, Converter<Stream, ICodedOutputStream>>(StringComparer.OrdinalIgnoreCase)
{
{"application/json", JsonFormatWriter.CreateInstance},
{"application/x-json", JsonFormatWriter.CreateInstance},
{"application/x-javascript", JsonFormatWriter.CreateInstance},
{"text/javascript", JsonFormatWriter.CreateInstance},
{"text/x-javascript", JsonFormatWriter.CreateInstance},
{"text/x-json", JsonFormatWriter.CreateInstance},
{"text/json", JsonFormatWriter.CreateInstance},
{"text/xml", XmlFormatWriter.CreateInstance},
{"application/xml", XmlFormatWriter.CreateInstance},
{"application/binary", CodedOutputStream.CreateInstance},
{"application/x-protobuf", CodedOutputStream.CreateInstance},
{"application/vnd.google.protobuf", CodedOutputStream.CreateInstance},
}
);
private string _defaultContentType;
private string _xmlReaderRootElementName;
private string _xmlWriterRootElementName;
private ExtensionRegistry _extensionRegistry;
private Dictionary<string, Converter<Stream, ICodedInputStream>> _mimeInputTypes;
private Dictionary<string, Converter<Stream, ICodedOutputStream>> _mimeOutputTypes;
/// <summary> Provides access to modify the mime-type input stream construction </summary>
public IDictionary<string, Converter<Stream, ICodedInputStream>> MimeInputTypes
{
get
{
return _mimeInputTypes ??
(_mimeInputTypes = new Dictionary<string, Converter<Stream, ICodedInputStream>>(
MimeInputDefaults, StringComparer.OrdinalIgnoreCase));
}
}
/// <summary> Provides access to modify the mime-type input stream construction </summary>
public IDictionary<string, Converter<Stream, ICodedOutputStream>> MimeOutputTypes
{
get
{
return _mimeOutputTypes ??
(_mimeOutputTypes = new Dictionary<string, Converter<Stream, ICodedOutputStream>>(
MimeOutputDefaults, StringComparer.OrdinalIgnoreCase));
}
}
internal IDictionary<string, Converter<Stream, ICodedInputStream>> MimeInputTypesReadOnly
{ get { return _mimeInputTypes ?? MimeInputDefaults; } }
internal IDictionary<string, Converter<Stream, ICodedOutputStream>> MimeOutputTypesReadOnly
{ get { return _mimeOutputTypes ?? MimeOutputDefaults; } }
/// <summary>
/// The default content type to use if the input type is null or empty. If this
......
......@@ -98,6 +98,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Extensions.cs" />
<Compile Include="Http\FormUrlEncodedReader.cs" />
<Compile Include="Http\MessageFormatFactory.cs" />
<Compile Include="Http\MessageFormatOptions.cs" />
<Compile Include="Http\ServiceExtensions.cs" />
......
......@@ -92,6 +92,7 @@
</Compile>
<Compile Include="Compatibility\TextCompatibilityTests.cs" />
<Compile Include="Compatibility\XmlCompatibilityTests.cs" />
<Compile Include="TestReaderForUrlEncoded.cs" />
<Compile Include="CSharpOptionsTest.cs" />
<Compile Include="DescriptorsTest.cs" />
<Compile Include="Descriptors\MessageDescriptorTest.cs" />
......
......@@ -221,5 +221,44 @@ namespace Google.ProtocolBuffers
Assert.AreEqual("<root>\r\n <text>a</text>\r\n <number>1</number>\r\n</root>", Encoding.UTF8.GetString(ms.ToArray()));
}
[Test]
public void TestReadCustomMimeTypes()
{
var options = new MessageFormatOptions();
//Remove existing mime-type mappings
options.MimeInputTypes.Clear();
//Add our own
options.MimeInputTypes.Add("-custom-XML-mime-type-", XmlFormatReader.CreateInstance);
Assert.AreEqual(1, options.MimeInputTypes.Count);
Stream xmlStream = new MemoryStream(Encoding.ASCII.GetBytes(
TestXmlMessage.CreateBuilder().SetText("a").SetNumber(1).Build().ToXml()
));
TestXmlMessage msg = new TestXmlMessage.Builder().MergeFrom(
options, "-custom-XML-mime-type-", xmlStream)
.Build();
Assert.AreEqual("a", msg.Text);
Assert.AreEqual(1, msg.Number);
}
[Test]
public void TestWriteToCustomType()
{
var options = new MessageFormatOptions();
//Remove existing mime-type mappings
options.MimeOutputTypes.Clear();
//Add our own
options.MimeOutputTypes.Add("-custom-XML-mime-type-", XmlFormatWriter.CreateInstance);
Assert.AreEqual(1, options.MimeOutputTypes.Count);
MemoryStream ms = new MemoryStream();
TestXmlMessage.CreateBuilder().SetText("a").SetNumber(1).Build()
.WriteTo(options, "-custom-XML-mime-type-", ms);
Assert.AreEqual("<root><text>a</text><number>1</number></root>", Encoding.UTF8.GetString(ms.ToArray()));
}
}
}
\ No newline at end of file
using System;
using System.IO;
using System.Text;
using NUnit.Framework;
using Google.ProtocolBuffers.TestProtos;
using Google.ProtocolBuffers.Serialization.Http;
namespace Google.ProtocolBuffers
{
[TestFixture]
public class TestReaderForUrlEncoded
{
[Test]
public void Example_FromQueryString()
{
Uri sampleUri = new Uri("http://sample.com/Path/File.ext?text=two+three%20four&valid=true&numbers=1&numbers=2", UriKind.Absolute);
ICodedInputStream input = FormUrlEncodedReader.CreateInstance(sampleUri.Query);
TestXmlMessage.Builder builder = TestXmlMessage.CreateBuilder();
builder.MergeFrom(input);
TestXmlMessage message = builder.Build();
Assert.AreEqual(true, message.Valid);
Assert.AreEqual("two three four", message.Text);
Assert.AreEqual(2, message.NumbersCount);
Assert.AreEqual(1, message.NumbersList[0]);
Assert.AreEqual(2, message.NumbersList[1]);
}
[Test]
public void Example_FromFormData()
{
Stream rawPost = new MemoryStream(Encoding.UTF8.GetBytes("text=two+three%20four&valid=true&numbers=1&numbers=2"), false);
ICodedInputStream input = FormUrlEncodedReader.CreateInstance(rawPost);
TestXmlMessage.Builder builder = TestXmlMessage.CreateBuilder();
builder.MergeFrom(input);
TestXmlMessage message = builder.Build();
Assert.AreEqual(true, message.Valid);
Assert.AreEqual("two three four", message.Text);
Assert.AreEqual(2, message.NumbersCount);
Assert.AreEqual(1, message.NumbersList[0]);
Assert.AreEqual(2, message.NumbersList[1]);
}
[Test]
public void TestEmptyValues()
{
ICodedInputStream input = FormUrlEncodedReader.CreateInstance("valid=true&text=&numbers=1");
TestXmlMessage.Builder builder = TestXmlMessage.CreateBuilder();
builder.MergeFrom(input);
Assert.IsTrue(builder.Valid);
Assert.IsTrue(builder.HasText);
Assert.AreEqual("", builder.Text);
Assert.AreEqual(1, builder.NumbersCount);
Assert.AreEqual(1, builder.NumbersList[0]);
}
[Test]
public void TestNoValue()
{
ICodedInputStream input = FormUrlEncodedReader.CreateInstance("valid=true&text&numbers=1");
TestXmlMessage.Builder builder = TestXmlMessage.CreateBuilder();
builder.MergeFrom(input);
Assert.IsTrue(builder.Valid);
Assert.IsTrue(builder.HasText);
Assert.AreEqual("", builder.Text);
Assert.AreEqual(1, builder.NumbersCount);
Assert.AreEqual(1, builder.NumbersList[0]);
}
[Test, ExpectedException(typeof(NotSupportedException))]
public void FormUrlEncodedReaderDoesNotSupportChildren()
{
ICodedInputStream input = FormUrlEncodedReader.CreateInstance("child=uh0");
TestXmlMessage.CreateBuilder().MergeFrom(input);
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment