浏览代码

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.

csharptest 14 年之前
父节点
当前提交
2cf6e1b077

+ 162 - 0
src/ProtocolBuffers.Serialization/Http/FormUrlEncodedReader.cs

@@ -0,0 +1,162 @@
+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(); }
+    }
+}

+ 39 - 71
src/ProtocolBuffers.Serialization/Http/MessageFormatFactory.cs

@@ -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);
         }
+
     }
 }

+ 85 - 0
src/ProtocolBuffers.Serialization/Http/MessageFormatOptions.cs

@@ -1,4 +1,7 @@
 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

+ 1 - 0
src/ProtocolBuffers.Serialization/ProtocolBuffers.Serialization.csproj

@@ -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" />

+ 1 - 0
src/ProtocolBuffers.Test/ProtocolBuffers.Test.csproj

@@ -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" />

+ 39 - 0
src/ProtocolBuffers.Test/TestMimeMessageFormats.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()));
+        }
     }
 }

+ 84 - 0
src/ProtocolBuffers.Test/TestReaderForUrlEncoded.cs

@@ -0,0 +1,84 @@
+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);
+        }
+    }
+}