Browse Source

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 năm trước cách đây
mục cha
commit
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);
+        }
+    }
+}