From ce275c9a25b2402be1d157f61b60fa329a29e47e Mon Sep 17 00:00:00 2001 From: bbaia Date: Wed, 28 Jul 2010 00:09:16 +0000 Subject: [PATCH] REST client API: Initial checkin (SPRNET-1345) --- .../Spring.RestQuickStart.2008.sln | 40 + .../Spring.RestQuickStart.2010.sln | 40 + .../src/Spring.RestQuickStart/Program.cs | 29 + .../Spring.RestQuickStart.2008.csproj | 60 ++ .../Spring.RestQuickStart.2010.csproj | 80 ++ src/Spring/Spring.Http/AssemblyInfo.cs | 5 + .../AbstractHttpMessageConverter.cs | 224 +++++ .../ByteArrayHttpMessageConverter.cs | 75 ++ .../Feed/AbstractFeedHttpMessageConverter.cs | 63 ++ .../Feed/Atom10FeedHttpMessageConverter.cs | 43 + .../Feed/RssFeedHttpMessageConverter.cs | 43 + .../Http/Converters/IHttpMessageConverter.cs | 85 ++ .../Json/JsonHttpMessageConverter.cs | 89 ++ .../Converters/StringHttpMessageConverter.cs | 99 +++ .../Xml/AbstractXmlHttpMessageConverter.cs | 110 +++ .../Xml/DataContractHttpMessageConverter.cs | 93 +++ .../Xml/XElementHttpMessageConverter.cs | 54 ++ .../Xml/XmlDocumentHttpMessageConverter.cs | 52 ++ .../XmlSerializableHttpMessageConverter.cs | 58 ++ .../Http/DefaultHttpWebRequestFactory.cs | 116 +++ src/Spring/Spring.Http/Http/HttpMethod.cs | 44 + .../Spring.Http/Http/HttpRequestMessage.cs | 134 +++ .../Spring.Http/Http/HttpResponseMessage.cs | 107 +++ .../Http/IHttpWebRequestFactory.cs | 46 + src/Spring/Spring.Http/Http/MediaType.cs | 790 ++++++++++++++++++ .../Spring.Http/Http/Rest/IRequestCallback.cs | 47 ++ .../Http/Rest/IResponseExtractor.cs | 48 ++ .../Spring.Http/Http/Rest/IRestOperations.cs | 439 ++++++++++ .../Http/Rest/RestClientException.cs | 78 ++ .../Spring.Http/Http/Rest/RestTemplate.cs | 553 ++++++++++++ .../Support/AcceptHeaderRequestCallback.cs | 96 +++ .../Rest/Support/HeadersResponseExtractor.cs | 35 + .../Support/HttpMessageRequestCallback.cs | 132 +++ .../Support/HttpMessageResponseExtractor.cs | 55 ++ .../MessageConverterResponseExtractor.cs | 88 ++ .../Rest/Support/MethodRequestCallback.cs | 60 ++ .../Spring.Http/Spring.Http.2008.csproj | 146 ++++ .../Spring.Http/Spring.Http.2010.csproj | 184 ++++ src/Spring/Spring.Http/Util/UriTemplate.cs | 239 ++++++ test/Spring/Spring.Http.Tests/AssemblyInfo.cs | 25 + .../ByteArrayHttpMessageConverterTests.cs | 99 +++ .../StringHttpMessageConverterTests.cs | 126 +++ ...actHttpMessageConverterIntegrationTests.cs | 328 ++++++++ .../DataContractHttpMessageConverterTests.cs | 119 +++ .../XmlDocumentHttpMessageConverterTests.cs | 105 +++ .../Spring.Http.Tests/Http/MediaTypeTests.cs | 544 ++++++++++++ .../Http/Rest/RestTemplateIntegrationTests.cs | 486 +++++++++++ .../Http/Rest/RestTemplateTests.cs | 643 ++++++++++++++ .../Spring.Http.Tests.2008.csproj | 125 +++ .../Spring.Http.Tests.2010.csproj | 151 ++++ .../Spring.Http.Tests.dll.config | 18 + .../Util/UriTemplateTests.cs | 227 +++++ 52 files changed, 7775 insertions(+) create mode 100644 examples/Spring/Spring.RestQuickStart/Spring.RestQuickStart.2008.sln create mode 100644 examples/Spring/Spring.RestQuickStart/Spring.RestQuickStart.2010.sln create mode 100644 examples/Spring/Spring.RestQuickStart/src/Spring.RestQuickStart/Program.cs create mode 100644 examples/Spring/Spring.RestQuickStart/src/Spring.RestQuickStart/Spring.RestQuickStart.2008.csproj create mode 100644 examples/Spring/Spring.RestQuickStart/src/Spring.RestQuickStart/Spring.RestQuickStart.2010.csproj create mode 100644 src/Spring/Spring.Http/AssemblyInfo.cs create mode 100644 src/Spring/Spring.Http/Http/Converters/AbstractHttpMessageConverter.cs create mode 100644 src/Spring/Spring.Http/Http/Converters/ByteArrayHttpMessageConverter.cs create mode 100644 src/Spring/Spring.Http/Http/Converters/Feed/AbstractFeedHttpMessageConverter.cs create mode 100644 src/Spring/Spring.Http/Http/Converters/Feed/Atom10FeedHttpMessageConverter.cs create mode 100644 src/Spring/Spring.Http/Http/Converters/Feed/RssFeedHttpMessageConverter.cs create mode 100644 src/Spring/Spring.Http/Http/Converters/IHttpMessageConverter.cs create mode 100644 src/Spring/Spring.Http/Http/Converters/Json/JsonHttpMessageConverter.cs create mode 100644 src/Spring/Spring.Http/Http/Converters/StringHttpMessageConverter.cs create mode 100644 src/Spring/Spring.Http/Http/Converters/Xml/AbstractXmlHttpMessageConverter.cs create mode 100644 src/Spring/Spring.Http/Http/Converters/Xml/DataContractHttpMessageConverter.cs create mode 100644 src/Spring/Spring.Http/Http/Converters/Xml/XElementHttpMessageConverter.cs create mode 100644 src/Spring/Spring.Http/Http/Converters/Xml/XmlDocumentHttpMessageConverter.cs create mode 100644 src/Spring/Spring.Http/Http/Converters/Xml/XmlSerializableHttpMessageConverter.cs create mode 100644 src/Spring/Spring.Http/Http/DefaultHttpWebRequestFactory.cs create mode 100644 src/Spring/Spring.Http/Http/HttpMethod.cs create mode 100644 src/Spring/Spring.Http/Http/HttpRequestMessage.cs create mode 100644 src/Spring/Spring.Http/Http/HttpResponseMessage.cs create mode 100644 src/Spring/Spring.Http/Http/IHttpWebRequestFactory.cs create mode 100644 src/Spring/Spring.Http/Http/MediaType.cs create mode 100644 src/Spring/Spring.Http/Http/Rest/IRequestCallback.cs create mode 100644 src/Spring/Spring.Http/Http/Rest/IResponseExtractor.cs create mode 100644 src/Spring/Spring.Http/Http/Rest/IRestOperations.cs create mode 100644 src/Spring/Spring.Http/Http/Rest/RestClientException.cs create mode 100644 src/Spring/Spring.Http/Http/Rest/RestTemplate.cs create mode 100644 src/Spring/Spring.Http/Http/Rest/Support/AcceptHeaderRequestCallback.cs create mode 100644 src/Spring/Spring.Http/Http/Rest/Support/HeadersResponseExtractor.cs create mode 100644 src/Spring/Spring.Http/Http/Rest/Support/HttpMessageRequestCallback.cs create mode 100644 src/Spring/Spring.Http/Http/Rest/Support/HttpMessageResponseExtractor.cs create mode 100644 src/Spring/Spring.Http/Http/Rest/Support/MessageConverterResponseExtractor.cs create mode 100644 src/Spring/Spring.Http/Http/Rest/Support/MethodRequestCallback.cs create mode 100644 src/Spring/Spring.Http/Spring.Http.2008.csproj create mode 100644 src/Spring/Spring.Http/Spring.Http.2010.csproj create mode 100644 src/Spring/Spring.Http/Util/UriTemplate.cs create mode 100644 test/Spring/Spring.Http.Tests/AssemblyInfo.cs create mode 100644 test/Spring/Spring.Http.Tests/Http/Converters/ByteArrayHttpMessageConverterTests.cs create mode 100644 test/Spring/Spring.Http.Tests/Http/Converters/StringHttpMessageConverterTests.cs create mode 100644 test/Spring/Spring.Http.Tests/Http/Converters/Xml/DataContractHttpMessageConverterIntegrationTests.cs create mode 100644 test/Spring/Spring.Http.Tests/Http/Converters/Xml/DataContractHttpMessageConverterTests.cs create mode 100644 test/Spring/Spring.Http.Tests/Http/Converters/Xml/XmlDocumentHttpMessageConverterTests.cs create mode 100644 test/Spring/Spring.Http.Tests/Http/MediaTypeTests.cs create mode 100644 test/Spring/Spring.Http.Tests/Http/Rest/RestTemplateIntegrationTests.cs create mode 100644 test/Spring/Spring.Http.Tests/Http/Rest/RestTemplateTests.cs create mode 100644 test/Spring/Spring.Http.Tests/Spring.Http.Tests.2008.csproj create mode 100644 test/Spring/Spring.Http.Tests/Spring.Http.Tests.2010.csproj create mode 100644 test/Spring/Spring.Http.Tests/Spring.Http.Tests.dll.config create mode 100644 test/Spring/Spring.Http.Tests/Util/UriTemplateTests.cs diff --git a/examples/Spring/Spring.RestQuickStart/Spring.RestQuickStart.2008.sln b/examples/Spring/Spring.RestQuickStart/Spring.RestQuickStart.2008.sln new file mode 100644 index 00000000..7ff336dd --- /dev/null +++ b/examples/Spring/Spring.RestQuickStart/Spring.RestQuickStart.2008.sln @@ -0,0 +1,40 @@ +Microsoft Visual Studio Solution File, Format Version 10.00 +# Visual Studio 2008 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spring.Core.2008", "..\..\..\src\Spring\Spring.Core\Spring.Core.2008.csproj", "{710961A3-0DF4-49E4-A26E-F5B9C044AC84}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spring.Http.2008", "..\..\..\src\Spring\Spring.Http\Spring.Http.2008.csproj", "{FAC04F79-4B1F-1D13-B30F-00EE04EC0FBC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spring.Http.Tests.2008", "..\..\..\test\Spring\Spring.Http.Tests\Spring.Http.Tests.2008.csproj", "{F04CEE18-3897-A399-46BF-459437475B21}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spring.RestQuickStart.2008", "src\Spring.RestQuickStart\Spring.RestQuickStart.2008.csproj", "{5B47D309-31C9-4282-86E2-11FC63DFF55B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {710961A3-0DF4-49E4-A26E-F5B9C044AC84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {710961A3-0DF4-49E4-A26E-F5B9C044AC84}.Debug|Any CPU.Build.0 = Debug|Any CPU + {710961A3-0DF4-49E4-A26E-F5B9C044AC84}.Release|Any CPU.ActiveCfg = Release|Any CPU + {710961A3-0DF4-49E4-A26E-F5B9C044AC84}.Release|Any CPU.Build.0 = Release|Any CPU + {FAC04F79-4B1F-1D13-B30F-00EE04EC0FBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FAC04F79-4B1F-1D13-B30F-00EE04EC0FBC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FAC04F79-4B1F-1D13-B30F-00EE04EC0FBC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FAC04F79-4B1F-1D13-B30F-00EE04EC0FBC}.Release|Any CPU.Build.0 = Release|Any CPU + {F04CEE18-3897-A399-46BF-459437475B21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F04CEE18-3897-A399-46BF-459437475B21}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F04CEE18-3897-A399-46BF-459437475B21}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F04CEE18-3897-A399-46BF-459437475B21}.Release|Any CPU.Build.0 = Release|Any CPU + {5B47D309-31C9-4282-86E2-11FC63DFF55B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B47D309-31C9-4282-86E2-11FC63DFF55B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B47D309-31C9-4282-86E2-11FC63DFF55B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B47D309-31C9-4282-86E2-11FC63DFF55B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + NAntAddinLastFileName = Spring.build + EndGlobalSection +EndGlobal diff --git a/examples/Spring/Spring.RestQuickStart/Spring.RestQuickStart.2010.sln b/examples/Spring/Spring.RestQuickStart/Spring.RestQuickStart.2010.sln new file mode 100644 index 00000000..0bef0f04 --- /dev/null +++ b/examples/Spring/Spring.RestQuickStart/Spring.RestQuickStart.2010.sln @@ -0,0 +1,40 @@ +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spring.Core.2010", "..\..\..\src\Spring\Spring.Core\Spring.Core.2010.csproj", "{710961A3-0DF4-49E4-A26E-F5B9C044AC84}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spring.Http.2010", "..\..\..\src\Spring\Spring.Http\Spring.Http.2010.csproj", "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spring.Http.Tests.2010", "..\..\..\test\Spring\Spring.Http.Tests\Spring.Http.Tests.2010.csproj", "{4594CEE7-3897-A3BF-9946-5B4374F01821}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spring.RestQuickStart.2010", "src\Spring.RestQuickStart\Spring.RestQuickStart.2010.csproj", "{5B47D309-31C9-4282-86E2-11FC63DFF55B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {710961A3-0DF4-49E4-A26E-F5B9C044AC84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {710961A3-0DF4-49E4-A26E-F5B9C044AC84}.Debug|Any CPU.Build.0 = Debug|Any CPU + {710961A3-0DF4-49E4-A26E-F5B9C044AC84}.Release|Any CPU.ActiveCfg = Release|Any CPU + {710961A3-0DF4-49E4-A26E-F5B9C044AC84}.Release|Any CPU.Build.0 = Release|Any CPU + {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}.Release|Any CPU.Build.0 = Release|Any CPU + {4594CEE7-3897-A3BF-9946-5B4374F01821}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4594CEE7-3897-A3BF-9946-5B4374F01821}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4594CEE7-3897-A3BF-9946-5B4374F01821}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4594CEE7-3897-A3BF-9946-5B4374F01821}.Release|Any CPU.Build.0 = Release|Any CPU + {5B47D309-31C9-4282-86E2-11FC63DFF55B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B47D309-31C9-4282-86E2-11FC63DFF55B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B47D309-31C9-4282-86E2-11FC63DFF55B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B47D309-31C9-4282-86E2-11FC63DFF55B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + NAntAddinLastFileName = Spring.build + EndGlobalSection +EndGlobal diff --git a/examples/Spring/Spring.RestQuickStart/src/Spring.RestQuickStart/Program.cs b/examples/Spring/Spring.RestQuickStart/src/Spring.RestQuickStart/Program.cs new file mode 100644 index 00000000..6acf2663 --- /dev/null +++ b/examples/Spring/Spring.RestQuickStart/src/Spring.RestQuickStart/Program.cs @@ -0,0 +1,29 @@ +using System; + +using Spring.Http.Rest; + +namespace Spring.RestQuickStart +{ + class Program + { + static void Main(string[] args) + { + try + { + RestTemplate rt = new RestTemplate("http://twitter.com"); + string result = rt.GetForObject("/statuses/user_timeline.xml?id={id}", "lancearmstrong"); + + Console.WriteLine(result); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + finally + { + Console.WriteLine("--- hit to quit ---"); + Console.ReadLine(); + } + } + } +} diff --git a/examples/Spring/Spring.RestQuickStart/src/Spring.RestQuickStart/Spring.RestQuickStart.2008.csproj b/examples/Spring/Spring.RestQuickStart/src/Spring.RestQuickStart/Spring.RestQuickStart.2008.csproj new file mode 100644 index 00000000..d1f5b7e6 --- /dev/null +++ b/examples/Spring/Spring.RestQuickStart/src/Spring.RestQuickStart/Spring.RestQuickStart.2008.csproj @@ -0,0 +1,60 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {5B47D309-31C9-4282-86E2-11FC63DFF55B} + Exe + Properties + Spring.RestQuickStart + Spring.RestQuickStart + v3.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + 3.5 + + + + + + + + {710961A3-0DF4-49E4-A26E-F5B9C044AC84} + Spring.Core.2008 + + + {FAC04F79-4B1F-1D13-B30F-00EE04EC0FBC} + Spring.Http.2008 + + + + + \ No newline at end of file diff --git a/examples/Spring/Spring.RestQuickStart/src/Spring.RestQuickStart/Spring.RestQuickStart.2010.csproj b/examples/Spring/Spring.RestQuickStart/src/Spring.RestQuickStart/Spring.RestQuickStart.2010.csproj new file mode 100644 index 00000000..62e73c12 --- /dev/null +++ b/examples/Spring/Spring.RestQuickStart/src/Spring.RestQuickStart/Spring.RestQuickStart.2010.csproj @@ -0,0 +1,80 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {5B47D309-31C9-4282-86E2-11FC63DFF55B} + Exe + Properties + Spring.RestQuickStart + Spring.RestQuickStart + v4.0 + 512 + + + 3.5 + + + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + 3.5 + + + + + + + + {710961A3-0DF4-49E4-A26E-F5B9C044AC84} + Spring.Core.2010 + + + {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Spring.Http.2010 + + + + + \ No newline at end of file diff --git a/src/Spring/Spring.Http/AssemblyInfo.cs b/src/Spring/Spring.Http/AssemblyInfo.cs new file mode 100644 index 00000000..43bd989e --- /dev/null +++ b/src/Spring/Spring.Http/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System; +using System.Reflection; + +[assembly: AssemblyTitle("Spring.Http")] +[assembly: AssemblyDescription("Interfaces and classes that provide REST client API in Spring.Net")] \ No newline at end of file diff --git a/src/Spring/Spring.Http/Http/Converters/AbstractHttpMessageConverter.cs b/src/Spring/Spring.Http/Http/Converters/AbstractHttpMessageConverter.cs new file mode 100644 index 00000000..98ee761e --- /dev/null +++ b/src/Spring/Spring.Http/Http/Converters/AbstractHttpMessageConverter.cs @@ -0,0 +1,224 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Net; +using System.Collections.Generic; + +using Spring.Util; + +namespace Spring.Http.Converters +{ + /** + * Abstract base class for most {@link HttpMessageConverter} implementations. + * + *

This base class adds support for setting supported {@code MediaTypes}, through the + * {@link #setSupportedMediaTypes(List) supportedMediaTypes} bean property. It also adds + * support for {@code Content-Type} and {@code Content-Length} when writing to output messages. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 3.0 + */ + public abstract class AbstractHttpMessageConverter : IHttpMessageConverter + { + #region Logging + + private static readonly Common.Logging.ILog LOG = Common.Logging.LogManager.GetLogger(typeof(AbstractHttpMessageConverter)); + + #endregion + + private IList _supportedMediaTypes = new List(); + + /** + * Set the list of {@link MediaType} objects supported by this converter. + */ + public IList SupportedMediaTypes + { + get { return _supportedMediaTypes; } + set { _supportedMediaTypes = value; } + } + + #region Constructor(s) + + /** + * Construct an {@code AbstractHttpMessageConverter} with no supported media types. + * @see #setSupportedMediaTypes + */ + protected AbstractHttpMessageConverter() + { + } + + /** + * Construct an {@code AbstractHttpMessageConverter} with multiple supported media type. + * @param supportedMediaTypes the supported media types + */ + protected AbstractHttpMessageConverter(params MediaType[] supportedMediaTypes) + { + this._supportedMediaTypes = new List(supportedMediaTypes); + } + + #endregion + + #region IHttpMessageConverter Membres + + /** + * {@inheritDoc} + *

This implementation checks if the given class is {@linkplain #supports(Class) supported}, + * and if the {@linkplain #getSupportedMediaTypes() supported media types} + * {@linkplain MediaType#includes(MediaType) include} the given media type. + */ + public bool CanRead(Type type, MediaType mediaType) + { + return Supports(type) && CanRead(mediaType); + } + + /** + * {@inheritDoc} + *

This implementation checks if the given class is {@linkplain #supports(Class) supported}, + * and if the {@linkplain #getSupportedMediaTypes() supported media types} + * {@linkplain MediaType#includes(MediaType) include} the given media type. + */ + public bool CanWrite(Type type, MediaType mediaType) + { + return Supports(type) && CanWrite(mediaType); + } + + /** + * {@inheritDoc} + *

This implementation simple delegates to {@link #readInternal(Class, HttpInputMessage)}. + * Future implementations might add some default behavior, however. + */ + public T Read(HttpWebResponse response) where T : class + { + return ReadInternal(response); + } + + /** + * {@inheritDoc} + *

This implementation delegates to {@link #getDefaultContentType(Object)} if a content + * type was not provided, calls {@link #getContentLength}, and sets the corresponding headers + * on the output message. It then calls {@link #writeInternal}. + */ + public void Write(object content, MediaType mediaType, HttpWebRequest request) + { + if (!StringUtils.HasText(request.Headers[HttpRequestHeader.ContentType])) + { + if (mediaType == null || mediaType.IsWildcardType || mediaType.IsWildcardSubtype) + { + mediaType = GetDefaultContentType(content.GetType()); + } + if (mediaType != null) + { + request.ContentType = mediaType.ToString(); + } + } + WriteInternal(content, request); + } + + #endregion + + /** + * Returns true if any of the {@linkplain #setSupportedMediaTypes(List) supported media types} + * include the given media type. + * @param mediaType the media type to read, can be {@code null} if not specified. Typically the value of a + * {@code Content-Type} header. + * @return true if the supported media types include the media type, or if the media type is {@code null} + */ + protected bool CanRead(MediaType mediaType) + { + if (mediaType == null) + { + return true; + } + foreach(MediaType supportedMediaType in this._supportedMediaTypes) + { + if (supportedMediaType.Includes(mediaType)) + { + return true; + } + } + return false; + } + + /** + * Returns true if the given media type includes any of the + * {@linkplain #setSupportedMediaTypes(List) supported media types}. + * @param mediaType the media type to write, can be {@code null} if not specified. Typically the value of an + * {@code Accept} header. + * @return true if the supported media types are compatible with the media type, or if the media type is {@code null} + */ + protected bool CanWrite(MediaType mediaType) + { + if (mediaType == null || mediaType.Equals(MediaType.ALL)) + { + return true; + } + foreach(MediaType supportedMediaType in this._supportedMediaTypes) + { + if (supportedMediaType.IsCompatibleWith(mediaType)) + { + return true; + } + } + return false; + } + + /** + * Returns the default content type for the given type. Called when {@link #write} + * is invoked without a specified content type parameter. + *

By default, this returns the first element of the + * {@link #setSupportedMediaTypes(List) supportedMediaTypes} property, if any. + * Can be overridden in subclasses. + * @param t the type to return the content type for + * @return the content type, or null if not known + */ + protected virtual MediaType GetDefaultContentType(Type type) + { + return (this._supportedMediaTypes.Count > 0 ? this._supportedMediaTypes[0] : null); + } + + /** + * Indicates whether the given class is supported by this converter. + * @param clazz the class to test for support + * @return true if supported; false otherwise + */ + protected abstract bool Supports(Type type); + + /** + * Abstract template method that reads the actualy object. Invoked from {@link #read}. + * @param clazz the type of object to return + * @param inputMessage the HTTP input message to read from + * @return the converted object + * @throws IOException in case of I/O errors + * @throws HttpMessageNotReadableException in case of conversion errors + */ + protected abstract T ReadInternal(HttpWebResponse response) where T : class; + + /** + * Abstract template method that writes the actual body. Invoked from {@link #write}. + * @param t the object to write to the output message + * @param outputMessage the message to write to + * @throws IOException in case of I/O errors + * @throws HttpMessageNotWritableException in case of conversion errors + */ + protected abstract void WriteInternal(object content, HttpWebRequest request); + } +} diff --git a/src/Spring/Spring.Http/Http/Converters/ByteArrayHttpMessageConverter.cs b/src/Spring/Spring.Http/Http/Converters/ByteArrayHttpMessageConverter.cs new file mode 100644 index 00000000..6b32d410 --- /dev/null +++ b/src/Spring/Spring.Http/Http/Converters/ByteArrayHttpMessageConverter.cs @@ -0,0 +1,75 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.IO; +using System.Net; +using System.Text; + +namespace Spring.Http.Converters +{ + /** + * Implementation of {@link HttpMessageConverter} that can read and write byte arrays. + * + *

By default, this converter supports all media types (*/*), and writes with a {@code + * Content-Type} of {@code application/octet-stream}. This can be overridden by setting the {@link + * #setSupportedMediaTypes(java.util.List) supportedMediaTypes} property. + * + * @author Arjen Poutsma + * @since 3.0 + */ + public class ByteArrayHttpMessageConverter : AbstractHttpMessageConverter + { + /** Creates a new instance of the {@code ByteArrayHttpMessageConverter}. */ + public ByteArrayHttpMessageConverter() : + base(new MediaType("application", "octet-stream"), MediaType.ALL) + { + } + + protected override bool Supports(Type type) + { + return type.Equals(typeof(byte[])); + } + + protected override T ReadInternal(HttpWebResponse response) + { + // Get the response stream + using (BinaryReader reader = new BinaryReader(response.GetResponseStream())) + { + return reader.ReadBytes((int)response.ContentLength) as T; + } + } + + protected override void WriteInternal(object content, HttpWebRequest request) + { + // Create a byte array of the data we want to send + byte[] byteData = content as byte[]; + + // Set the content length in the request headers + request.ContentLength = byteData.Length; + + // Write data + using (Stream postStream = request.GetRequestStream()) + { + postStream.Write(byteData, 0, byteData.Length); + } + } + } +} diff --git a/src/Spring/Spring.Http/Http/Converters/Feed/AbstractFeedHttpMessageConverter.cs b/src/Spring/Spring.Http/Http/Converters/Feed/AbstractFeedHttpMessageConverter.cs new file mode 100644 index 00000000..fdf4770a --- /dev/null +++ b/src/Spring/Spring.Http/Http/Converters/Feed/AbstractFeedHttpMessageConverter.cs @@ -0,0 +1,63 @@ +#if NET_3_5 +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Net; +using System.Xml; +using System.ServiceModel.Syndication; + +using Spring.Http.Converters.Xml; + +namespace Spring.Http.Converters.Feed +{ + public abstract class AbstractFeedHttpMessageConverter : AbstractXmlHttpMessageConverter + { + /** + * Construct an {@code AbstractHttpMessageConverter} with multiple supported media type. + * @param supportedMediaTypes the supported media types + */ + protected AbstractFeedHttpMessageConverter(params MediaType[] supportedMediaTypes) : + base(supportedMediaTypes) + { + } + + protected override bool Supports(Type type) + { + return type.Equals(typeof(SyndicationFeed)); + } + + protected override T ReadXml(XmlReader xmlReader, HttpWebResponse response) + { + return SyndicationFeed.Load(xmlReader) as T; + } + + protected override XmlReaderSettings GetDefaultXmlReaderSettings() + { + XmlReaderSettings settings = new XmlReaderSettings(); + settings.CloseInput = true; + settings.IgnoreProcessingInstructions = true; + settings.ProhibitDtd = false; + settings.XmlResolver = null; + return settings; + } + } +} +#endif \ No newline at end of file diff --git a/src/Spring/Spring.Http/Http/Converters/Feed/Atom10FeedHttpMessageConverter.cs b/src/Spring/Spring.Http/Http/Converters/Feed/Atom10FeedHttpMessageConverter.cs new file mode 100644 index 00000000..da075e77 --- /dev/null +++ b/src/Spring/Spring.Http/Http/Converters/Feed/Atom10FeedHttpMessageConverter.cs @@ -0,0 +1,43 @@ +#if NET_3_5 +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Net; +using System.Xml; +using System.ServiceModel.Syndication; + +namespace Spring.Http.Converters.Feed +{ + public class Atom10FeedHttpMessageConverter : AbstractFeedHttpMessageConverter + { + public Atom10FeedHttpMessageConverter() : + base(new MediaType("application", "atom+xml")) + { + } + + protected override void WriteXml(XmlWriter xmlWriter, object content, HttpWebRequest request) + { + SyndicationFeed rssFeed = content as SyndicationFeed; + rssFeed.SaveAsAtom10(xmlWriter); + } + } +} +#endif \ No newline at end of file diff --git a/src/Spring/Spring.Http/Http/Converters/Feed/RssFeedHttpMessageConverter.cs b/src/Spring/Spring.Http/Http/Converters/Feed/RssFeedHttpMessageConverter.cs new file mode 100644 index 00000000..9f691a7a --- /dev/null +++ b/src/Spring/Spring.Http/Http/Converters/Feed/RssFeedHttpMessageConverter.cs @@ -0,0 +1,43 @@ +#if NET_3_5 +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Net; +using System.Xml; +using System.ServiceModel.Syndication; + +namespace Spring.Http.Converters.Feed +{ + public class Rss20FeedHttpMessageConverter : AbstractFeedHttpMessageConverter + { + public Rss20FeedHttpMessageConverter() : + base(new MediaType("application", "rss+xml")) + { + } + + protected override void WriteXml(XmlWriter xmlWriter, object content, HttpWebRequest request) + { + SyndicationFeed rssFeed = content as SyndicationFeed; + rssFeed.SaveAsRss20(xmlWriter); + } + } +} +#endif \ No newline at end of file diff --git a/src/Spring/Spring.Http/Http/Converters/IHttpMessageConverter.cs b/src/Spring/Spring.Http/Http/Converters/IHttpMessageConverter.cs new file mode 100644 index 00000000..5b48699a --- /dev/null +++ b/src/Spring/Spring.Http/Http/Converters/IHttpMessageConverter.cs @@ -0,0 +1,85 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Net; +using System.Collections.Generic; + +namespace Spring.Http.Converters +{ + /** + * Strategy interface that specifies a converter that can convert from and to HTTP requests and responses. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 3.0 + */ + public interface IHttpMessageConverter + { + /** + * Indicates whether the given class can be read by this converter. + * @param clazz the class to test for readability + * @param mediaType the media type to read, can be {@code null} if not specified. Typically the value of a + * {@code Content-Type} header. + * @return {@code true} if readable; {@code false} otherwise + */ + bool CanRead(Type type, MediaType mediaType); + + /** + * Indicates whether the given class can be written by this converter. + * @param clazz the class to test for writability + * @param mediaType the media type to write, can be {@code null} if not specified. Typically the value of an + * {@code Accept} header. + * @return {@code true} if writable; {@code false} otherwise + */ + bool CanWrite(Type type, MediaType mediaType); + + /** + * Return the list of {@link MediaType} objects supported by this converter. + * @return the list of supported media types + */ + IList SupportedMediaTypes { get; } + + /** + * Read an object of the given type form the given input message, and returns it. + * @param clazz the type of object to return. This type must have previously been passed to the + * {@link #canRead canRead} method of this interface, which must have returned {@code true}. + * @param inputMessage the HTTP input message to read from + * @return the converted object + * @throws IOException in case of I/O errors + * @throws HttpMessageNotReadableException in case of conversion errors + */ + T Read(HttpWebResponse response) where T : class; + + /** + * Write an given object to the given output message. + * @param t the object to write to the output message. The type of this object must have previously been + * passed to the {@link #canWrite canWrite} method of this interface, which must have returned {@code true}. + * @param contentType the content type to use when writing. May be {@code null} to indicate that the + * default content type of the converter must be used. If not {@code null}, this media type must have + * previously been passed to the {@link #canWrite canWrite} method of this interface, which must have + * returned {@code true}. + * @param outputMessage the message to write to + * @throws IOException in case of I/O errors + * @throws HttpMessageNotWritableException in case of conversion errors + */ + void Write(object content, MediaType mediaType, HttpWebRequest request); + } +} diff --git a/src/Spring/Spring.Http/Http/Converters/Json/JsonHttpMessageConverter.cs b/src/Spring/Spring.Http/Http/Converters/Json/JsonHttpMessageConverter.cs new file mode 100644 index 00000000..b0050c57 --- /dev/null +++ b/src/Spring/Spring.Http/Http/Converters/Json/JsonHttpMessageConverter.cs @@ -0,0 +1,89 @@ +#if NET_3_5 +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Xml; +using System.IO; +using System.Net; +using System.Text; +using System.Runtime.Serialization.Json; + +using Spring.Util; + +namespace Spring.Http.Converters.Json +{ + // TODO : Support for known types, etc... + public class JsonHttpMessageConverter : AbstractHttpMessageConverter + { + public static readonly Encoding DEFAULT_CHARSET = Encoding.UTF8; + + public JsonHttpMessageConverter() : + base(new MediaType("application", "json")) + { + } + + protected override bool Supports(Type type) + { + return true; + } + + protected override T ReadInternal(HttpWebResponse response) + { + DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(T)); + using (Stream stream = response.GetResponseStream()) + { + return (T)serializer.ReadObject(stream) as T; + } + } + + protected override void WriteInternal(object content, HttpWebRequest request) + { + // Get the request encoding + MediaType mediaType = MediaType.ParseMediaType(request.Headers[HttpRequestHeader.ContentType]); + Encoding encoding; + if (mediaType == null || String.IsNullOrEmpty(mediaType.CharSet)) + { + encoding = DEFAULT_CHARSET; + } + else + { + encoding = Encoding.GetEncoding(mediaType.CharSet); + } + + DataContractJsonSerializer serializer = new DataContractJsonSerializer(content.GetType()); + + // Write data + using (Stream postStream = request.GetRequestStream()) + { + using (XmlDictionaryWriter jsonWriter = JsonReaderWriterFactory.CreateJsonWriter(postStream, encoding, false)) + { + serializer.WriteObject(jsonWriter, content); + jsonWriter.Flush(); + } + postStream.Flush(); + + // Set the content length in the request headers + request.ContentLength = postStream.Length; + } + } + } +} +#endif \ No newline at end of file diff --git a/src/Spring/Spring.Http/Http/Converters/StringHttpMessageConverter.cs b/src/Spring/Spring.Http/Http/Converters/StringHttpMessageConverter.cs new file mode 100644 index 00000000..23ba33eb --- /dev/null +++ b/src/Spring/Spring.Http/Http/Converters/StringHttpMessageConverter.cs @@ -0,0 +1,99 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.IO; +using System.Net; +using System.Text; + +namespace Spring.Http.Converters +{ + /** + * Implementation of {@link HttpMessageConverter} that can read and write strings. + * + *

By default, this converter supports all media types (*/*), and writes with a {@code + * Content-Type} of {@code text/plain}. This can be overridden by setting the {@link + * #setSupportedMediaTypes(java.util.List) supportedMediaTypes} property. + * + * @author Arjen Poutsma + * @since 3.0 + */ + public class StringHttpMessageConverter : AbstractHttpMessageConverter + { + public static readonly Encoding DEFAULT_CHARSET = Encoding.GetEncoding("ISO-8859-1"); + + public StringHttpMessageConverter() : + base(new MediaType("text", "plain", "ISO-8859-1"), MediaType.ALL) + { + } + + protected override bool Supports(Type type) + { + return type.Equals(typeof(string)); + } + + protected override T ReadInternal(HttpWebResponse response) + { + // Get the response encoding + Encoding encoding; + if (String.IsNullOrEmpty(response.CharacterSet)) + { + encoding = DEFAULT_CHARSET; + } + else + { + encoding = Encoding.GetEncoding(response.CharacterSet); + } + + // Get the response stream + using (StreamReader reader = new StreamReader(response.GetResponseStream(), encoding)) + { + return reader.ReadToEnd() as T; + } + } + + protected override void WriteInternal(object content, HttpWebRequest request) + { + // Get the request encoding + MediaType mediaType = MediaType.ParseMediaType(request.Headers[HttpRequestHeader.ContentType]); + Encoding encoding; + if (mediaType == null || String.IsNullOrEmpty(mediaType.CharSet)) + { + encoding = DEFAULT_CHARSET; + } + else + { + encoding = Encoding.GetEncoding(mediaType.CharSet); + } + + // Create a byte array of the data we want to send + byte[] byteData = encoding.GetBytes(content as string); + + // Set the content length in the request headers + request.ContentLength = byteData.Length; + + // Write data + using (Stream postStream = request.GetRequestStream()) + { + postStream.Write(byteData, 0, byteData.Length); + } + } + } +} diff --git a/src/Spring/Spring.Http/Http/Converters/Xml/AbstractXmlHttpMessageConverter.cs b/src/Spring/Spring.Http/Http/Converters/Xml/AbstractXmlHttpMessageConverter.cs new file mode 100644 index 00000000..c961d841 --- /dev/null +++ b/src/Spring/Spring.Http/Http/Converters/Xml/AbstractXmlHttpMessageConverter.cs @@ -0,0 +1,110 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.IO; +using System.Xml; +using System.Net; +using System.Text; + +namespace Spring.Http.Converters.Xml +{ + public abstract class AbstractXmlHttpMessageConverter : AbstractHttpMessageConverter + { + public static readonly Encoding DEFAULT_CHARSET = Encoding.UTF8; + + private XmlReaderSettings _xmlReaderSettings; + + public XmlReaderSettings XmlReaderSettings + { + get + { + if (_xmlReaderSettings == null) + { + _xmlReaderSettings = this.GetDefaultXmlReaderSettings(); + } + return _xmlReaderSettings; + } + set { _xmlReaderSettings = value; } + } + + /** + * Construct an {@code AbstractHttpMessageConverter} with multiple supported media type. + * @param supportedMediaTypes the supported media types + */ + protected AbstractXmlHttpMessageConverter(params MediaType[] supportedMediaTypes) : + base(supportedMediaTypes) + { + } + + protected override T ReadInternal(HttpWebResponse response) + { + using (Stream stream = response.GetResponseStream()) + { + using (XmlReader xmlReader = XmlReader.Create(stream, this.XmlReaderSettings)) + { + return ReadXml(xmlReader, response); + } + } + } + + protected override void WriteInternal(object content, HttpWebRequest request) + { + // Get the request encoding + MediaType mediaType = MediaType.ParseMediaType(request.Headers[HttpRequestHeader.ContentType]); + Encoding encoding; + if (mediaType == null || String.IsNullOrEmpty(mediaType.CharSet)) + { + encoding = DEFAULT_CHARSET; + } + else + { + encoding = Encoding.GetEncoding(mediaType.CharSet); + } + + using (Stream postStream = request.GetRequestStream()) + { + using (XmlTextWriter xmlWriter = new XmlTextWriter(postStream, encoding)) + { + WriteXml(xmlWriter, content, request); + xmlWriter.Flush(); + } + + // TODO : Don't work + // Set the content length in the request headers + request.ContentLength = postStream.Length; + } + } + + protected virtual XmlReaderSettings GetDefaultXmlReaderSettings() + { + XmlReaderSettings settings = new XmlReaderSettings(); + settings.ConformanceLevel = ConformanceLevel.Auto; + settings.CloseInput = true; + settings.IgnoreProcessingInstructions = true; + settings.IgnoreWhitespace = true; + return settings; + } + + protected abstract T ReadXml(XmlReader xmlReader, HttpWebResponse response) where T : class; + + protected abstract void WriteXml(XmlWriter xmlWriter, object content, HttpWebRequest request); + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Http/Http/Converters/Xml/DataContractHttpMessageConverter.cs b/src/Spring/Spring.Http/Http/Converters/Xml/DataContractHttpMessageConverter.cs new file mode 100644 index 00000000..5c0fec3d --- /dev/null +++ b/src/Spring/Spring.Http/Http/Converters/Xml/DataContractHttpMessageConverter.cs @@ -0,0 +1,93 @@ +#if NET_3_0 +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.IO; +using System.Net; +using System.Xml; +using System.Text; +using System.Runtime.Serialization; + +using Spring.Util; + +namespace Spring.Http.Converters.Xml +{ + // TODO : Derive from AbstractXmlHttpMessageConverter ? + // TODO : Support for known types, etc... + public class DataContractHttpMessageConverter : AbstractHttpMessageConverter + { + public static readonly Encoding DEFAULT_CHARSET = Encoding.UTF8; + + public DataContractHttpMessageConverter() : + base(new MediaType("application", "xml"), new MediaType("text", "xml"), new MediaType("application", "*+xml")) + { + } + + protected override bool Supports(Type type) + { + return true; + //return ( + // AttributeUtils.FindAttribute(type, typeof(DataContractAttribute)) != null || + // AttributeUtils.FindAttribute(type, typeof(SerializableAttribute)) != null || + // typeof(ISerializable).IsAssignableFrom(type)); + } + + protected override T ReadInternal(HttpWebResponse response) + { + DataContractSerializer serializer = new DataContractSerializer(typeof(T)); + using (Stream stream = response.GetResponseStream()) + { + return (T)serializer.ReadObject(stream) as T; + } + } + + protected override void WriteInternal(object content, HttpWebRequest request) + { + // Get the request encoding + MediaType mediaType = MediaType.ParseMediaType(request.Headers[HttpRequestHeader.ContentType]); + Encoding encoding; + if (mediaType == null || String.IsNullOrEmpty(mediaType.CharSet)) + { + encoding = DEFAULT_CHARSET; + } + else + { + encoding = Encoding.GetEncoding(mediaType.CharSet); + } + + DataContractSerializer serializer = new DataContractSerializer(content.GetType()); + + // Write data + using (Stream postStream = request.GetRequestStream()) + { + using (XmlTextWriter xmlWriter = new XmlTextWriter(postStream, encoding)) + { + serializer.WriteObject(xmlWriter, content); + xmlWriter.Flush(); + } + + // Set the content length in the request headers + request.ContentLength = postStream.Length; + } + } + } +} +#endif \ No newline at end of file diff --git a/src/Spring/Spring.Http/Http/Converters/Xml/XElementHttpMessageConverter.cs b/src/Spring/Spring.Http/Http/Converters/Xml/XElementHttpMessageConverter.cs new file mode 100644 index 00000000..e47278d9 --- /dev/null +++ b/src/Spring/Spring.Http/Http/Converters/Xml/XElementHttpMessageConverter.cs @@ -0,0 +1,54 @@ +#if NET_3_5 +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Net; +using System.Xml; +using System.Xml.Linq; + +namespace Spring.Http.Converters.Xml +{ + // TODO : Support XElement.Load options + public class XElementHttpMessageConverter : AbstractXmlHttpMessageConverter + { + public XElementHttpMessageConverter() : + base(new MediaType("application", "xml"), new MediaType("text", "xml"), new MediaType("application", "*+xml")) + { + } + + protected override bool Supports(Type type) + { + return type.Equals(typeof(XElement)); + } + + protected override T ReadXml(XmlReader xmlReader, HttpWebResponse response) + { + return XElement.Load(xmlReader) as T; + } + + protected override void WriteXml(XmlWriter xmlWriter, object content, HttpWebRequest request) + { + XElement xElement = content as XElement; + xElement.WriteTo(xmlWriter); + } + } +} +#endif \ No newline at end of file diff --git a/src/Spring/Spring.Http/Http/Converters/Xml/XmlDocumentHttpMessageConverter.cs b/src/Spring/Spring.Http/Http/Converters/Xml/XmlDocumentHttpMessageConverter.cs new file mode 100644 index 00000000..cac254be --- /dev/null +++ b/src/Spring/Spring.Http/Http/Converters/Xml/XmlDocumentHttpMessageConverter.cs @@ -0,0 +1,52 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Xml; +using System.Net; + +namespace Spring.Http.Converters.Xml +{ + public class XmlDocumentHttpMessageConverter : AbstractXmlHttpMessageConverter + { + public XmlDocumentHttpMessageConverter() : + base(new MediaType("application", "xml"), new MediaType("text", "xml"), new MediaType("application", "*+xml")) + { + } + + protected override bool Supports(Type type) + { + return type.Equals(typeof(XmlDocument)); + } + + protected override T ReadXml(XmlReader xmlReader, HttpWebResponse response) + { + XmlDocument document = new XmlDocument(); + document.Load(xmlReader); + return document as T; + } + + protected override void WriteXml(XmlWriter xmlWriter, object content, HttpWebRequest request) + { + XmlDocument document = content as XmlDocument; + document.WriteTo(xmlWriter); + } + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Http/Http/Converters/Xml/XmlSerializableHttpMessageConverter.cs b/src/Spring/Spring.Http/Http/Converters/Xml/XmlSerializableHttpMessageConverter.cs new file mode 100644 index 00000000..37eb38ac --- /dev/null +++ b/src/Spring/Spring.Http/Http/Converters/Xml/XmlSerializableHttpMessageConverter.cs @@ -0,0 +1,58 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Xml; +using System.Net; +using System.Xml.Serialization; + +using Spring.Util; + +namespace Spring.Http.Converters.Xml +{ + // TODO : Support for known types, etc... + public class XmlSerializableHttpMessageConverter : AbstractXmlHttpMessageConverter + { + public XmlSerializableHttpMessageConverter() : + base(new MediaType("application", "xml"), new MediaType("text", "xml"), new MediaType("application", "*+xml")) + { + } + + protected override bool Supports(Type type) + { + return true; + //return ( + // AttributeUtils.FindAttribute(type, typeof(XmlRootAttribute)) != null || + // AttributeUtils.FindAttribute(type, typeof(XmlTypeAttribute)) != null); + } + + protected override T ReadXml(XmlReader xmlReader, HttpWebResponse response) + { + XmlSerializer serializer = new XmlSerializer(typeof(T)); + return serializer.Deserialize(xmlReader) as T; + } + + protected override void WriteXml(XmlWriter xmlWriter, object content, HttpWebRequest request) + { + XmlSerializer serializer = new XmlSerializer(content.GetType()); + serializer.Serialize(xmlWriter, content); + } + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Http/Http/DefaultHttpWebRequestFactory.cs b/src/Spring/Spring.Http/Http/DefaultHttpWebRequestFactory.cs new file mode 100644 index 00000000..a73604fb --- /dev/null +++ b/src/Spring/Spring.Http/Http/DefaultHttpWebRequestFactory.cs @@ -0,0 +1,116 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Net; +using System.Security.Cryptography.X509Certificates; + +namespace Spring.Http +{ + /** + * Factory for {@link ClientHttpRequest} objects. + * Requests are created by the {@link #createRequest(URI, HttpMethod)} method. + * + * @author Arjen Poutsma + * @since 3.0 + */ + public class DefaultHttpWebRequestFactory : IHttpWebRequestFactory + { + // TODO : Add other properties + + private X509CertificateCollection _clientCertificates; + private ICredentials _credentials; + private IWebProxy _proxy; + private int? _timeout; + + public X509CertificateCollection ClientCertificates + { + get + { + if (this._clientCertificates == null) + { + this._clientCertificates = new X509CertificateCollection(); + } + return this._clientCertificates; + } + } + + public ICredentials Credentials + { + get { return _credentials; } + set { _credentials = value; } + } + + public IWebProxy Proxy + { + get { return _proxy; } + set { _proxy = value; } + } + + public int? Timeout + { + get { return _timeout; } + set { _timeout = value; } + } + + #region IHttpWebRequestFactory Membres + + /** + * Create a new {@link ClientHttpRequest} for the specified URI and HTTP method. + *

The returned request can be written to, and then executed by calling + * {@link ClientHttpRequest#execute()}. + * @param uri the URI to create a request for + * @param httpMethod the HTTP method to execute + * @return the created request + * @throws IOException in case of I/O errors + */ + public HttpWebRequest CreateRequest(Uri uri) + { + HttpWebRequest request = WebRequest.Create(uri) as HttpWebRequest; + + if (this._clientCertificates != null) + { + foreach (X509Certificate2 certificate in this._clientCertificates) + { + request.ClientCertificates.Add(certificate); + } + } + + if (this._credentials != null) + { + request.Credentials = this._credentials; + } + + if (this._proxy != null) + { + request.Proxy = this._proxy; + } + + if (this._timeout != null) + { + request.Timeout = this._timeout.Value; + } + + return request; + } + + #endregion + } +} diff --git a/src/Spring/Spring.Http/Http/HttpMethod.cs b/src/Spring/Spring.Http/Http/HttpMethod.cs new file mode 100644 index 00000000..e2be1362 --- /dev/null +++ b/src/Spring/Spring.Http/Http/HttpMethod.cs @@ -0,0 +1,44 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; + +namespace Spring.Http +{ + /** + * Java 5 enumeration of HTTP request methods. Intended for use + * with {@link org.springframework.http.client.ClientHttpRequest} + * and {@link org.springframework.web.client.RestTemplate}. + * + * @author Arjen Poutsma + * @since 3.0 + */ + public enum HttpMethod + { + OPTIONS, + GET, + HEAD, + POST, + PUT, + DELETE, + TRACE, + CONNECT + } +} diff --git a/src/Spring/Spring.Http/Http/HttpRequestMessage.cs b/src/Spring/Spring.Http/Http/HttpRequestMessage.cs new file mode 100644 index 00000000..f4a4bcbf --- /dev/null +++ b/src/Spring/Spring.Http/Http/HttpRequestMessage.cs @@ -0,0 +1,134 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System.Net; + +namespace Spring.Http +{ + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5 + public class HttpRequestMessage + { + //private string requestUri; + //private string httpVersion; + private HttpMethod method; + private WebHeaderCollection headers; + private object body; + + //public string RequestUri + //{ + // get { return this.requestUri; } + // set { this.requestUri = value; } + //} + + //public string HttpVersion + //{ + // get { return httpVersion; } + // set { httpVersion = value; } + //} + + public HttpMethod Method + { + get { return this.method; } + set { this.method = value; } + } + + /** + * Returns the headers of this message. + */ + public WebHeaderCollection Headers + { + get { return this.headers; } + } + + /** + * Returns the body of this message. + */ + public object Body + { + get { return this.body; } + } + + /** + * Create a new {@code HttpRequestMessage} with no body and no headers. + */ + public HttpRequestMessage(HttpMethod method) : + this(null, new WebHeaderCollection(), method) + { + } + + /** + * Create a new {@code HttpRequestMessage} with the given headers and no body. + * @param headers the message headers + */ + public HttpRequestMessage(WebHeaderCollection headers) : + this(null, headers, HttpMethod.GET) + { + } + + /** + * Create a new {@code HttpRequestMessage} with the given headers and no body. + * @param headers the message headers + */ + public HttpRequestMessage(WebHeaderCollection headers, HttpMethod method) : + this(null, headers, method) + { + } + + /** + * Create a new {@code HttpRequestMessage} with the given body and no headers. + * @param body the message body + */ + public HttpRequestMessage(object body) : + this(body, new WebHeaderCollection(), HttpMethod.GET) + { + } + + /** + * Create a new {@code HttpRequestMessage} with the given body and no headers. + * @param body the message body + */ + public HttpRequestMessage(object body, HttpMethod method) : + this(body, new WebHeaderCollection(), method) + { + } + + /** + * Create a new {@code HttpRequestMessage} with the given body and headers. + * @param body the messagae body + * @param headers the message headers + */ + public HttpRequestMessage(object body, WebHeaderCollection headers) : + this(body, headers, HttpMethod.GET) + { + } + + /** + * Create a new {@code HttpRequestMessage} with the given body and headers. + * @param body the messagae body + * @param headers the message headers + */ + public HttpRequestMessage(object body, WebHeaderCollection headers, HttpMethod method) + { + this.method = method; + this.body = body; + this.headers = headers; + } + } +} diff --git a/src/Spring/Spring.Http/Http/HttpResponseMessage.cs b/src/Spring/Spring.Http/Http/HttpResponseMessage.cs new file mode 100644 index 00000000..6f33bc6d --- /dev/null +++ b/src/Spring/Spring.Http/Http/HttpResponseMessage.cs @@ -0,0 +1,107 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System.Net; + +namespace Spring.Http +{ + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6 + public class HttpResponseMessage where T : class + { + private WebHeaderCollection headers; + private T body; + private HttpStatusCode statusCode; + private string statusDescription; + + /** + * Returns the headers of this entity. + */ + public WebHeaderCollection Headers + { + get { return this.headers; } + } + + /** + * Returns the body of this entity. + */ + public T Body + { + get { return this.body; } + } + + /** + * Return the HTTP status code of the response. + * @return the HTTP status as an HttpStatus enum value + */ + public HttpStatusCode StatusCode + { + get { return statusCode; } + } + + public string StatusDescription + { + get { return statusDescription; } + } + + /** + * Create a new {@code ResponseEntity} with the given status code, and no body, no headers. + * @param body the entity body + * @param statusCode the status code + */ + public HttpResponseMessage(HttpStatusCode statusCode, string statusDescription) : + this(null, null, statusCode, statusDescription) + { + } + + /** + * Create a new {@code ResponseEntity} with the given body and status code, and no headers. + * @param body the entity body + * @param statusCode the status code + */ + public HttpResponseMessage(T body, HttpStatusCode statusCode, string statusDescription) : + this(body, null, statusCode, statusDescription) + { + } + + /** + * Create a new {@code HttpEntity} with the given headers and status code, and no body. + * @param headers the entity headers + * @param statusCode the status code + */ + public HttpResponseMessage(WebHeaderCollection headers, HttpStatusCode statusCode, string statusDescription) : + this(null, headers, statusCode, statusDescription) + { + } + + /** + * Create a new {@code HttpEntity} with the given body, headers, and status code. + * @param body the entity body + * @param headers the entity headers + * @param statusCode the status code + */ + public HttpResponseMessage(T body, WebHeaderCollection headers, HttpStatusCode statusCode, string statusDescription) + { + this.statusCode = statusCode; + this.statusDescription = statusDescription; + this.body = body; + this.headers = headers; + } + } +} diff --git a/src/Spring/Spring.Http/Http/IHttpWebRequestFactory.cs b/src/Spring/Spring.Http/Http/IHttpWebRequestFactory.cs new file mode 100644 index 00000000..4a34e38e --- /dev/null +++ b/src/Spring/Spring.Http/Http/IHttpWebRequestFactory.cs @@ -0,0 +1,46 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Net; + +namespace Spring.Http +{ + /** + * Factory for {@link ClientHttpRequest} objects. + * Requests are created by the {@link #createRequest(URI, HttpMethod)} method. + * + * @author Arjen Poutsma + * @since 3.0 + */ + public interface IHttpWebRequestFactory + { + /** + * Create a new {@link ClientHttpRequest} for the specified URI and HTTP method. + *

The returned request can be written to, and then executed by calling + * {@link ClientHttpRequest#execute()}. + * @param uri the URI to create a request for + * @param httpMethod the HTTP method to execute + * @return the created request + * @throws IOException in case of I/O errors + */ + HttpWebRequest CreateRequest(Uri uri); + } +} diff --git a/src/Spring/Spring.Http/Http/MediaType.cs b/src/Spring/Spring.Http/Http/MediaType.cs new file mode 100644 index 00000000..07b0ee64 --- /dev/null +++ b/src/Spring/Spring.Http/Http/MediaType.cs @@ -0,0 +1,790 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Text; +using System.Globalization; +using System.Collections; +using System.Collections.Generic; + +using Spring.Util; + +namespace Spring.Http +{ + /** + * Represents an Internet Media Type, as defined in the HTTP specification. + * + *

Consists of a {@linkplain #getType() type} and a {@linkplain #getSubtype() subtype}. + * Also has functionality to parse media types from a string using {@link #parseMediaType(String)}, + * or multiple comma-separated media types using {@link #parseMediaTypes(String)}. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 3.0 + * @see HTTP 1.1, section 3.7 + */ + public class MediaType : IComparable + { + /** + * Public constant media type that includes all media ranges (i.e. */*). + */ + public static readonly MediaType ALL = new MediaType("*", "*"); + + /** + * Public constant media type for {@code application/atom+xml}. + */ + public static readonly MediaType APPLICATION_ATOM_XML = new MediaType("application", "atom+xml"); + + /** + * Public constant media type for {@code application/x-www-form-urlencoded}. + * */ + public static readonly MediaType APPLICATION_FORM_URLENCODED = new MediaType("application", "x-www-form-urlencoded"); + + /** + * Public constant media type for {@code application/json}. + * */ + public static readonly MediaType APPLICATION_JSON = new MediaType("application", "json"); + + /** + * Public constant media type for {@code application/octet-stream}. + * */ + public static readonly MediaType APPLICATION_OCTET_STREAM = new MediaType("application", "octet-stream"); + + /** + * Public constant media type for {@code application/xhtml+xml}. + * */ + public static readonly MediaType APPLICATION_XHTML_XML = new MediaType("application", "xhtml+xml"); + + /** + * Public constant media type for {@code image/gif}. + */ + public static readonly MediaType IMAGE_GIF = new MediaType("image", "gif"); + + /** + * Public constant media type for {@code image/jpeg}. + */ + public static readonly MediaType IMAGE_JPEG = new MediaType("image", "jpeg"); + + /** + * Public constant media type for {@code image/png}. + */ + public static readonly MediaType IMAGE_PNG = new MediaType("image", "png"); + + /** + * Public constant media type for {@code image/xml}. + */ + public static readonly MediaType APPLICATION_XML = new MediaType("application", "xml"); + + /** + * Public constant media type for {@code multipart/form-data}. + * */ + public static readonly MediaType MULTIPART_FORM_DATA = new MediaType("multipart", "form-data"); + + /** + * Public constant media type for {@code text/html}. + * */ + public static readonly MediaType TEXT_HTML = new MediaType("text", "html"); + + /** + * Public constant media type for {@code text/plain}. + * */ + public static readonly MediaType TEXT_PLAIN = new MediaType("text", "plain"); + + /** + * Public constant media type for {@code text/xml}. + * */ + public static readonly MediaType TEXT_XML = new MediaType("text", "xml"); + + + private const string WILDCARD_TYPE = "*"; + + private const string PARAM_QUALITY_FACTOR = "q"; + + private const string PARAM_CHARSET = "charset"; + + private string type; + + private string subtype; + + private IDictionary parameters; + + /** + * Return the primary type. + */ + public string Type + { + get { return this.type; } + } + + /** + * Return the subtype. + */ + public string Subtype + { + get { return this.subtype; } + } + + /** + * Indicate whether the {@linkplain #getType() type} is the wildcard character * or not. + */ + public bool IsWildcardType + { + get { return WILDCARD_TYPE == type; } + } + + /** + * Indicate whether the {@linkplain #getSubtype() subtype} is the wildcard character * or not. + * @return whether the subtype is * + */ + public bool IsWildcardSubtype + { + get { return WILDCARD_TYPE == subtype; } + } + + /** + * Return the character set, as indicated by a charset parameter, if any. + * @return the character set; or null if not available + */ + public string CharSet + { + get + { + string charSet = null; + this.parameters.TryGetValue(PARAM_CHARSET, out charSet); + return charSet; + //string charSet = this.parameters[PARAM_CHARSET]; + //return (charSet != null ? Charset.forName(charSet) : null); + } + } + + /** + * Return the quality value, as indicated by a q parameter, if any. + * Defaults to 1.0. + * @return the quality factory + */ + public double QualityValue + { + get + { + string qualityFactory = null; + return this.parameters.TryGetValue(PARAM_QUALITY_FACTOR, out qualityFactory) + ? Double.Parse(qualityFactory, CultureInfo.InvariantCulture) + : 1D; + } + } + + /** + * Create a new {@link MediaType} for the given primary type. + *

The {@linkplain #getSubtype() subtype} is set to *, parameters empty. + * @param type the primary type + * @throws IllegalArgumentException if any of the parameters contain illegal characters + */ + public MediaType(string type) : + this(type, WILDCARD_TYPE) + { + } + + /** + * Create a new {@link MediaType} for the given primary type and subtype. + *

The parameters are empty. + * @param type the primary type + * @param subtype the subtype + * @throws IllegalArgumentException if any of the parameters contain illegal characters + */ + public MediaType(string type, string subtype) : + this(type, subtype, new Dictionary(StringComparer.InvariantCultureIgnoreCase)) + { + } + + /** + * Create a new {@link MediaType} for the given type, subtype, and character set. + * @param type the primary type + * @param subtype the subtype + * @param charSet the character set + * @throws IllegalArgumentException if any of the parameters contain illegal characters + */ + public MediaType(string type, string subtype, string charSet) : + this(type, subtype) + { + this.parameters.Add(PARAM_CHARSET, charSet); + } + + /** + * Create a new {@link MediaType} for the given type, subtype, and quality value. + * + * @param type the primary type + * @param subtype the subtype + * @param qualityValue the quality value + * @throws IllegalArgumentException if any of the parameters contain illegal characters + */ + public MediaType(String type, String subtype, double qualityValue) : + this(type, subtype) + { + this.parameters.Add(PARAM_QUALITY_FACTOR, qualityValue.ToString(CultureInfo.InvariantCulture)); + } + + /** + * Copy-constructor that copies the type and subtype of the given {@link MediaType}, + * and allows for different parameter. + * @param other the other media type + * @param parameters the parameters, may be null + * @throws IllegalArgumentException if any of the parameters contain illegal characters + */ + public MediaType(MediaType other, IDictionary parameters) : + this(other.Type, other.Subtype, parameters) + { + } + + /** + * Create a new {@link MediaType} for the given type, subtype, and parameters. + * @param type the primary type + * @param subtype the subtype + * @param parameters the parameters, may be null + * @throws IllegalArgumentException if any of the parameters contain illegal characters + */ + public MediaType(string type, string subtype, IDictionary parameters) + { + AssertUtils.ArgumentHasText(type, "'type' must not be empty"); + AssertUtils.ArgumentHasText(subtype, "'subtype' must not be empty"); + //checkToken(type); + //checkToken(subtype); + this.type = type.ToLowerInvariant(); + this.subtype = subtype.ToLowerInvariant(); + this.parameters = new Dictionary(parameters, StringComparer.InvariantCultureIgnoreCase); + //if (parameters.Count > 0) + //{ + // NameValueCollection m = new NameValueCollection(parameters.Count, null, new CaseInsensitiveComparer()); + // for (Map.Entry entry : parameters.entrySet()) { + // String attribute = entry.getKey(); + // String value = entry.getValue(); + // checkParameters(attribute, value); + // m.put(attribute, unquote(value)); + // } + // this.parameters = Collections.unmodifiableMap(m); + //} + //else + //{ + // this.parameters = Collections.emptyMap(); + //} + } + + public override bool Equals(object obj) + { + if (this == obj) + { + return true; + } + if (obj is MediaType) + { + MediaType otherMediaType = (MediaType)obj; + if (this.type == otherMediaType.type && + this.subtype == otherMediaType.subtype) + { + if (otherMediaType.parameters.Count == this.parameters.Count) + { + foreach(string key in this.parameters.Keys) + { + if (!otherMediaType.parameters.ContainsKey(key) || + !String.Equals(otherMediaType.parameters[key], this.parameters[key])) + { + return false; + } + } + return true; + } + } + } + return false; + } + + public override int GetHashCode() + { + int result = this.type.GetHashCode(); + result = 31 * result + this.subtype.GetHashCode(); + result = 31 * result + this.parameters.GetHashCode(); + return result; + } + + public override string ToString() + { + StringBuilder builder = new StringBuilder(); + builder.Append(this.type); + builder.Append('/'); + builder.Append(this.subtype); + foreach(string key in this.parameters.Keys) + { + builder.Append(';'); + builder.Append(key); + builder.Append('='); + builder.Append(this.parameters[key]); + } + return builder.ToString(); + } + + /** + * Checks the given token string for illegal characters, as defined in RFC 2616, section 2.2. + * @throws IllegalArgumentException in case of illegal characters + * @see HTTP 1.1, section 2.2 + */ + //private void checkToken(String s) { + // for (int i=0; i < s.length(); i++ ) { + // char ch = s.charAt(i); + // if (!TOKEN.get(ch)) { + // throw new IllegalArgumentException("Invalid token character '" + ch + "' in token \"" + s + "\""); + // } + // } + //} + + //private void checkParameters(String attribute, String value) { + // Assert.hasLength(attribute, "parameter attribute must not be empty"); + // Assert.hasLength(value, "parameter value must not be empty"); + // checkToken(attribute); + // if (PARAM_QUALITY_FACTOR.equals(attribute)) { + // value = unquote(value); + // double d = Double.parseDouble(value); + // Assert.isTrue(d >= 0D && d <= 1D, + // "Invalid quality value \"" + value + "\": should be between 0.0 and 1.0"); + // } + // else if (PARAM_CHARSET.equals(attribute)) { + // value = unquote(value); + // Charset.forName(value); + // } + // else if (!isQuotedString(value)) { + // checkToken(value); + // } + //} + + //private boolean isQuotedString(String s) { + // return s.length() > 1 && s.startsWith("\"") && s.endsWith("\"") ; + //} + + //private String unquote(String s) { + // if (s == null) { + // return null; + // } + // return isQuotedString(s) ? s.substring(1, s.length() - 1) : s; + //} + + /** + * Return a generic parameter value, given a parameter name. + * @param name the parameter name + * @return the parameter value; or null if not present + */ + public string GetParameter(string name) + { + return this.parameters[name]; + } + + /** + * Indicate whether this {@link MediaType} includes the given media type. + *

For instance, {@code text/*} includes {@code text/plain}, {@code text/html}, and {@code application/*+xml} + * includes {@code application/soap+xml}, etc. This method is non-symmetic. + * @param other the reference media type with which to compare + * @return true if this media type includes the given media type; false otherwise + */ + public bool Includes(MediaType other) + { + if (other == null) + { + return false; + } + if (this.IsWildcardType) + { + // */* includes anything + return true; + } + else if (this.type == other.type) + { + if (this.subtype == other.subtype || this.IsWildcardSubtype) + { + return true; + } + // application/*+xml includes application/soap+xml + int thisPlusIdx = this.subtype.IndexOf('+'); + int otherPlusIdx = other.subtype.IndexOf('+'); + if (thisPlusIdx != -1 && otherPlusIdx != -1) + { + string thisSubtypeNoSuffix = this.subtype.Substring(0, thisPlusIdx); + + string thisSubtypeSuffix = this.subtype.Substring(thisPlusIdx + 1); + string otherSubtypeSuffix = other.subtype.Substring(otherPlusIdx + 1); + if (thisSubtypeSuffix == otherSubtypeSuffix && WILDCARD_TYPE == thisSubtypeNoSuffix) + { + return true; + } + } + } + return false; + } + + /** + * Indicate whether this {@link MediaType} is compatible with the given media type. + *

For instance, {@code text/*} is compatible with {@code text/plain}, {@code text/html}, and vice versa. + * In effect, this method is similar to {@link #includes(MediaType)}, except that it's symmetric. + * @param other the reference media type with which to compare + * @return true if this media type is compatible with the given media type; false otherwise + */ + public bool IsCompatibleWith(MediaType other) + { + if (other == null) + { + return false; + } + if (this.IsWildcardType || other.IsWildcardType) + { + return true; + } + else if (this.type == other.type) + { + if (this.subtype == other.subtype || this.IsWildcardSubtype || other.IsWildcardSubtype) + { + return true; + } + // application/*+xml is compatible with application/soap+xml, and vice-versa + int thisPlusIdx = this.subtype.IndexOf('+'); + int otherPlusIdx = other.subtype.IndexOf('+'); + if (thisPlusIdx != -1 && otherPlusIdx != -1) + { + string thisSubtypeNoSuffix = this.subtype.Substring(0, thisPlusIdx); + string otherSubtypeNoSuffix = other.subtype.Substring(0, otherPlusIdx); + + string thisSubtypeSuffix = this.subtype.Substring(thisPlusIdx + 1); + string otherSubtypeSuffix = other.subtype.Substring(otherPlusIdx + 1); + + if (thisSubtypeSuffix == otherSubtypeSuffix && + (WILDCARD_TYPE == thisSubtypeNoSuffix || WILDCARD_TYPE == otherSubtypeNoSuffix)) + { + return true; + } + } + } + return false; + } + + #region IComparable Membres + + /** + * Compares this {@link MediaType} to another alphabetically. + * @param other media type to compare to + * @see #sortBySpecificity(List) + */ + public int CompareTo(MediaType other) + { + int comp = this.type.CompareTo(other.type); + if (comp != 0) + { + return comp; + } + comp = this.subtype.CompareTo(other.subtype); + if (comp != 0) + { + return comp; + } + comp = this.parameters.Count - other.parameters.Count; + if (comp != 0) + { + return comp; + } + foreach(string key in this.parameters.Keys) + { + if (!other.parameters.ContainsKey(key)) + { + return -1; + } + comp = String.Compare(this.parameters[key], other.parameters[key]); + if (comp != 0) + { + return comp; + } + } + return 0; + } + + #endregion + + /** + * Parse the given String into a single {@link MediaType}. + * @param mediaType the string to parse + * @return the media type + * @throws IllegalArgumentException if the string cannot be parsed + */ + public static MediaType ParseMediaType(string mediaType) + { + AssertUtils.ArgumentHasText(mediaType, "'mediaType' must not be empty"); + + string[] parts = mediaType.Split(';'); + string fullType = parts[0].Trim(); + if (fullType == WILDCARD_TYPE) + { + fullType = "*/*"; + } + int subIndex = fullType.IndexOf('/'); + if (subIndex == -1) + { + throw new ArgumentException( + String.Format("'{0}' does not contain '/'", mediaType), + "mediaType"); + } + if (subIndex == fullType.Length - 1) + { + throw new ArgumentException( + String.Format("'{0}' does not contain subtype after '/'", mediaType), + "mediaType"); + } + string type = fullType.Substring(0, subIndex); + string subtype = fullType.Substring(subIndex + 1); + + IDictionary parameters = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + if (parts.Length > 1) + { + for (int i = 1; i < parts.Length; i++) + { + string parameter = parts[i].Trim(); + int eqIndex = parameter.IndexOf('='); + if (eqIndex != -1) + { + string attribute = parameter.Substring(0, eqIndex); + string value = parameter.Substring(eqIndex + 1); + parameters.Add(attribute, value); + } + } + } + + return new MediaType(type, subtype, parameters); + } + + /** + * Parse the given, comma-seperated string into a list of {@link MediaType} objects. + *

This method can be used to parse an Accept or Content-Type header. + * @param mediaTypes the string to parse + * @return the list of media types + * @throws IllegalArgumentException if the string cannot be parsed + */ + public static List ParseMediaTypes(string mediaTypes) + { + List mediaTypeList = new List(); + if (!StringUtils.HasLength(mediaTypes)) + { + return mediaTypeList; + } + string[] tokens = mediaTypes.Split(','); + foreach (string token in tokens) + { + mediaTypeList.Add(ParseMediaType(token)); + } + return mediaTypeList; + } + + /** + * Return a string representation of the given list of {@link MediaType} objects. + *

This method can be used to for an {@code Accept} or {@code Content-Type} header. + * @param mediaTypes the string to parse + * @return the list of media types + * @throws IllegalArgumentException if the String cannot be parsed + */ + public static string ToString(IEnumerable mediaTypes) + { + StringBuilder builder = new StringBuilder(); + foreach(MediaType mediaType in mediaTypes) + { + if (builder.Length > 0) + { + builder.Append(", "); + } + builder.Append(mediaType); + } + return builder.ToString(); + } + + /** + * Sorts the given list of {@link MediaType} objects by specificity. + *

Given two media types: + *

    + *
  1. if either media type has a {@linkplain #isWildcardType() wildcard type}, then the media type without the + * wildcard is ordered before the other.
  2. + *
  3. if the two media types have different {@linkplain #getType() types}, then they are considered equal and + * remain their current order.
  4. + *
  5. if either media type has a {@linkplain #isWildcardSubtype() wildcard subtype}, then the media type without + * the wildcard is sorted before the other.
  6. + *
  7. if the two media types have different {@linkplain #getSubtype() subtypes}, then they are considered equal + * and remain their current order.
  8. + *
  9. if the two media types have different {@linkplain #getQualityValue() quality value}, then the media type + * with the highest quality value is ordered before the other.
  10. + *
  11. if the two media types have a different amount of {@linkplain #getParameter(String) parameters}, then the + * media type with the most parameters is ordered before the other.
  12. + *
+ *

For example: + *

audio/basic < audio/* < */*
+ *
audio/* < audio/*;q=0.7; audio/*;q=0.3
+ *
audio/basic;level=1 < audio/basic
+ *
audio/basic == text/html
+ *
audio/basic == audio/wave
+ * @param mediaTypes the list of media types to be sorted + * @see HTTP 1.1, section 14.1 + */ + public static void SortBySpecificity(List mediaTypes) + { + AssertUtils.ArgumentNotNull(mediaTypes, "mediaTypes"); + + if (mediaTypes.Count > 1) + { + mediaTypes.Sort(SPECIFICITY_COMPARER); + } + } + + /** + * Sorts the given list of {@link MediaType} objects by quality value. + *

Given two media types: + *

    + *
  1. if the two media types have different {@linkplain #getQualityValue() quality value}, then the media type + * with the highest quality value is ordered before the other.
  2. + *
  3. if either media type has a {@linkplain #isWildcardType() wildcard type}, then the media type without the + * wildcard is ordered before the other.
  4. + *
  5. if the two media types have different {@linkplain #getType() types}, then they are considered equal and + * remain their current order.
  6. + *
  7. if either media type has a {@linkplain #isWildcardSubtype() wildcard subtype}, then the media type without + * the wildcard is sorted before the other.
  8. + *
  9. if the two media types have different {@linkplain #getSubtype() subtypes}, then they are considered equal + * and remain their current order.
  10. + *
  11. if the two media types have a different amount of {@linkplain #getParameter(String) parameters}, then the + * media type with the most parameters is ordered before the other.
  12. + *
+ * @param mediaTypes the list of media types to be sorted + * @see #getQualityValue() + */ + public static void SortByQualityValue(List mediaTypes) + { + AssertUtils.ArgumentNotNull(mediaTypes, "mediaTypes"); + + if (mediaTypes.Count > 1) + { + mediaTypes.Sort(QUALITY_VALUE_COMPARER); + } + } + + public static IComparer SPECIFICITY_COMPARER = new SpecificityComparer(); + public static IComparer QUALITY_VALUE_COMPARER = new QualityValueComparer(); + + #region SpecificityComparer + + private class SpecificityComparer : IComparer + { + public int Compare(MediaType x, MediaType y) + { + if (x.IsWildcardType && !y.IsWildcardType) + { // */* < audio/* + return 1; + } + else if (y.IsWildcardType && !x.IsWildcardType) + { // audio/* > */* + return -1; + } + else if (x.type != y.type) + { // audio/basic == text/html + return 0; + } + else + { // mediaType1.type == mediaType2.type + if (x.IsWildcardSubtype && !y.IsWildcardSubtype) + { // audio/* < audio/basic + return 1; + } + else if (y.IsWildcardSubtype && !x.IsWildcardSubtype) + { // audio/basic > audio/* + return -1; + } + else if (x.subtype != y.subtype) + { // audio/basic == audio/wave + return 0; + } + else + { // mediaType2.subtype == mediaType2.subtype + double quality1 = x.QualityValue; + double quality2 = y.QualityValue; + int qualityComparison = quality2.CompareTo(quality1); + if (qualityComparison != 0) + { + return qualityComparison; // audio/*;q=0.7 < audio/*;q=0.3 + } + else + { + int paramsSize1 = x.parameters.Count; + int paramsSize2 = y.parameters.Count; + return (paramsSize2 < paramsSize1 ? -1 : (paramsSize2 == paramsSize1 ? 0 : 1)); // audio/basic;level=1 < audio/basic + } + } + } + } + } + + #endregion + + #region QualityValueComparer + + private class QualityValueComparer : IComparer + { + public int Compare(MediaType x, MediaType y) + { + double quality1 = x.QualityValue; + double quality2 = y.QualityValue; + int qualityComparison = quality2.CompareTo(quality1); + if (qualityComparison != 0) + { + return qualityComparison; // audio/*;q=0.7 < audio/*;q=0.3 + } + else if (x.IsWildcardType && !y.IsWildcardType) + { // */* < audio/* + return 1; + } + else if (y.IsWildcardType && !x.IsWildcardType) + { // audio/* > */* + return -1; + } + else if (x.type != y.type) + { // audio/basic == text/html + return 0; + } + else + { // mediaType1.type == mediaType2.type + if (x.IsWildcardSubtype && !y.IsWildcardSubtype) + { // audio/* < audio/basic + return 1; + } + else if (y.IsWildcardSubtype && !x.IsWildcardSubtype) + { // audio/basic > audio/* + return -1; + } + else if (x.subtype != y.subtype) + { // audio/basic == audio/wave + return 0; + } + else + { + int paramsSize1 = x.parameters.Count; + int paramsSize2 = y.parameters.Count; + return (paramsSize2 < paramsSize1 ? -1 : (paramsSize2 == paramsSize1 ? 0 : 1)); // audio/basic;level=1 < audio/basic + } + } + } + } + + #endregion + } +} diff --git a/src/Spring/Spring.Http/Http/Rest/IRequestCallback.cs b/src/Spring/Spring.Http/Http/Rest/IRequestCallback.cs new file mode 100644 index 00000000..11c2cc58 --- /dev/null +++ b/src/Spring/Spring.Http/Http/Rest/IRequestCallback.cs @@ -0,0 +1,47 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Net; + +namespace Spring.Http.Rest +{ + /** + * Callback interface for code that operates on a {@link ClientHttpRequest}. Allows to manipulate the request + * headers, and write to the request body. + * + *

Used internally by the {@link RestTemplate}, but also useful for application code. + * + * @author Arjen Poutsma + * @see RestTemplate#execute + * @since 3.0 + */ + public interface IRequestCallback + { + /** + * Gets called by {@link RestTemplate#execute} with an opened {@code ClientHttpRequest}. + * Does not need to care about closing the request or about handling errors: + * this will all be handled by the {@code RestTemplate}. + * @param request the active HTTP request + * @throws IOException in case of I/O errors + */ + void DoWithRequest(HttpWebRequest request); + } +} diff --git a/src/Spring/Spring.Http/Http/Rest/IResponseExtractor.cs b/src/Spring/Spring.Http/Http/Rest/IResponseExtractor.cs new file mode 100644 index 00000000..3aa609ce --- /dev/null +++ b/src/Spring/Spring.Http/Http/Rest/IResponseExtractor.cs @@ -0,0 +1,48 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Net; + +namespace Spring.Http.Rest +{ + /** + * Generic callback interface used by {@link RestTemplate}'s retrieval methods + * Implementations of this interface perform the actual work of extracting data + * from a {@link ClientHttpResponse}, but don't need to worry about exception + * handling or closing resources. + * + *

Used internally by the {@link RestTemplate}, but also useful for application code. + * + * @author Arjen Poutsma + * @since 3.0 + * @see RestTemplate#execute + */ + public interface IResponseExtractor where T : class + { + /** + * Extract data from the given {@code ClientHttpResponse} and return it. + * @param response the HTTP response + * @return the extracted data + * @throws IOException in case of I/O errors + */ + T ExtractData(HttpWebResponse response); // throws IOException; + } +} diff --git a/src/Spring/Spring.Http/Http/Rest/IRestOperations.cs b/src/Spring/Spring.Http/Http/Rest/IRestOperations.cs new file mode 100644 index 00000000..82b32130 --- /dev/null +++ b/src/Spring/Spring.Http/Http/Rest/IRestOperations.cs @@ -0,0 +1,439 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Net; +using System.Collections.Generic; + +using Spring.Http; + +namespace Spring.Http.Rest +{ + public interface IRestOperations + { + #region GET + + /** + * Retrieve a representation by doing a GET on the specified URL. + * The response (if any) is converted and returned. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param responseType the type of the return value + * @param uriVariables the variables to expand the template + * @return the converted object + */ + T GetForObject(string url, params string[] uriVariables) where T : class; //throws RestClientException; + + /** + * Retrieve a representation by doing a GET on the URI template. + * The response (if any) is converted and returned. + *

URI Template variables are expanded using the given map. + * @param url the URL + * @param responseType the type of the return value + * @param uriVariables the map containing variables for the URI template + * @return the converted object + */ + T GetForObject(string url, IDictionary uriVariables) where T : class; //throws RestClientException; + + /** + * Retrieve a representation by doing a GET on the URL . + * The response (if any) is converted and returned. + * @param url the URL + * @param responseType the type of the return value + * @return the converted object + */ + T GetForObject(Uri url) where T : class; //throws RestClientException; + + /** + * Retrieve an entity by doing a GET on the specified URL. + * The response is converted and stored in an {@link ResponseEntity}. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param responseType the type of the return value + * @param uriVariables the variables to expand the template + * @return the entity + * @since 3.0.2 + */ + HttpResponseMessage GetForMessage(string url, params string[] uriVariables) where T : class; //throws RestClientException; + + /** + * Retrieve a representation by doing a GET on the URI template. + * The response is converted and stored in an {@link ResponseEntity}. + *

URI Template variables are expanded using the given map. + * @param url the URL + * @param responseType the type of the return value + * @param uriVariables the map containing variables for the URI template + * @return the converted object + * @since 3.0.2 + */ + HttpResponseMessage GetForMessage(string url, IDictionary uriVariables) where T : class; //throws RestClientException; + + /** + * Retrieve a representation by doing a GET on the URL . + * The response is converted and stored in an {@link ResponseEntity}. + * @param url the URL + * @param responseType the type of the return value + * @return the converted object + * @since 3.0.2 + */ + HttpResponseMessage GetForMessage(Uri url) where T : class; //throws RestClientException; + + #endregion + + #region HEAD + + /** + * Retrieve all headers of the resource specified by the URI template. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand the template + * @return all HTTP headers of that resource + */ + WebHeaderCollection HeadForHeaders(string url, params string[] uriVariables); //throws RestClientException; + + /** + * Retrieve all headers of the resource specified by the URI template. + *

URI Template variables are expanded using the given map. + * @param url the URL + * @param uriVariables the map containing variables for the URI template + * @return all HTTP headers of that resource + */ + WebHeaderCollection HeadForHeaders(string url, IDictionary uriVariables); //throws RestClientException; + + /** + * Retrieve all headers of the resource specified by the URL. + * @param url the URL + * @return all HTTP headers of that resource + */ + WebHeaderCollection HeadForHeaders(Uri url); //throws RestClientException; + + #endregion + + #region POST + + /** + * Create a new resource by POSTing the given object to the URI template, and returns the value of the + * Location header. This header typically indicates where the new resource is stored. + *

URI Template variables are expanded using the given URI variables, if any. + *

The {@code request} parameter can be a {@link HttpEntity} in order to + * add additional HTTP headers to the request. + * @param url the URL + * @param request the Object to be POSTed, may be null + * @param uriVariables the variables to expand the template + * @return the value for the Location header + * @see HttpEntity + */ + Uri PostForLocation(string url, object request, params string[] uriVariables); //throws RestClientException; + + /** + * Create a new resource by POSTing the given object to the URI template, and returns the value of the + * Location header. This header typically indicates where the new resource is stored. + *

URI Template variables are expanded using the given map. + *

The {@code request} parameter can be a {@link HttpEntity} in order to + * add additional HTTP headers to the request. + * @param url the URL + * @param request the Object to be POSTed, may be null + * @param uriVariables the variables to expand the template + * @return the value for the Location header + * @see HttpEntity + */ + Uri PostForLocation(string url, object request, IDictionary uriVariables); //throws RestClientException; + + /** + * Create a new resource by POSTing the given object to the URL, and returns the value of the + * Location header. This header typically indicates where the new resource is stored. + *

The {@code request} parameter can be a {@link HttpEntity} in order to + * add additional HTTP headers to the request. + * @param url the URL + * @param request the Object to be POSTed, may be null + * @return the value for the Location header + * @see HttpEntity + */ + Uri PostForLocation(Uri url, object request); //throws RestClientException; + + /** + * Create a new resource by POSTing the given object to the URI template, + * and returns the representation found in the response. + *

URI Template variables are expanded using the given URI variables, if any. + *

The {@code request} parameter can be a {@link HttpEntity} in order to + * add additional HTTP headers to the request. + * @param url the URL + * @param request the Object to be POSTed, may be null + * @param responseType the type of the return value + * @param uriVariables the variables to expand the template + * @return the converted object + * @see HttpEntity + */ + T PostForObject(string url, object request, params string[] uriVariables) where T : class; //throws RestClientException; + + /** + * Create a new resource by POSTing the given object to the URI template, + * and returns the representation found in the response. + *

URI Template variables are expanded using the given map. + *

The {@code request} parameter can be a {@link HttpEntity} in order to + * add additional HTTP headers to the request. + * @param url the URL + * @param request the Object to be POSTed, may be null + * @param responseType the type of the return value + * @param uriVariables the variables to expand the template + * @return the converted object + * @see HttpEntity + */ + T PostForObject(string url, object request, IDictionary uriVariables) where T : class; //throws RestClientException; + + /** + * Create a new resource by POSTing the given object to the URL, + * and returns the representation found in the response. + *

The {@code request} parameter can be a {@link HttpEntity} in order to + * add additional HTTP headers to the request. + * @param url the URL + * @param request the Object to be POSTed, may be null + * @param responseType the type of the return value + * @return the converted object + * @see HttpEntity + */ + T PostForObject(Uri url, object request) where T : class; //throws RestClientException; + + /** + * Create a new resource by POSTing the given object to the URI template, + * and returns the response as {@link ResponseEntity}. + *

URI Template variables are expanded using the given URI variables, if any. + *

The {@code request} parameter can be a {@link HttpEntity} in order to + * add additional HTTP headers to the request. + * @param url the URL + * @param request the Object to be POSTed, may be null + * @param uriVariables the variables to expand the template + * @return the converted object + * @see HttpEntity + * @since 3.0.2 + */ + HttpResponseMessage PostForMessage(string url, object request, params string[] uriVariables) where T : class; //throws RestClientException; + + /** + * Create a new resource by POSTing the given object to the URI template, + * and returns the response as {@link HttpEntity}. + *

URI Template variables are expanded using the given map. + *

The {@code request} parameter can be a {@link HttpEntity} in order to + * add additional HTTP headers to the request. + * @param url the URL + * @param request the Object to be POSTed, may be null + * @param uriVariables the variables to expand the template + * @return the converted object + * @see HttpEntity + * @since 3.0.2 + */ + HttpResponseMessage PostForMessage(string url, object request, IDictionary uriVariables) where T : class; //throws RestClientException; + + /** + * Create a new resource by POSTing the given object to the URL, + * and returns the response as {@link ResponseEntity}. + *

The {@code request} parameter can be a {@link HttpEntity} in order to + * add additional HTTP headers to the request. + * @param url the URL + * @param request the Object to be POSTed, may be null + * @return the converted object + * @see HttpEntity + * @since 3.0.2 + */ + HttpResponseMessage PostForMessage(Uri url, object request) where T : class; //throws RestClientException; + + #endregion + + #region PUT + + /** + * Create or update a resource by PUTting the given object to the URI. + *

URI Template variables are expanded using the given URI variables, if any. + *

The {@code request} parameter can be a {@link HttpEntity} in order to + * add additional HTTP headers to the request. + * @param url the URL + * @param request the Object to be PUT, may be null + * @param uriVariables the variables to expand the template + * @see HttpEntity + */ + void Put(string url, object request, params string[] uriVariables); //throws RestClientException; + + /** + * Creates a new resource by PUTting the given object to URI template. + *

URI Template variables are expanded using the given map. + *

The {@code request} parameter can be a {@link HttpEntity} in order to + * add additional HTTP headers to the request. + * @param url the URL + * @param request the Object to be PUT, may be null + * @param uriVariables the variables to expand the template + * @see HttpEntity + */ + void Put(string url, object request, IDictionary uriVariables); //throws RestClientException; + + /** + * Creates a new resource by PUTting the given object to URL. + *

The {@code request} parameter can be a {@link HttpEntity} in order to + * add additional HTTP headers to the request. + * @param url the URL + * @param request the Object to be PUT, may be null + * @see HttpEntity + */ + void Put(Uri url, object request); //throws RestClientException; + + #endregion + + #region DELETE + + /** + * Delete the resources at the specified URI. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand in the template + */ + void Delete(string url, params string[] uriVariables); //throws RestClientException; + + /** + * Delete the resources at the specified URI. + *

URI Template variables are expanded using the given map. + * + * @param url the URL + * @param uriVariables the variables to expand the template + */ + void Delete(string url, IDictionary uriVariables); //throws RestClientException; + + /** + * Delete the resources at the specified URL. + * @param url the URL + */ + void Delete(Uri url); //throws RestClientException; + + #endregion + + #region OPTIONS + + /** + * Return the value of the Allow header for the given URI. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand in the template + * @return the value of the allow header + */ + IList OptionsForAllow(string url, params string[] uriVariables); //throws RestClientException; + + /** + * Return the value of the Allow header for the given URI. + *

URI Template variables are expanded using the given map. + * @param url the URL + * @param uriVariables the variables to expand in the template + * @return the value of the allow header + */ + IList OptionsForAllow(string url, IDictionary uriVariables); //throws RestClientException; + + /** + * Return the value of the Allow header for the given URL. + * @param url the URL + * @return the value of the allow header + */ + IList OptionsForAllow(Uri url); //throws RestClientException; + + #endregion + + + #region Exchange + + /** + * Execute the HTTP method to the given URI template, writing the given request entity to the request, and + * returns the response as {@link ResponseEntity}. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param method the HTTP method (GET, POST, etc) + * @param requestEntity the entity (headers and/or body) to write to the request, may be {@code null} + * @param responseType the type of the return value + * @param uriVariables the variables to expand in the template + * @return the response as entity + * @since 3.0.2 + */ + HttpResponseMessage Exchange(string url, HttpRequestMessage requestMessage, params string[] uriVariables) where T : class; //throws RestClientException; + + /** + * Execute the HTTP method to the given URI template, writing the given request entity to the request, and + * returns the response as {@link ResponseEntity}. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param method the HTTP method (GET, POST, etc) + * @param requestEntity the entity (headers and/or body) to write to the request, may be {@code null} + * @param responseType the type of the return value + * @param uriVariables the variables to expand in the template + * @return the response as entity + * @since 3.0.2 + */ + HttpResponseMessage Exchange(string url, HttpRequestMessage requestMessage, IDictionary uriVariables) where T : class; //throws RestClientException; + + /** + * Execute the HTTP method to the given URI template, writing the given request entity to the request, and + * returns the response as {@link ResponseEntity}. + * @param url the URL + * @param method the HTTP method (GET, POST, etc) + * @param requestEntity the entity (headers and/or body) to write to the request, may be {@code null} + * @param responseType the type of the return value + * @return the response as entity + * @since 3.0.2 + */ + HttpResponseMessage Exchange(Uri url, HttpRequestMessage requestMessage) where T : class; //throws RestClientException; + + #endregion + + #region General execution + + /** + * Execute the HTTP method to the given URI template, preparing the request with the + * {@link RequestCallback}, and reading the response with a {@link ResponseExtractor}. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param method the HTTP method (GET, POST, etc) + * @param requestCallback object that prepares the request + * @param responseExtractor object that extracts the return value from the response + * @param uriVariables the variables to expand in the template + * @return an arbitrary object, as returned by the {@link ResponseExtractor} + */ + T Execute(string url, IRequestCallback requestCallback, IResponseExtractor responseExtractor, params string[] uriVariables) where T : class; //throws RestClientException; + + /** + * Execute the HTTP method to the given URI template, preparing the request with the + * {@link RequestCallback}, and reading the response with a {@link ResponseExtractor}. + *

URI Template variables are expanded using the given URI variables map. + * @param url the URL + * @param method the HTTP method (GET, POST, etc) + * @param requestCallback object that prepares the request + * @param responseExtractor object that extracts the return value from the response + * @param uriVariables the variables to expand in the template + * @return an arbitrary object, as returned by the {@link ResponseExtractor} + */ + T Execute(string url, IRequestCallback requestCallback, IResponseExtractor responseExtractor, IDictionary uriVariables) where T : class; //throws RestClientException; + + /** + * Execute the HTTP method to the given URL, preparing the request with the + * {@link RequestCallback}, and reading the response with a {@link ResponseExtractor}. + * @param url the URL + * @param method the HTTP method (GET, POST, etc) + * @param requestCallback object that prepares the request + * @param responseExtractor object that extracts the return value from the response + * @return an arbitrary object, as returned by the {@link ResponseExtractor} + */ + T Execute(Uri url, IRequestCallback requestCallback, IResponseExtractor responseExtractor) where T : class; //throws RestClientException; + + #endregion + } +} diff --git a/src/Spring/Spring.Http/Http/Rest/RestClientException.cs b/src/Spring/Spring.Http/Http/Rest/RestClientException.cs new file mode 100644 index 00000000..52fdf86e --- /dev/null +++ b/src/Spring/Spring.Http/Http/Rest/RestClientException.cs @@ -0,0 +1,78 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Runtime.Serialization; + +namespace Spring.Http.Rest +{ + [Serializable] + public class RestClientException : Exception + { + ///

+ /// Creates a new instance of the + /// RestClientException class. + /// + public RestClientException() + { + } + + /// + /// Creates a new instance of the RestClientException class. + /// + /// + /// A message about the exception. + /// + public RestClientException(string message) + : base(message) + { + } + + /// + /// Creates a new instance of the RestClientException class. + /// + /// + /// A message about the exception. + /// + /// + /// The root exception that is being wrapped. + /// + public RestClientException(string message, Exception rootCause) + : base(message, rootCause) + { + } + + /// + /// Creates a new instance of the RestClientException class. + /// + /// + /// The + /// that holds the serialized object data about the exception being thrown. + /// + /// + /// The + /// that contains contextual information about the source or destination. + /// + protected RestClientException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/src/Spring/Spring.Http/Http/Rest/RestTemplate.cs b/src/Spring/Spring.Http/Http/Rest/RestTemplate.cs new file mode 100644 index 00000000..43c39872 --- /dev/null +++ b/src/Spring/Spring.Http/Http/Rest/RestTemplate.cs @@ -0,0 +1,553 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.IO; +using System.Net; +using System.Collections.Generic; + +using Spring.Util; +using Spring.Http; +using Spring.Http.Converters; +using Spring.Http.Converters.Xml; +using Spring.Http.Converters.Json; +using Spring.Http.Converters.Feed; +using Spring.Http.Rest.Support; +using UriTemplate = Spring.Util.UriTemplate; // UriTemplate in .NET Framework since 3.5 + +namespace Spring.Http.Rest +{ + /** + * The central class for client-side HTTP access. It simplifies communication with HTTP servers, and + * enforces RESTful principles. It handles HTTP connections, leaving application code to provide URLs (with possible + * template variables) and extract results. + * + *

The main entry points of this template are the methods named after the six main HTTP methods: + * + * + * + * + * + * + * + * + * + * + * + *
HTTP methodRestTemplate methods
DELETE{@link #delete}
GET{@link #getForObject}
{@link #getForEntity}
HEAD{@link #headForHeaders}
OPTIONS{@link #optionsForAllow}
POST{@link #postForLocation}
{@link #postForObject}
PUT{@link #put}
any{@link #exchange}
{@link #execute}
+ * + *

For each of these HTTP methods, there are three corresponding Java methods in the {@code RestTemplate}. Two + * variant take a {@code String} URI as first argument (eg. {@link #getForObject(String, Class, Object[])}, {@link + * #getForObject(String, Class, Map)}), and are capable of substituting any {@linkplain UriTemplate URI templates} in + * that URL using either a {@code String} variable arguments array, or a {@code Map}. The string varargs + * variant expands the given template variables in order, so that + *

+     * String result = restTemplate.getForObject("http://example.com/hotels/{hotel}/bookings/{booking}", String.class,"42",
+     * "21");
+     * 
+ * will perform a GET on {@code http://example.com/hotels/42/bookings/21}. The map variant expands the template based on + * variable name, and is therefore more useful when using many variables, or when a single variable is used multiple + * times. For example: + *
+     * Map<String, String> vars = Collections.singletonMap("hotel", "42");
+     * String result = restTemplate.getForObject("http://example.com/hotels/{hotel}/rooms/{hotel}", String.class, vars);
+     * 
+ * will perform a GET on {@code http://example.com/hotels/42/rooms/42}. Alternatively, there are {@link URI} variant + * methods ({@link #getForObject(URI, Class)}), which do not allow for URI templates, but allow you to reuse a single, + * expanded URI multiple times. + * + *

Furthermore, the {@code String}-argument methods assume that the URL String is unencoded. This means that + *

+     * restTemplate.getForObject("http://example.com/hotel list");
+     * 
+ * will perform a GET on {@code http://example.com/hotel%20list}. As a result, any URL passed that is already encoded + * will be encoded twice (i.e. {@code http://example.com/hotel%20list} will become {@code + * http://example.com/hotel%2520list}). If this behavior is undesirable, use the {@code URI}-argument methods, which + * will not perform any URL encoding. + * + *

Objects passed to and returned from these methods are converted to and from HTTP messages by {@link + * HttpMessageConverter} instances. Converters for the main mime types are registered by default, but you can also write + * your own converter and register it via the {@link #setMessageConverters messageConverters} bean property. + * + *

This template uses a {@link org.springframework.http.client.SimpleClientHttpRequestFactory} and a {@link + * DefaultResponseErrorHandler} as default strategies for creating HTTP connections or handling HTTP errors, + * respectively. These defaults can be overridden through the {@link #setRequestFactory(ClientHttpRequestFactory) + * requestFactory} and {@link #setErrorHandler(ResponseErrorHandler) errorHandler} bean properties. + * + * @author Arjen Poutsma + * @see HttpMessageConverter + * @see RequestCallback + * @see ResponseExtractor + * @see ResponseErrorHandler + * @since 3.0 + */ + public class RestTemplate : IRestOperations + { + #region Logging + + private static readonly Common.Logging.ILog LOG = Common.Logging.LogManager.GetLogger(typeof(RestTemplate)); + + #endregion + + #region Fields / Properties + + private Uri _baseAddress; + private bool _throwExceptionOnError; + private IList _messageConverters; + private IHttpWebRequestFactory _requestFactory; + + private IResponseExtractor headersExtractor; + + + public Uri BaseAddress + { + get + { + return this._baseAddress; + } + set + { + AssertUtils.ArgumentNotNull(value, "BaseAddress"); + if (!value.IsAbsoluteUri) + { + throw new ArgumentException(String.Format("'{0}' is not an absolute URI", value), "BaseAddress"); + } + this._baseAddress = value; + } + } + + public bool ThrowExceptionOnError + { + get { return _throwExceptionOnError; } + set { _throwExceptionOnError = value; } + } + + public IList MessageConverters + { + get { return this._messageConverters; } + set { this._messageConverters = value; } + } + + public IHttpWebRequestFactory RequestFactory + { + get { return this._requestFactory; } + set { this._requestFactory = value; } + } + + #endregion + + #region Constructor(s) + + public RestTemplate(Uri baseAddress) : + this() + { + this.BaseAddress = baseAddress; + } + + public RestTemplate(string baseAddress) : + this() + { + this.BaseAddress = new Uri(baseAddress, UriKind.Absolute); + } + + /** Create a new instance of the {@link RestTemplate} using default settings. */ + public RestTemplate() + { + this._throwExceptionOnError = true; + this.headersExtractor = new HeadersResponseExtractor(); + this._requestFactory = new DefaultHttpWebRequestFactory(); + + this._messageConverters = new List(); +#if NET_3_5 + //this._messageConverters.Add(new JsonHttpMessageConverter()); + this._messageConverters.Add(new Atom10FeedHttpMessageConverter()); + this._messageConverters.Add(new Rss20FeedHttpMessageConverter()); + this._messageConverters.Add(new XElementHttpMessageConverter()); +#endif + this._messageConverters.Add(new XmlDocumentHttpMessageConverter()); + //this._messageConverters.Add(new XmlSerializableHttpMessageConverter()); + this._messageConverters.Add(new StringHttpMessageConverter()); + this._messageConverters.Add(new ByteArrayHttpMessageConverter()); + } + + #endregion + + #region IRestOperations Membres + + public T GetForObject(string url, params string[] uriVariables) where T : class + { + AcceptHeaderRequestCallback requestCallback = new AcceptHeaderRequestCallback(HttpMethod.GET, typeof(T), this._messageConverters); + MessageConverterResponseExtractor responseExtractor = new MessageConverterResponseExtractor(this._messageConverters); + return this.Execute(url, requestCallback, responseExtractor, uriVariables); + } + + public T GetForObject(string url, IDictionary uriVariables) where T : class + { + AcceptHeaderRequestCallback requestCallback = new AcceptHeaderRequestCallback(HttpMethod.GET, typeof(T), this._messageConverters); + MessageConverterResponseExtractor responseExtractor = new MessageConverterResponseExtractor(this._messageConverters); + return this.Execute(url, requestCallback, responseExtractor, uriVariables); + } + + public T GetForObject(Uri url) where T : class + { + AcceptHeaderRequestCallback requestCallback = new AcceptHeaderRequestCallback(HttpMethod.GET, typeof(T), this._messageConverters); + MessageConverterResponseExtractor responseExtractor = new MessageConverterResponseExtractor(this._messageConverters); + return this.Execute(url, requestCallback, responseExtractor); + } + + public HttpResponseMessage GetForMessage(string url, params string[] uriVariables) where T : class + { + AcceptHeaderRequestCallback requestCallback = new AcceptHeaderRequestCallback(HttpMethod.GET, typeof(T), this._messageConverters); + HttpMessageResponseExtractor responseExtractor = new HttpMessageResponseExtractor(this._messageConverters); + return this.Execute>(url, requestCallback, responseExtractor, uriVariables); + } + + public HttpResponseMessage GetForMessage(string url, IDictionary uriVariables) where T : class + { + AcceptHeaderRequestCallback requestCallback = new AcceptHeaderRequestCallback(HttpMethod.GET, typeof(T), this._messageConverters); + HttpMessageResponseExtractor responseExtractor = new HttpMessageResponseExtractor(this._messageConverters); + return this.Execute>(url, requestCallback, responseExtractor, uriVariables); + } + + public HttpResponseMessage GetForMessage(Uri url) where T : class + { + AcceptHeaderRequestCallback requestCallback = new AcceptHeaderRequestCallback(HttpMethod.GET, typeof(T), this._messageConverters); + HttpMessageResponseExtractor responseExtractor = new HttpMessageResponseExtractor(this._messageConverters); + return this.Execute>(url, requestCallback, responseExtractor); + } + + public WebHeaderCollection HeadForHeaders(string url, params string[] uriVariables) + { + MethodRequestCallback requestCallback = new MethodRequestCallback(HttpMethod.HEAD); + return this.Execute(url, requestCallback, this.headersExtractor, uriVariables); + } + + public WebHeaderCollection HeadForHeaders(string url, IDictionary uriVariables) + { + MethodRequestCallback requestCallback = new MethodRequestCallback(HttpMethod.HEAD); + return this.Execute(url, requestCallback, this.headersExtractor, uriVariables); + } + + public WebHeaderCollection HeadForHeaders(Uri url) + { + MethodRequestCallback requestCallback = new MethodRequestCallback(HttpMethod.HEAD); + return this.Execute(url, requestCallback, this.headersExtractor); + } + + public Uri PostForLocation(string url, object request, params string[] uriVariables) + { + HttpMessageRequestCallback requestCallback = new HttpMessageRequestCallback( + HttpMethod.POST, request, this._messageConverters); + WebHeaderCollection headers = this.Execute( + url, requestCallback, this.headersExtractor, uriVariables); + string location = headers[HttpResponseHeader.Location]; + return StringUtils.HasText(location) ? new Uri(location) : null; + } + + public Uri PostForLocation(string url, object request, IDictionary uriVariables) + { + HttpMessageRequestCallback requestCallback = new HttpMessageRequestCallback( + HttpMethod.POST, request, this._messageConverters); + WebHeaderCollection headers = this.Execute( + url, requestCallback, this.headersExtractor, uriVariables); + string location = headers[HttpResponseHeader.Location]; + return StringUtils.HasText(location) ? new Uri(location) : null; + } + + public Uri PostForLocation(Uri url, object request) + { + HttpMessageRequestCallback requestCallback = new HttpMessageRequestCallback( + HttpMethod.POST, request, this._messageConverters); + WebHeaderCollection headers = this.Execute( + url, requestCallback, this.headersExtractor); + string location = headers[HttpResponseHeader.Location]; + return StringUtils.HasText(location) ? new Uri(location) : null; + } + + public T PostForObject(string url, object request, params string[] uriVariables) where T : class + { + HttpMessageRequestCallback requestCallback = new HttpMessageRequestCallback( + HttpMethod.POST, request, typeof(T), this._messageConverters); + MessageConverterResponseExtractor responseExtractor = new MessageConverterResponseExtractor(this._messageConverters); + return this.Execute(url, requestCallback, responseExtractor, uriVariables); + } + + public T PostForObject(string url, object request, IDictionary uriVariables) where T : class + { + HttpMessageRequestCallback requestCallback = new HttpMessageRequestCallback( + HttpMethod.POST, request, typeof(T), this._messageConverters); + MessageConverterResponseExtractor responseExtractor = new MessageConverterResponseExtractor(this._messageConverters); + return this.Execute(url, requestCallback, responseExtractor, uriVariables); + } + + public T PostForObject(Uri url, object request) where T : class + { + HttpMessageRequestCallback requestCallback = new HttpMessageRequestCallback( + HttpMethod.POST, request, typeof(T), this._messageConverters); + MessageConverterResponseExtractor responseExtractor = new MessageConverterResponseExtractor(this._messageConverters); + return this.Execute(url, requestCallback, responseExtractor); + } + + public HttpResponseMessage PostForMessage(string url, object request, params string[] uriVariables) where T : class + { + HttpMessageRequestCallback requestCallback = new HttpMessageRequestCallback( + HttpMethod.POST, request, typeof(T), this._messageConverters); + HttpMessageResponseExtractor responseExtractor = new HttpMessageResponseExtractor(this._messageConverters); + return this.Execute>(url, requestCallback, responseExtractor, uriVariables); + } + + public HttpResponseMessage PostForMessage(string url, object request, IDictionary uriVariables) where T : class + { + HttpMessageRequestCallback requestCallback = new HttpMessageRequestCallback( + HttpMethod.POST, request, typeof(T), this._messageConverters); + HttpMessageResponseExtractor responseExtractor = new HttpMessageResponseExtractor(this._messageConverters); + return this.Execute>(url, requestCallback, responseExtractor, uriVariables); + } + + public HttpResponseMessage PostForMessage(Uri url, object request) where T : class + { + HttpMessageRequestCallback requestCallback = new HttpMessageRequestCallback( + HttpMethod.POST, request, typeof(T), this._messageConverters); + HttpMessageResponseExtractor responseExtractor = new HttpMessageResponseExtractor(this._messageConverters); + return this.Execute>(url, requestCallback, responseExtractor); + } + + public void Put(string url, object request, params string[] uriVariables) + { + HttpMessageRequestCallback requestCallback = new HttpMessageRequestCallback( + HttpMethod.PUT, request, this._messageConverters); + this.Execute(url, requestCallback, null, uriVariables); + } + + public void Put(string url, object request, IDictionary uriVariables) + { + HttpMessageRequestCallback requestCallback = new HttpMessageRequestCallback( + HttpMethod.PUT, request, this._messageConverters); + this.Execute(url, requestCallback, null, uriVariables); + } + + public void Put(Uri url, object request) + { + HttpMessageRequestCallback requestCallback = new HttpMessageRequestCallback( + HttpMethod.PUT, request, this._messageConverters); + this.Execute(url, requestCallback, null); + } + + public void Delete(string url, params string[] uriVariables) + { + MethodRequestCallback requestCallback = new MethodRequestCallback(HttpMethod.DELETE); + this.Execute(url, requestCallback, null, uriVariables); + } + + public void Delete(string url, IDictionary uriVariables) + { + MethodRequestCallback requestCallback = new MethodRequestCallback(HttpMethod.DELETE); + this.Execute(url, requestCallback, null, uriVariables); + } + + public void Delete(Uri url) + { + MethodRequestCallback requestCallback = new MethodRequestCallback(HttpMethod.DELETE); + this.Execute(url, requestCallback, null); + } + + public IList OptionsForAllow(string url, params string[] uriVariables) + { + MethodRequestCallback requestCallback = new MethodRequestCallback(HttpMethod.OPTIONS); + WebHeaderCollection headers = this.Execute( + url, requestCallback, this.headersExtractor, uriVariables); + string allow = headers[HttpResponseHeader.Allow]; + + return ParseAllowHeader(allow); + } + + public IList OptionsForAllow(string url, IDictionary uriVariables) + { + MethodRequestCallback requestCallback = new MethodRequestCallback(HttpMethod.OPTIONS); + WebHeaderCollection headers = this.Execute(url, requestCallback, this.headersExtractor, uriVariables); + string allow = headers[HttpResponseHeader.Allow]; + + return ParseAllowHeader(allow); + } + + public IList OptionsForAllow(Uri url) + { + MethodRequestCallback requestCallback = new MethodRequestCallback(HttpMethod.OPTIONS); + WebHeaderCollection headers = this.Execute(url, requestCallback, this.headersExtractor); + string allow = headers[HttpResponseHeader.Allow]; + + return ParseAllowHeader(allow); + } + + public HttpResponseMessage Exchange(string url, HttpRequestMessage requestMessage, params string[] uriVariables) where T : class + { + HttpMessageRequestCallback requestCallback = new HttpMessageRequestCallback(requestMessage, typeof(T), this._messageConverters); + HttpMessageResponseExtractor responseExtractor = new HttpMessageResponseExtractor(this._messageConverters); + return this.Execute>(url, requestCallback, responseExtractor, uriVariables); + } + + public HttpResponseMessage Exchange(string url, HttpRequestMessage requestMessage, IDictionary uriVariables) where T : class + { + HttpMessageRequestCallback requestCallback = new HttpMessageRequestCallback(requestMessage, typeof(T), this._messageConverters); + HttpMessageResponseExtractor responseExtractor = new HttpMessageResponseExtractor(this._messageConverters); + return this.Execute>(url, requestCallback, responseExtractor, uriVariables); + } + + public HttpResponseMessage Exchange(Uri url, HttpRequestMessage requestMessage) where T : class + { + HttpMessageRequestCallback requestCallback = new HttpMessageRequestCallback(requestMessage, typeof(T), this._messageConverters); + HttpMessageResponseExtractor responseExtractor = new HttpMessageResponseExtractor(this._messageConverters); + return this.Execute>(url, requestCallback, responseExtractor); + } + + public T Execute(string url, IRequestCallback requestCallback, IResponseExtractor responseExtractor, params string[] uriVariables) where T : class + { + UriTemplate uriTemplate = new UriTemplate(url); + Uri uri = uriTemplate.Expand(uriVariables); + return this.DoExecute(uri, requestCallback, responseExtractor); + } + + public T Execute(string url, IRequestCallback requestCallback, IResponseExtractor responseExtractor, IDictionary uriVariables) where T : class + { + UriTemplate uriTemplate = new UriTemplate(url); + Uri uri = uriTemplate.Expand(uriVariables); + return this.DoExecute(uri, requestCallback, responseExtractor); + } + + public T Execute(Uri url, IRequestCallback requestCallback, IResponseExtractor responseExtractor) where T : class + { + return this.DoExecute(url, requestCallback, responseExtractor); + } + + #endregion + + /** + * Execute the given method on the provided URI. The {@link ClientHttpRequest} is processed using the {@link + * RequestCallback}; the response with the {@link ResponseExtractor}. + * @param url the fully-expanded URL to connect to + * @param method the HTTP method to execute (GET, POST, etc.) + * @param requestCallback object that prepares the request (can be null) + * @param responseExtractor object that extracts the return value from the response (can be null) + * @return an arbitrary object, as returned by the {@link ResponseExtractor} + */ + protected virtual T DoExecute(Uri url, IRequestCallback requestCallback, IResponseExtractor responseExtractor) where T : class + { + HttpWebRequest request; + HttpWebResponse response = null; + + Uri finalUri = url; + if (!url.IsAbsoluteUri) + { + if (this._baseAddress != null) + { + finalUri = new Uri(this._baseAddress, url); + } + else + { + throw new ArgumentException(String.Format("'{0}' is not an absolute URI", url), "url"); + } + } + + // Create and initialize the web request + request = this._requestFactory.CreateRequest(finalUri); + + if (requestCallback != null) + { + requestCallback.DoWithRequest(request); + } + + try + { + // Get response + response = request.GetResponse() as HttpWebResponse; + + if (request.HaveResponse == true && response != null) + { + #region Instrumentation + + if (LOG.IsDebugEnabled) + { + LOG.Debug(String.Format( + "Request for '{0}' resulted in {1:d} - {1} ({2})", + finalUri, response.StatusCode, response.StatusDescription)); + } + + #endregion + + if (responseExtractor != null) + { + return responseExtractor.ExtractData(response); + } + } + } + catch (WebException ex) + { + // This exception will be raised if the server didn't return 200 - OK + // Try to retrieve more information about the network error + if (ex.Response != null) + { + using (HttpWebResponse errorResponse = (HttpWebResponse)ex.Response) + { + if (this._throwExceptionOnError) + { + throw new RestClientException(String.Format( + "The server returned '{0}' with the status code {1:d} - {1}.", + errorResponse.StatusDescription, errorResponse.StatusCode), + ex); + } + else + { + if (responseExtractor != null) + { + return responseExtractor.ExtractData(errorResponse); + } + } + } + } + } + finally + { + if (response != null) + { + response.Close(); + } + } + + return null; + } + + private static IList ParseAllowHeader(string allow) + { + IList methods = new List(); + + if (StringUtils.HasText(allow)) + { + string[] methodsArray = allow.Split(','); + + foreach (string method in methodsArray) + { + methods.Add((HttpMethod)Enum.Parse(typeof(HttpMethod), method.Trim(), true)); + } + } + + return methods; + } + } +} diff --git a/src/Spring/Spring.Http/Http/Rest/Support/AcceptHeaderRequestCallback.cs b/src/Spring/Spring.Http/Http/Rest/Support/AcceptHeaderRequestCallback.cs new file mode 100644 index 00000000..afd08cb7 --- /dev/null +++ b/src/Spring/Spring.Http/Http/Rest/Support/AcceptHeaderRequestCallback.cs @@ -0,0 +1,96 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Net; +using System.Collections.Generic; + +using Spring.Http; +using Spring.Http.Converters; + +namespace Spring.Http.Rest.Support +{ + /** + * Request callback implementation that prepares the request's accept headers. + */ + public class AcceptHeaderRequestCallback : MethodRequestCallback + { + #region Logging + + private static readonly Common.Logging.ILog LOG = Common.Logging.LogManager.GetLogger(typeof(AcceptHeaderRequestCallback)); + + #endregion + + protected Type responseType; + protected IList messageConverters; + + public AcceptHeaderRequestCallback(HttpMethod method, Type responseType, IList messageConverters) : + base(method) + { + this.responseType = responseType; + this.messageConverters = messageConverters; + } + + public override void DoWithRequest(HttpWebRequest request) + { + base.DoWithRequest(request); + + if (responseType != null) + { + List allSupportedMediaTypes = new List(); + foreach (IHttpMessageConverter messageConverter in this.messageConverters) + { + if (messageConverter.CanRead(responseType, null)) + { + foreach (MediaType supportedMediaType in messageConverter.SupportedMediaTypes) + { + if (!String.IsNullOrEmpty(supportedMediaType.CharSet)) + { + allSupportedMediaTypes.Add(new MediaType( + supportedMediaType.Type, supportedMediaType.Subtype)); + } + else + { + allSupportedMediaTypes.Add(supportedMediaType); + } + } + } + } + if (allSupportedMediaTypes.Count > 0) + { + MediaType.SortBySpecificity(allSupportedMediaTypes); + + #region Instrumentation + + if (LOG.IsDebugEnabled) + { + LOG.Debug(String.Format( + "Setting request Accept header to '{0}'", + MediaType.ToString(allSupportedMediaTypes))); + } + + #endregion + + request.Accept = MediaType.ToString(allSupportedMediaTypes); + } + } + } + } +} diff --git a/src/Spring/Spring.Http/Http/Rest/Support/HeadersResponseExtractor.cs b/src/Spring/Spring.Http/Http/Rest/Support/HeadersResponseExtractor.cs new file mode 100644 index 00000000..121e54e1 --- /dev/null +++ b/src/Spring/Spring.Http/Http/Rest/Support/HeadersResponseExtractor.cs @@ -0,0 +1,35 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System.Net; + +namespace Spring.Http.Rest.Support +{ + /** + * Response extractor that extracts the response {@link HttpHeaders}. + */ + public class HeadersResponseExtractor : IResponseExtractor + { + public WebHeaderCollection ExtractData(HttpWebResponse response) + { + return response.Headers; + } + } +} diff --git a/src/Spring/Spring.Http/Http/Rest/Support/HttpMessageRequestCallback.cs b/src/Spring/Spring.Http/Http/Rest/Support/HttpMessageRequestCallback.cs new file mode 100644 index 00000000..f9f34893 --- /dev/null +++ b/src/Spring/Spring.Http/Http/Rest/Support/HttpMessageRequestCallback.cs @@ -0,0 +1,132 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Net; +using System.Collections.Generic; + +using Spring.Util; +using Spring.Http; +using Spring.Http.Converters; + +namespace Spring.Http.Rest.Support +{ + /** + * Request callback implementation that writes the given object to the request stream. + */ + public class HttpMessageRequestCallback : AcceptHeaderRequestCallback + { + #region Logging + + private static readonly Common.Logging.ILog LOG = Common.Logging.LogManager.GetLogger(typeof(HttpMessageRequestCallback)); + + #endregion + + private HttpRequestMessage requestMessage; + + public HttpMessageRequestCallback(HttpMethod method, object requestBody, IList messageConverters) : + this(method, requestBody, null, messageConverters) + { + } + + public HttpMessageRequestCallback(HttpMethod method, object requestBody, Type responseType, IList messageConverters) : + base(method, responseType, messageConverters) + { + if (requestBody is HttpRequestMessage) + { + this.requestMessage = (HttpRequestMessage)requestBody; + this.requestMessage.Method = method; + } + else + { + this.requestMessage = new HttpRequestMessage(requestBody, method); + } + } + + public HttpMessageRequestCallback(HttpRequestMessage requestMessage, IList messageConverters) : + this(requestMessage, null, messageConverters) + { + } + + public HttpMessageRequestCallback(HttpRequestMessage requestMessage, Type responseType, IList messageConverters) : + base(requestMessage.Method, responseType, messageConverters) + { + this.requestMessage = requestMessage; + } + + public override void DoWithRequest(HttpWebRequest request) + { + base.DoWithRequest(request); + + // headers + if (requestMessage.Headers.Count > 0) + { + request.Headers.Add(requestMessage.Headers); + } + + // body + if (requestMessage.Body != null) + { + object requestBody = requestMessage.Body; + MediaType requestContentType = null; + if (StringUtils.HasText(requestMessage.Headers[HttpRequestHeader.ContentType])) + { + requestContentType = MediaType.ParseMediaType(requestMessage.Headers[HttpRequestHeader.ContentType]); + } + foreach (IHttpMessageConverter messageConverter in base.messageConverters) + { + if (messageConverter.CanWrite(requestBody.GetType(), requestContentType)) + { + #region Instrumentation + + if (LOG.IsDebugEnabled) + { + if (requestContentType != null) + { + LOG.Debug(String.Format( + "Writing [{0}] as '{1}' using [{2}]", + requestBody, requestContentType, messageConverter)); + } + else + { + LOG.Debug(String.Format( + "Writing [{0}] using [{1}]", + requestBody, messageConverter)); + } + } + + #endregion + + messageConverter.Write(requestBody, requestContentType, request); + return; + } + } + string message = String.Format( + "Could not write request: no suitable IHttpMessageConverter found for request type [{0}]", + requestBody.GetType().FullName); + if (requestContentType != null) + { + message = String.Format("{0} and content type [{1}]", message, requestContentType); + } + throw new RestClientException(message); + } + } + } +} diff --git a/src/Spring/Spring.Http/Http/Rest/Support/HttpMessageResponseExtractor.cs b/src/Spring/Spring.Http/Http/Rest/Support/HttpMessageResponseExtractor.cs new file mode 100644 index 00000000..e7b2f993 --- /dev/null +++ b/src/Spring/Spring.Http/Http/Rest/Support/HttpMessageResponseExtractor.cs @@ -0,0 +1,55 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System.Net; +using System.Collections.Generic; + +using Spring.Util; +using Spring.Http; +using Spring.Http.Converters; + +namespace Spring.Http.Rest.Support +{ + /** + * Response extractor for {@link HttpEntity}. + */ + public class HttpMessageResponseExtractor : IResponseExtractor> where T : class + { + private MessageConverterResponseExtractor httpMessageConverterExtractor; + + public HttpMessageResponseExtractor(IList messageConverters) + { + httpMessageConverterExtractor = new MessageConverterResponseExtractor(messageConverters); + } + + public HttpResponseMessage ExtractData(HttpWebResponse response) + { + if (StringUtils.HasText(response.Headers[HttpResponseHeader.ContentType])) + { + T body = httpMessageConverterExtractor.ExtractData(response); + return new HttpResponseMessage(body, response.Headers, response.StatusCode, response.StatusDescription); + } + else + { + return new HttpResponseMessage(response.Headers, response.StatusCode, response.StatusDescription); + } + } + } +} diff --git a/src/Spring/Spring.Http/Http/Rest/Support/MessageConverterResponseExtractor.cs b/src/Spring/Spring.Http/Http/Rest/Support/MessageConverterResponseExtractor.cs new file mode 100644 index 00000000..13d29b31 --- /dev/null +++ b/src/Spring/Spring.Http/Http/Rest/Support/MessageConverterResponseExtractor.cs @@ -0,0 +1,88 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Net; +using System.Collections.Generic; + +using Spring.Util; +using Spring.Http; +using Spring.Http.Converters; + +namespace Spring.Http.Rest.Support +{ + /** + * Response extractor that uses the given {@linkplain HttpMessageConverter entity converters} to convert the response + * into a type T. + * + * @author Arjen Poutsma + * @see RestTemplate + * @since 3.0 + */ + public class MessageConverterResponseExtractor : IResponseExtractor where T : class + { + #region Logging + + private static readonly Common.Logging.ILog LOG = Common.Logging.LogManager.GetLogger(typeof(MessageConverterResponseExtractor)); + + #endregion + + private IList messageConverters; + + /** + * Creates a new instance of the {@code HttpMessageConverterExtractor} with the given response type and message + * converters. The given converters must support the response type. + */ + public MessageConverterResponseExtractor(IList messageConverters) + { + this.messageConverters = messageConverters; + } + + public T ExtractData(HttpWebResponse response) + { + if (!StringUtils.HasText(response.Headers[HttpResponseHeader.ContentType])) + { + throw new RestClientException("Could not extract response: no Content-Type found"); + } + MediaType contentType = MediaType.ParseMediaType(response.Headers[HttpResponseHeader.ContentType]); + foreach(IHttpMessageConverter messageConverter in messageConverters) + { + if (messageConverter.CanRead(typeof(T), contentType)) + { + #region Instrumentation + + if (LOG.IsDebugEnabled) + { + LOG.Debug(String.Format( + "Reading [{0}] as '{1}' using [{2}]", + typeof(T).FullName, contentType, messageConverter)); + } + + #endregion + + return messageConverter.Read(response); + } + } + throw new RestClientException(String.Format( + "Could not extract response: no suitable HttpMessageConverter found for response type [{0}] and content type [{1}]", + typeof(T).FullName, contentType)); + } + } +} diff --git a/src/Spring/Spring.Http/Http/Rest/Support/MethodRequestCallback.cs b/src/Spring/Spring.Http/Http/Rest/Support/MethodRequestCallback.cs new file mode 100644 index 00000000..bb140af0 --- /dev/null +++ b/src/Spring/Spring.Http/Http/Rest/Support/MethodRequestCallback.cs @@ -0,0 +1,60 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Net; + +using Spring.Http; + +namespace Spring.Http.Rest.Support +{ + /** + * Request callback implementation that sets the Http method. + */ + public class MethodRequestCallback : IRequestCallback + { + #region Logging + + private static readonly Common.Logging.ILog LOG = Common.Logging.LogManager.GetLogger(typeof(MethodRequestCallback)); + + #endregion + + protected HttpMethod method; + + public MethodRequestCallback(HttpMethod method) + { + this.method = method; + } + + public virtual void DoWithRequest(HttpWebRequest request) + { + #region Instrumentation + + if (LOG.IsDebugEnabled) + { + LOG.Debug(String.Format("Setting request Method to '{0}'", this.method)); + } + + #endregion + + request.Method = this.method.ToString(); + } + } +} diff --git a/src/Spring/Spring.Http/Spring.Http.2008.csproj b/src/Spring/Spring.Http/Spring.Http.2008.csproj new file mode 100644 index 00000000..00af2400 --- /dev/null +++ b/src/Spring/Spring.Http/Spring.Http.2008.csproj @@ -0,0 +1,146 @@ + + + Local + 9.0.30729 + 2.0 + {FAC04F79-4B1F-1D13-B30F-00EE04EC0FBC} + Debug + AnyCPU + + + + + Spring.Http + + + JScript + Grid + IE50 + false + Library + Spring + OnBuildSuccess + + + false + v3.5 + + + + + ..\..\..\build\VS.Net.2008\Spring.Http\Debug\ + false + 285212672 + false + + + TRACE;DEBUG;NET_2_0;NET_3_0;NET_3_5 + Spring.Http.xml + true + 4096 + false + + + false + false + false + false + 4 + full + prompt + + + ..\..\..\build\VS.Net.2008\Spring.Http\Release\ + false + 285212672 + false + + + TRACE;NET_2_0;NET_3_0;NET_3_5 + + + false + 4096 + false + + + true + false + false + false + 4 + none + prompt + + + + False + ..\..\..\lib\Net\2.0\Common.Logging.dll + + + System + + + + 3.0 + + + 3.5 + + + + 3.5 + + + + + CommonAssemblyInfo.cs + Code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {710961A3-0DF4-49E4-A26E-F5B9C044AC84} + Spring.Core.2008 + + + + + + + + + + \ No newline at end of file diff --git a/src/Spring/Spring.Http/Spring.Http.2010.csproj b/src/Spring/Spring.Http/Spring.Http.2010.csproj new file mode 100644 index 00000000..c6da53bd --- /dev/null +++ b/src/Spring/Spring.Http/Spring.Http.2010.csproj @@ -0,0 +1,184 @@ + + + + Local + 9.0.30729 + 2.0 + {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Debug + AnyCPU + + + + + Spring.Http + + + JScript + Grid + IE50 + false + Library + Spring + OnBuildSuccess + + + false + v3.5 + + + 3.5 + + + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + + ..\..\..\build\VS.Net.2010\Spring.Http\Debug\ + false + 285212672 + false + + + TRACE;DEBUG;NET_2_0;NET_3_0;NET_3_5 + Spring.Http.xml + true + 4096 + false + + + false + false + false + false + 4 + full + prompt + + + ..\..\..\build\VS.Net.2010\Spring.Http\Release\ + false + 285212672 + false + + + TRACE;NET_2_0;NET_3_0;NET_3_5 + + + false + 4096 + false + + + true + false + false + false + 4 + none + prompt + + + + False + ..\..\..\lib\Net\2.0\Common.Logging.dll + + + System + + + + + + + + + + CommonAssemblyInfo.cs + Code + + + + + + Code + + + Code + + + + + + Code + + + + Code + + + + + + + + + + + + + + + + + + + + + + + + + + {710961A3-0DF4-49E4-A26E-F5B9C044AC84} + Spring.Core.2010 + + + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 3.5 SP1 + true + + + False + Windows Installer 3.1 + true + + + + + + + + + + \ No newline at end of file diff --git a/src/Spring/Spring.Http/Util/UriTemplate.cs b/src/Spring/Spring.Http/Util/UriTemplate.cs new file mode 100644 index 00000000..b9d51f12 --- /dev/null +++ b/src/Spring/Spring.Http/Util/UriTemplate.cs @@ -0,0 +1,239 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Text; +using System.Text.RegularExpressions; +using System.Collections.Generic; + +namespace Spring.Util +{ + // TODO : Check .NET 3.5 class + // TODO : Back to original Java behavior Expand(params string[]) method ? + + /** + * Represents a URI template. An URI template is a URI-like String that contained variables marked of in braces + * ({, }), which can be expanded to produce a URI.

See {@link #expand(Map)}, + * {@link #expand(Object[])}, and {@link #match(String)} for example usages. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 3.0 + * @see URI Templates + */ + public class UriTemplate + { + /** Captures URI template variable names. */ + private static Regex VARIABLENAMES_REGEX = new Regex(@"\{([^/]+?)\}", RegexOptions.Compiled); + //private static Regex VARIABLENAMES_REGEX = new Regex(@"\{[^{}]+\}", RegexOptions.Compiled); + + /** Replaces template variables in the URI template. */ + private static string VARIABLEVALUE_PATTERN = "(?<{0}>.*)"; + + private const string BRACE_LEFT = "{"; + private const string BRACE_RIGHT = "}"; + + private string uriTemplate; + private string[] variableNames; + private Regex matchRegex; + + public string[] VariableNames + { + get { return this.variableNames; } + } + + public UriTemplate(string uriTemplate) + { + this.uriTemplate = uriTemplate; + Parser parser = new Parser(uriTemplate); + this.variableNames = parser.GetVariableNames(); + this.matchRegex = parser.GetMatchRegex(); + } + + public Uri Expand(IDictionary uriVariables) + { + if (uriVariables.Count != this.variableNames.Length) + { + throw new ArgumentException(String.Format( + "Invalid amount of variables values in '{0}': expected {1}; got {2}", + this.uriTemplate, this.variableNames.Length, uriVariables.Count)); + } + + string uri = this.uriTemplate; + foreach (string variableName in this.variableNames) + { + if (!uriVariables.ContainsKey(variableName)) + { + throw new ArgumentException(String.Format( + "'uriVariables' dictionary has no value for '{0}'", + variableName)); + } + uri = Replace(uri, variableName, uriVariables[variableName]); + } + + return new Uri(uri, UriKind.RelativeOrAbsolute); + + //string[] uriVariableValues = new String[this.variableNames.Length]; + //for (int i = 0; i < this.variableNames.Length; i++) + //{ + // string variableName = this.variableNames[i]; + // if (!uriVariables.ContainsKey(variableName)) + // { + // throw new ArgumentException(String.Format( + // "'uriVariables' dictionary has no value for '{0}'", + // variableName)); + // } + // uriVariableValues[i] = uriVariables[variableName]; + //} + //return Expand(uriVariableValues); + } + + public Uri Expand(params string[] uriVariableValues) + { + if (uriVariableValues.Length != this.variableNames.Length) + { + throw new ArgumentException(String.Format( + "Invalid amount of variables values in '{0}': expected {1}; got {2}", + this.uriTemplate, this.variableNames.Length, uriVariableValues.Length)); + } + + string uri = this.uriTemplate; + for (int i = 0; i < this.variableNames.Length; i++) + { + uri = Replace(uri, this.variableNames[i], uriVariableValues[i]); + } + + return new Uri(uri, UriKind.RelativeOrAbsolute); + } + + /** + * Indicate whether the given URI matches this template. + * @param uri the URI to match to + * @return true if it matches; false otherwise + */ + public bool Matches(string uri) + { + if (uri == null) + { + return false; + } + return this.matchRegex.IsMatch(uri); + } + + /** + * Match the given URI to a map of variable values. Keys in the returned map are variable names, values are variable + * values, as occurred in the given URI.

Example:

 UriTemplate template = new
+         * UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); System.out.println(template.match("http://example.com/hotels/1/bookings/42"));
+         * 
will print:
{hotel=1, booking=42}
+ * @param uri the URI to match to + * @return a map of variable values + */ + public IDictionary Match(string uri) + { + AssertUtils.ArgumentNotNull(uri, "uri"); + + IDictionary result = new Dictionary(); + Match match = this.matchRegex.Match(uri); + for (int i = 1; i < match.Groups.Count; i++ ) + { + result.Add(this.matchRegex.GroupNameFromNumber(i), match.Groups[i].Value); + } + return result; + } + + public override string ToString() + { + return this.uriTemplate; + } + + //private static string[] GetVariableNames(string uriTemplate) + //{ + // List variableNames = new List(); + // foreach (Match match in VARIABLENAMES_REGEX.Matches(uriTemplate)) + // { + // string token = match.Value; + // token = token.Substring(1, token.Length - 2); + + // if (!variableNames.Contains(token)) + // { + // variableNames.Add(token); + // } + // } + + // return variableNames.ToArray(); + //} + + private static string Replace(string uriTemplate, string token, string value) + { + string quotedToken = BRACE_LEFT + token + BRACE_RIGHT; + return uriTemplate.Replace(quotedToken, value); + } + + /** + * Static inner class to parse uri template strings into a matching regular expression. + */ + private class Parser + { + private List variableNames = new List(); + private StringBuilder patternBuilder = new StringBuilder(); + + public Parser(string uriTemplate) + { + AssertUtils.ArgumentHasText(uriTemplate, "'uriTemplate' must not be null"); + + int index = 0; + this.patternBuilder.Append("^"); + foreach (Match match in VARIABLENAMES_REGEX.Matches(uriTemplate)) + { + string variableName = match.Groups[1].Value; + if (!variableNames.Contains(variableName)) + { + variableNames.Add(variableName); + } + + this.patternBuilder.Append(Escape(uriTemplate, index, match.Index - index)); + this.patternBuilder.Append(String.Format(VARIABLEVALUE_PATTERN, variableName)); + index = match.Index + match.Length; + } + this.patternBuilder.Append(Escape(uriTemplate, index, uriTemplate.Length - index)); + this.patternBuilder.Append("$"); + } + + private static string Escape(String fullPath, int start, int end) + { + if (start == end) + { + return ""; + } + return Regex.Escape(fullPath.Substring(start, end)); + } + + public string[] GetVariableNames() + { + return this.variableNames.ToArray(); + } + + public Regex GetMatchRegex() + { + return new Regex(this.patternBuilder.ToString(), RegexOptions.Compiled); + } + } + } +} diff --git a/test/Spring/Spring.Http.Tests/AssemblyInfo.cs b/test/Spring/Spring.Http.Tests/AssemblyInfo.cs new file mode 100644 index 00000000..3ebb9609 --- /dev/null +++ b/test/Spring/Spring.Http.Tests/AssemblyInfo.cs @@ -0,0 +1,25 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: AssemblyTitle("Spring.Http Tests")] +[assembly: AssemblyDescription("Unit tests for Spring.Http assembly")] diff --git a/test/Spring/Spring.Http.Tests/Http/Converters/ByteArrayHttpMessageConverterTests.cs b/test/Spring/Spring.Http.Tests/Http/Converters/ByteArrayHttpMessageConverterTests.cs new file mode 100644 index 00000000..08db23ce --- /dev/null +++ b/test/Spring/Spring.Http.Tests/Http/Converters/ByteArrayHttpMessageConverterTests.cs @@ -0,0 +1,99 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.IO; +using System.Net; + +using NUnit.Framework; +using Rhino.Mocks; + +namespace Spring.Http.Converters +{ + /// + /// Unit tests for the ByteArrayHttpMessageConverter class. + /// + /// Arjen Poutsma + /// Bruno Baia (.NET) + [TestFixture] + public class ByteArrayHttpMessageConverterTests + { + private ByteArrayHttpMessageConverter converter; + private MockRepository mocks; + + [SetUp] + public void SetUp() + { + mocks = new MockRepository(); + converter = new ByteArrayHttpMessageConverter(); + } + + [Test] + public void CanRead() + { + Assert.IsTrue(converter.CanRead(typeof(byte[]), new MediaType("application", "octet-stream"))); + } + + [Test] + public void CanWrite() + { + Assert.IsTrue(converter.CanWrite(typeof(byte[]), new MediaType("application", "octet-stream"))); + Assert.IsTrue(converter.CanWrite(typeof(byte[]), MediaType.ALL)); + } + + [Test] + public void Read() + { + byte[] body = new byte[] { 0x1, 0x2 }; + + HttpWebResponse webResponse = mocks.CreateMock(); + Expect.Call(webResponse.GetResponseStream()).Return(new MemoryStream(body)).Repeat.Once(); + Expect.Call(webResponse.ContentLength).Return(2).Repeat.Once(); + + mocks.ReplayAll(); + + byte[] result = converter.Read(webResponse); + Assert.AreEqual(body.Length, result.Length, "Invalid result"); + Assert.AreEqual(body[0], result[0], "Invalid result"); + Assert.AreEqual(body[1], result[1], "Invalid result"); + + mocks.VerifyAll(); + } + + [Test] + public void Write() + { + byte[] body = new byte[] { 0x1, 0x2 }; + + HttpWebRequest webRequest = WebRequest.Create("http://localhost") as HttpWebRequest; + webRequest.Method = "POST"; + + converter.Write(body, null, webRequest); + + using (Stream postStream = webRequest.GetRequestStream()) + { + //Assert.AreEqual(body.Length, postStream.Length, "Invalid result"); + } + + Assert.AreEqual(new MediaType("application", "octet-stream"), MediaType.ParseMediaType(webRequest.ContentType), "Invalid content-type"); + Assert.AreEqual(2, webRequest.ContentLength, "Invalid content-length"); + } + } +} diff --git a/test/Spring/Spring.Http.Tests/Http/Converters/StringHttpMessageConverterTests.cs b/test/Spring/Spring.Http.Tests/Http/Converters/StringHttpMessageConverterTests.cs new file mode 100644 index 00000000..eeb2a099 --- /dev/null +++ b/test/Spring/Spring.Http.Tests/Http/Converters/StringHttpMessageConverterTests.cs @@ -0,0 +1,126 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.IO; +using System.Net; +using System.Text; + +using NUnit.Framework; +using Rhino.Mocks; + +namespace Spring.Http.Converters +{ + /// + /// Unit tests for the StringHttpMessageConverter class. + /// + /// Arjen Poutsma + /// Bruno Baia (.NET) + [TestFixture] + public class StringHttpMessageConverterTests + { + private StringHttpMessageConverter converter; + private MockRepository mocks; + + [SetUp] + public void SetUp() + { + mocks = new MockRepository(); + converter = new StringHttpMessageConverter(); + } + + [Test] + public void CanRead() + { + Assert.IsTrue(converter.CanRead(typeof(string), new MediaType("text", "plain"))); + } + + [Test] + public void CanWrite() + { + Assert.IsTrue(converter.CanWrite(typeof(string), new MediaType("text", "plain"))); + Assert.IsTrue(converter.CanWrite(typeof(string), MediaType.ALL)); + } + + [Test] + public void Read() + { + string body = "Hello Bruno Baïa"; + + HttpWebResponse webResponse = mocks.CreateMock(); + Expect.Call(webResponse.GetResponseStream()).Return(new MemoryStream(Encoding.UTF8.GetBytes(body))).Repeat.Once(); + Expect.Call(webResponse.CharacterSet).Return("utf-8").Repeat.Twice(); + + mocks.ReplayAll(); + + string result = converter.Read(webResponse); + Assert.AreEqual(body, result, "Invalid result"); + + mocks.VerifyAll(); + } + + [Test] + public void WriteDefaultCharset() + { + string body = "H\u00e9llo W\u00f6rld"; + + string charSet = "ISO-8859-1"; + Encoding charSetEncoding = Encoding.GetEncoding(charSet); + + HttpWebRequest webRequest = WebRequest.Create("http://localhost") as HttpWebRequest; + webRequest.Method = "POST"; + + converter.Write(body, null, webRequest); + + using (Stream postStream = webRequest.GetRequestStream()) + { + //Assert.AreEqual(body.Length, postStream.Length, "Invalid result"); + } + + Assert.AreEqual(new MediaType("text", "plain", charSet), MediaType.ParseMediaType(webRequest.ContentType), "Invalid content-type"); + Assert.AreEqual(charSetEncoding.GetBytes(body).Length, webRequest.ContentLength, "Invalid content-length"); + //Assert.IsFalse(String.IsNullOrEmpty(webRequest.Headers[HttpRequestHeader.AcceptCharset]), "Invalid accept-charset"); + } + + [Test] + public void WriteUTF8() + { + string body = "H\u00e9llo W\u00f6rld"; + + string charSet = "UTF-8"; + Encoding charSetEncoding = Encoding.GetEncoding(charSet); + MediaType mediaType = new MediaType("text", "plain", charSet); + + HttpWebRequest webRequest = WebRequest.Create("http://localhost") as HttpWebRequest; + webRequest.Method = "POST"; + + converter.Write(body, mediaType, webRequest); + + using (Stream postStream = webRequest.GetRequestStream()) + { + //Assert.AreEqual(body.Length, postStream.Length, "Invalid result"); + } + + Assert.AreEqual(mediaType, MediaType.ParseMediaType(webRequest.ContentType), "Invalid content-type"); + Assert.AreEqual(charSetEncoding.GetBytes(body).Length, webRequest.ContentLength, "Invalid content-length"); + //Assert.IsFalse(String.IsNullOrEmpty(webRequest.Headers[HttpRequestHeader.AcceptCharset]), "Invalid accept-charset"); + } + } +} diff --git a/test/Spring/Spring.Http.Tests/Http/Converters/Xml/DataContractHttpMessageConverterIntegrationTests.cs b/test/Spring/Spring.Http.Tests/Http/Converters/Xml/DataContractHttpMessageConverterIntegrationTests.cs new file mode 100644 index 00000000..eaac66de --- /dev/null +++ b/test/Spring/Spring.Http.Tests/Http/Converters/Xml/DataContractHttpMessageConverterIntegrationTests.cs @@ -0,0 +1,328 @@ +#if NET_4_0 +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Net; +using System.Collections.Generic; +using System.ServiceModel; +using System.ServiceModel.Web; + +using Spring.Http.Converters; +using Spring.Http.Converters.Xml; + +using NUnit.Framework; + +namespace Spring.Http.Rest.Xml +{ + /// + /// Integration tests for the DataContractHttpMessageConverter class. + /// + /// Bruno Baia + [TestFixture] + public class DataContractHttpMessageConverterIntegrationTests + { + #region Logging + + private static readonly Common.Logging.ILog LOG = Common.Logging.LogManager.GetLogger(typeof(RestTemplateIntegrationTests)); + + #endregion + + private WebServiceHost webServiceHost; + private string uri = "http://localhost:1337"; + private RestTemplate template; + private MediaType contentType; + + [SetUp] + public void SetUp() + { + template = new RestTemplate(uri); + template.MessageConverters = new List(); + template.MessageConverters.Add(new StringHttpMessageConverter()); // for debugging purpose + + contentType = new MediaType("application", "xml"); + + webServiceHost = new WebServiceHost(typeof(TestService), new Uri(uri)); + webServiceHost.Open(); + } + + [TearDown] + public void TearDownClass() + { + webServiceHost.Close(); + } + + //[Test] + //public void GetString() + //{ + // string result = template.GetForObject("users"); + // Assert.AreEqual("2", result, "Invalid content"); + //} + + [Test] + public void GetUser() + { + template.MessageConverters.Add(new DataContractHttpMessageConverter()); + + User result = template.GetForObject("user/{id}", "1"); + Assert.IsNotNull(result, "Invalid content"); + Assert.AreEqual("1", result.ID, "Invalid content"); + Assert.AreEqual("Bruno Baïa", result.Name, "Invalid content"); + } + + //[Test] + //public void GetStringDictionaryTemplateVariables() + //{ + // IDictionary uriVariables = new Dictionary(1); + // uriVariables.Add("id", "2"); + // string result = template.GetForObject("user/{id}", uriVariables); + // Assert.AreEqual("Marie Baia", result, "Invalid content"); + //} + + //[Test] + //[ExpectedException(typeof(RestClientException), + // ExpectedMessage = "The server returned 'User with id '5' not found' with the status code 404 - NotFound.")] + //public void GetStringError() + //{ + // string result = template.GetForObject("user/{id}", "5"); + //} + + //[Test] + //public void GetStringForMessage() + //{ + // HttpResponseMessage result = template.GetForMessage("user/{id}", "1"); + // Assert.AreEqual("Bruno Baïa", result.Body, "Invalid content"); + // Assert.AreEqual(contentType, MediaType.ParseMediaType(result.Headers[HttpResponseHeader.ContentType]), "Invalid content-type"); + // Assert.AreEqual(HttpStatusCode.OK, result.StatusCode, "Invalid status code"); + // Assert.AreEqual("OK", result.StatusDescription, "Invalid status description"); + //} + + //[Test] + //[ExpectedException(typeof(RestClientException), + // ExpectedMessage = "Could not extract response: no Content-Type found")] + //public void GetStringNoResponse() + //{ + // string result = template.GetForObject("/nothing"); + //} + + //[Test] + //public void HeadForHeaders() + //{ + // WebHeaderCollection result = template.HeadForHeaders("head"); + // Assert.AreEqual(new MediaType("text", "plain"), MediaType.ParseMediaType(result[HttpResponseHeader.ContentType]), "Invalid content-type"); + //} + + //[Test] + //public void PostStringForLocation() + //{ + // Uri result = template.PostForLocation("user", "Lisa Baia"); + // Assert.AreEqual(new Uri(new Uri(uri), "/user/3"), result, "Invalid location"); + //} + + //[Test] + //public void PostStringForMessage() + //{ + // HttpResponseMessage result = template.PostForMessage("user", "Lisa Baia"); + // Assert.AreEqual(new Uri(new Uri(uri), "/user/3"), new Uri(result.Headers[HttpResponseHeader.Location]), "Invalid location"); + // Assert.AreEqual(HttpStatusCode.Created, result.StatusCode, "Invalid status code"); + // Assert.AreEqual("User id '3' created with 'Lisa Baia'", result.StatusDescription, "Invalid status description"); + // Assert.AreEqual("3", result.Body, "Invalid content"); + //} + + [Test] + public void PostStringForObject() + { + template.MessageConverters.Add(new DataContractHttpMessageConverter()); + + User user = new User() { Name = "Lisa Baia" }; + + User result = template.PostForObject("user", user); + Assert.AreEqual("3", result.ID, "Invalid content"); + Assert.AreEqual(user.Name, result.Name, "Invalid content"); + } + + //[Test] + //[ExpectedException(typeof(RestClientException), + // ExpectedMessage = "The server returned 'Content cannot be null or empty' with the status code 400 - BadRequest.")] + //public void PostStringForObjectWithError() + //{ + // string result = template.PostForObject("user", ""); + //} + + //[Test] + //public void Put() + //{ + // string result = template.GetForObject("user/1"); + // Assert.AreEqual("Bruno Baïa", result, "Invalid content"); + + // template.Put("user/1", "Bruno Baia"); + + // result = template.GetForObject("user/1"); + // Assert.AreEqual("Bruno Baia", result, "Invalid content"); + //} + + //[Test] + //[ExpectedException(typeof(RestClientException), + // ExpectedMessage = "The server returned 'User id '4' does not exist' with the status code 400 - BadRequest.")] + //public void PutWithError() + //{ + // template.Put("user/4", "Dinora Baia"); + //} + + //[Test] + //public void Delete() + //{ + // string result = template.GetForObject("users"); + // Assert.AreEqual("2", result, "Invalid content"); + + // template.Delete("user/2"); + + // result = template.GetForObject("users"); + // Assert.AreEqual("1", result, "Invalid content"); + //} + + //[Test] + //[ExpectedException(typeof(RestClientException), + // ExpectedMessage = "The server returned 'User id '10' does not exist' with the status code 400 - BadRequest.")] + //public void DeleteWithError() + //{ + // template.Delete("user/10"); + //} + + //[Test] + //public void OptionsForAllow() + //{ + // IList result = template.OptionsForAllow("allow"); + // Assert.AreEqual(3, result.Count, "Invalid response"); + // Assert.IsTrue(result.Contains(HttpMethod.GET), "Invalid response"); + // Assert.IsTrue(result.Contains(HttpMethod.HEAD), "Invalid response"); + // Assert.IsTrue(result.Contains(HttpMethod.PUT), "Invalid response"); + //} + + //[Test] + //public void ExchangePost() + //{ + // HttpResponseMessage result = template.Exchange( + // "user", new HttpRequestMessage("Maryse Baia", HttpMethod.POST)); + + // Assert.AreEqual("3", result.Body, "Invalid content"); + // Assert.AreEqual(HttpStatusCode.Created, result.StatusCode, "Invalid status code"); + // Assert.AreEqual("User id '3' created with 'Maryse Baia'", result.StatusDescription, "Invalid status description"); + //} + + //[Test] + //public void ExchangePut() + //{ + // HttpResponseMessage result = template.Exchange( + // "user/1", new HttpRequestMessage("Bruno Baia", HttpMethod.PUT)); + + // Assert.AreEqual(HttpStatusCode.OK, result.StatusCode, "Invalid status code"); + // Assert.AreEqual("User id '1' updated with 'Bruno Baia'", result.StatusDescription, "Invalid status description"); + //} + + //[Test] + //[ExpectedException(ExpectedMessage = "The server returned 'Not Found' with the status code 404 - NotFound.")] + //public void ClientError() + //{ + // template.Execute("clienterror", null, null); + //} + + //[Test] + //[ExpectedException(ExpectedMessage = "The server returned 'Internal Server Error' with the status code 500 - InternalServerError.")] + //public void ServerError() + //{ + // template.Execute("servererror", null, null); + //} + + #region REST test service + + //[DataContract] + public class User + { + //[DataMember] + public string ID { get; set; } + + //[DataMember] + public string Name { get; set; } + } + + [ServiceContract] + [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)] + public class TestService + { + private IList users; + + public TestService() + { + users = new List(); + users.Add(new User() { ID = "1", Name = "Bruno Baïa" }); + users.Add(new User() { ID = "2", Name = "Marie Baia" }); + } + + [WebGet(UriTemplate = "user/{id}")] + public User GetUser(string id) + { + WebOperationContext context = WebOperationContext.Current; + + foreach (User user in this.users) + { + if (user.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) + { + return user; + } + } + + context.OutgoingResponse.SetStatusAsNotFound(String.Format("User with id '{0}' not found", id)); + return null; + } + + [WebInvoke(UriTemplate = "user", Method = "POST")] + public User Post(User user) + { + WebOperationContext context = WebOperationContext.Current; + + UriTemplateMatch match = context.IncomingRequest.UriTemplateMatch; + UriTemplate template = new UriTemplate("/user/{id}"); + + MediaType mediaType = MediaType.ParseMediaType(context.IncomingRequest.ContentType); + + user.ID = (users.Count + 1).ToString(); // generate new ID + + if (!String.IsNullOrEmpty(user.ID)) + { + context.OutgoingResponse.StatusCode = HttpStatusCode.BadRequest; + context.OutgoingResponse.StatusDescription = "Content cannot be null or empty"; + return user; + } + + users.Add(user); + + Uri uri = template.BindByPosition(match.BaseUri, user.ID); + context.OutgoingResponse.SetStatusAsCreated(uri); + context.OutgoingResponse.StatusDescription = String.Format("User id '{0}' created", user.ID); + + return user; + } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/test/Spring/Spring.Http.Tests/Http/Converters/Xml/DataContractHttpMessageConverterTests.cs b/test/Spring/Spring.Http.Tests/Http/Converters/Xml/DataContractHttpMessageConverterTests.cs new file mode 100644 index 00000000..29f2ee1a --- /dev/null +++ b/test/Spring/Spring.Http.Tests/Http/Converters/Xml/DataContractHttpMessageConverterTests.cs @@ -0,0 +1,119 @@ +#if NET_3_0 +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.IO; +using System.Net; +using System.Text; + +using NUnit.Framework; +using Rhino.Mocks; +using System.Xml; +using System.Runtime.Serialization; + +namespace Spring.Http.Converters.Xml +{ + /// + /// Unit tests for the DataContractHttpMessageConverter class. + /// + /// Bruno Baia + [TestFixture] + public class DataContractHttpMessageConverterTests + { + private DataContractHttpMessageConverter converter; + private MockRepository mocks; + + [SetUp] + public void SetUp() + { + mocks = new MockRepository(); + converter = new DataContractHttpMessageConverter(); + } + + [Test] + public void CanRead() + { + Assert.IsTrue(converter.CanRead(typeof(CustomClass), new MediaType("application", "xml"))); + Assert.IsTrue(converter.CanRead(typeof(CustomClass), new MediaType("text", "xml"))); + Assert.IsTrue(converter.CanRead(typeof(CustomClass), new MediaType("application", "soap+xml"))); // application/*+xml + } + + [Test] + public void CanWrite() + { + Assert.IsTrue(converter.CanWrite(typeof(CustomClass), new MediaType("application", "xml"))); + Assert.IsTrue(converter.CanWrite(typeof(CustomClass), new MediaType("text", "xml"))); + Assert.IsTrue(converter.CanRead(typeof(CustomClass), new MediaType("application", "soap+xml"))); // application/*+xml + } + + //[Test] + //public void Read() + //{ + // string body = ""; + + // HttpWebResponse webResponse = mocks.CreateMock(); + // Expect.Call(webResponse.GetResponseStream()).Return(new MemoryStream(Encoding.UTF8.GetBytes(body))).Repeat.Once(); + + // mocks.ReplayAll(); + + // XmlDocument result = converter.Read(webResponse); + // Assert.IsNotNull(result, "Invalid result"); + + // mocks.VerifyAll(); + //} + + //[Test] + //public void Write() + //{ + // XmlDocument body = new XmlDocument(); + // body.CreateElement("TestElement"); + + // HttpWebRequest webRequest = WebRequest.Create("http://localhost") as HttpWebRequest; + // webRequest.Method = "POST"; + + // converter.Write(body, null, webRequest); + + // Assert.AreEqual(new MediaType("application", "xml"), MediaType.ParseMediaType(webRequest.ContentType), "Invalid content-type"); + + // using (Stream postStream = webRequest.GetRequestStream()) + // { + // using (StreamReader reader = new StreamReader(postStream)) + // { + // string result = reader.ReadToEnd(); + // Assert.AreEqual(result.Length, webRequest.ContentLength, "Invalid content-length"); + // } + + // } + //} + + #region Test classes + + [DataContract] + public class CustomClass + { + [DataMember] + public string ID { get; set; } + } + + #endregion + } +} +#endif diff --git a/test/Spring/Spring.Http.Tests/Http/Converters/Xml/XmlDocumentHttpMessageConverterTests.cs b/test/Spring/Spring.Http.Tests/Http/Converters/Xml/XmlDocumentHttpMessageConverterTests.cs new file mode 100644 index 00000000..ee3e261b --- /dev/null +++ b/test/Spring/Spring.Http.Tests/Http/Converters/Xml/XmlDocumentHttpMessageConverterTests.cs @@ -0,0 +1,105 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.IO; +using System.Net; +using System.Text; + +using NUnit.Framework; +using Rhino.Mocks; +using System.Xml; + +namespace Spring.Http.Converters.Xml +{ + /// + /// Unit tests for the XmlDocumentHttpMessageConverter class. + /// + /// Bruno Baia + [TestFixture] + public class XmlDocumentHttpMessageConverterTests + { + private XmlDocumentHttpMessageConverter converter; + private MockRepository mocks; + + [SetUp] + public void SetUp() + { + mocks = new MockRepository(); + converter = new XmlDocumentHttpMessageConverter(); + } + + [Test] + public void CanRead() + { + Assert.IsTrue(converter.CanRead(typeof(XmlDocument), new MediaType("application", "xml"))); + Assert.IsTrue(converter.CanRead(typeof(XmlDocument), new MediaType("text", "xml"))); + Assert.IsTrue(converter.CanRead(typeof(XmlDocument), new MediaType("application", "soap+xml"))); // application/*+xml + } + + [Test] + public void CanWrite() + { + Assert.IsTrue(converter.CanWrite(typeof(XmlDocument), new MediaType("application", "xml"))); + Assert.IsTrue(converter.CanWrite(typeof(XmlDocument), new MediaType("text", "xml"))); + Assert.IsTrue(converter.CanRead(typeof(XmlDocument), new MediaType("application", "soap+xml"))); // application/*+xml + } + + [Test] + public void Read() + { + string body = ""; + + HttpWebResponse webResponse = mocks.CreateMock(); + Expect.Call(webResponse.GetResponseStream()).Return(new MemoryStream(Encoding.UTF8.GetBytes(body))).Repeat.Once(); + + mocks.ReplayAll(); + + XmlDocument result = converter.Read(webResponse); + Assert.IsNotNull(result, "Invalid result"); + + mocks.VerifyAll(); + } + + [Test] + public void Write() + { + XmlDocument body = new XmlDocument(); + body.CreateElement("TestElement"); + + HttpWebRequest webRequest = WebRequest.Create("http://localhost") as HttpWebRequest; + webRequest.Method = "POST"; + + converter.Write(body, null, webRequest); + + Assert.AreEqual(new MediaType("application", "xml"), MediaType.ParseMediaType(webRequest.ContentType), "Invalid content-type"); + + using (Stream postStream = webRequest.GetRequestStream()) + { + using (StreamReader reader = new StreamReader(postStream)) + { + string result = reader.ReadToEnd(); + Assert.AreEqual(result.Length, webRequest.ContentLength, "Invalid content-length"); + } + + } + } + } +} diff --git a/test/Spring/Spring.Http.Tests/Http/MediaTypeTests.cs b/test/Spring/Spring.Http.Tests/Http/MediaTypeTests.cs new file mode 100644 index 00000000..fe1c9c0b --- /dev/null +++ b/test/Spring/Spring.Http.Tests/Http/MediaTypeTests.cs @@ -0,0 +1,544 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Collections.Generic; + +using NUnit.Framework; + +namespace Spring.Http +{ + /// + /// Unit tests for the MediaType class. + /// + /// Arjen Poutsma + /// Juergen Hoeller + /// Bruno Baia (.NET) + [TestFixture] + public class MediaTypeTests + { + [Test] + public void Includes() + { + MediaType textPlain = MediaType.TEXT_PLAIN; + Assert.IsTrue(textPlain.Includes(textPlain), "Equal types is not inclusive"); + MediaType allText = new MediaType("text"); + + Assert.IsTrue(allText.Includes(textPlain), "All subtypes is not inclusive"); + Assert.IsFalse(textPlain.Includes(allText), "All subtypes is inclusive"); + + Assert.IsTrue(MediaType.ALL.Includes(textPlain), "All types is not inclusive"); + Assert.IsFalse(textPlain.Includes(MediaType.ALL), "All types is inclusive"); + + Assert.IsTrue(MediaType.ALL.Includes(textPlain), "All types is not inclusive"); + Assert.IsFalse(textPlain.Includes(MediaType.ALL), "All types is inclusive"); + + MediaType applicationSoapXml = new MediaType("application", "soap+xml"); + MediaType applicationWildcardXml = new MediaType("application", "*+xml"); + + Assert.IsTrue(applicationSoapXml.Includes(applicationSoapXml)); + Assert.IsTrue(applicationWildcardXml.Includes(applicationWildcardXml)); + + Assert.IsTrue(applicationWildcardXml.Includes(applicationSoapXml)); + Assert.IsFalse(applicationSoapXml.Includes(applicationWildcardXml)); + } + + [Test] + public void IsCompatible() + { + MediaType textPlain = MediaType.TEXT_PLAIN; + Assert.IsTrue(textPlain.IsCompatibleWith(textPlain), "Equal types is not compatible"); + MediaType allText = new MediaType("text"); + + Assert.IsTrue(allText.IsCompatibleWith(textPlain), "All subtypes is not compatible"); + Assert.IsTrue(textPlain.IsCompatibleWith(allText), "All subtypes is not compatible"); + + Assert.IsTrue(MediaType.ALL.IsCompatibleWith(textPlain), "All types is not compatible"); + Assert.IsTrue(textPlain.IsCompatibleWith(MediaType.ALL), "All types is not compatible"); + + Assert.IsTrue(MediaType.ALL.IsCompatibleWith(textPlain), "All types is not compatible"); + Assert.IsTrue(textPlain.IsCompatibleWith(MediaType.ALL), "All types is compatible"); + + MediaType applicationSoapXml = new MediaType("application", "soap+xml"); + MediaType applicationWildcardXml = new MediaType("application", "*+xml"); + + Assert.IsTrue(applicationSoapXml.IsCompatibleWith(applicationSoapXml)); + Assert.IsTrue(applicationWildcardXml.IsCompatibleWith(applicationWildcardXml)); + + Assert.IsTrue(applicationWildcardXml.IsCompatibleWith(applicationSoapXml)); + Assert.IsTrue(applicationSoapXml.IsCompatibleWith(applicationWildcardXml)); + } + + [Test] + public void TestToString() + { + MediaType mediaType = new MediaType("text", "plain", 0.7); + String result = mediaType.ToString(); + Assert.AreEqual("text/plain;q=0.7", result, "Invalid toString() returned"); + } + + //[Test](expected= IllegalArgumentException.class) + //public void slashInType() { + // new MediaType("text/plain"); + //} + + //[Test](expected= IllegalArgumentException.class) + //public void slashInSubtype() { + // new MediaType("text", "/"); + //} + + [Test] + public void GetDefaultQualityValue() + { + MediaType mediaType = new MediaType("text", "plain"); + Assert.AreEqual(1, mediaType.QualityValue, "Invalid quality value"); + } + + [Test] + public void ParseMediaType() + { + string s = "audio/*; q=0.2"; + MediaType mediaType = MediaType.ParseMediaType(s); + Assert.AreEqual("audio", mediaType.Type, "Invalid type"); + Assert.AreEqual("*", mediaType.Subtype, "Invalid subtype"); + Assert.AreEqual(0.2, mediaType.QualityValue, "Invalid quality factor"); + } + + [Test] + [ExpectedException(typeof(ArgumentException))] + public void ParseMediaTypeNoSubtype() + { + MediaType.ParseMediaType("audio"); + } + + [Test] + [ExpectedException(typeof(ArgumentException))] + public void ParseMediaTypeNoSubtypeSlash() + { + MediaType.ParseMediaType("audio/"); + } + + //[Test](expected = IllegalArgumentException.class) + //public void parseMediaTypeIllegalType() { + // MediaType.parseMediaType("audio(/basic"); + //} + + //[Test](expected = IllegalArgumentException.class) + //public void parseMediaTypeIllegalSubtype() { + // MediaType.parseMediaType("audio/basic)"); + //} + + //[Test](expected = IllegalArgumentException.class) + //public void parseMediaTypeEmptyParameterAttribute() { + // MediaType.parseMediaType("audio/*;=value"); + //} + + //[Test](expected = IllegalArgumentException.class) + //public void parseMediaTypeEmptyParameterValue() { + // MediaType.parseMediaType("audio/*;attr="); + //} + + //[Test](expected = IllegalArgumentException.class) + //public void parseMediaTypeIllegalParameterAttribute() { + // MediaType.parseMediaType("audio/*;attr<=value"); + //} + + //[Test](expected = IllegalArgumentException.class) + //public void parseMediaTypeIllegalParameterValue() { + // MediaType.parseMediaType("audio/*;attr=v>alue"); + //} + + //[Test](expected = IllegalArgumentException.class) + //public void parseMediaTypeIllegalQualityFactor() { + // MediaType.parseMediaType("audio/basic;q=1.1"); + //} + + //[Test](expected = IllegalArgumentException.class) + //public void parseMediaTypeIllegalCharset() { + // MediaType.parseMediaType("text/html; charset=foo-bar"); + //} + + //[Test] + //public void parseMediaTypeQuotedParameterValue() { + // MediaType.parseMediaType("audio/*;attr=\"v>alue\""); + //} + + //[Test](expected = IllegalArgumentException.class) + //public void parseMediaTypeIllegalQuotedParameterValue() { + // MediaType.parseMediaType("audio/*;attr=\""); + //} + + //[Test] + //public void parseCharset() throws Exception { + // String s = "text/html; charset=iso-8859-1"; + // MediaType mediaType = MediaType.parseMediaType(s); + // Assert.AreEqual("Invalid type", "text", mediaType.getType()); + // Assert.AreEqual("Invalid subtype", "html", mediaType.getSubtype()); + // Assert.AreEqual("Invalid charset", Charset.forName("ISO-8859-1"), mediaType.getCharSet()); + //} + + //[Test] + //public void parseQuotedCharset() { + // String s = "application/xml;charset=\"utf-8\""; + // MediaType mediaType = MediaType.parseMediaType(s); + // Assert.AreEqual("Invalid type", "application", mediaType.getType()); + // Assert.AreEqual("Invalid subtype", "xml", mediaType.getSubtype()); + // Assert.AreEqual("Invalid charset", Charset.forName("UTF-8"), mediaType.getCharSet()); + //} + + [Test] + public void ParseURLConnectionMediaType() + { + string s = "*; q=.2"; + MediaType mediaType = MediaType.ParseMediaType(s); + Assert.AreEqual(mediaType.Type, "*", "Invalid type"); + Assert.AreEqual("*", mediaType.Subtype, "Invalid subtype"); + Assert.AreEqual(0.2, mediaType.QualityValue, "Invalid quality factor"); + } + + [Test] + public void ParseMediaTypes() + { + string s = "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"; + List mediaTypes = MediaType.ParseMediaTypes(s); + Assert.NotNull(mediaTypes, "No media types returned"); + Assert.AreEqual(4, mediaTypes.Count, "Invalid amount of media types"); + + mediaTypes = MediaType.ParseMediaTypes(null); + Assert.NotNull(mediaTypes, "No media types returned"); + Assert.AreEqual(0, mediaTypes.Count, "Invalid amount of media types"); + } + + [Test] + public void CompareTo() + { + MediaType audioBasic = new MediaType("audio", "basic"); + MediaType audio = new MediaType("audio"); + MediaType audioWave = new MediaType("audio", "wave"); + MediaType audioBasicLevel = new MediaType("audio", "basic", SingletonDictionary("level", "1")); + MediaType audioBasic07 = new MediaType("audio", "basic", 0.7); + + // equal + Assert.AreEqual(0, audioBasic.CompareTo(audioBasic), "Invalid comparison result"); + Assert.AreEqual(0, audio.CompareTo(audio), "Invalid comparison result"); + Assert.AreEqual(0, audioBasicLevel.CompareTo(audioBasicLevel), "Invalid comparison result"); + + Assert.IsTrue(audioBasicLevel.CompareTo(audio) > 0, "Invalid comparison result"); + + List expected = new List(); + expected.Add(audio); + expected.Add(audioBasic); + expected.Add(audioBasicLevel); + expected.Add(audioBasic07); + expected.Add(audioWave); + + List result = new List(expected); + // shuffle & sort 10 times + for (int i = 0; i < 10; i++) + { + result.Sort(ShuffleComparison); + result.Sort(); + + for (int j = 0; j < result.Count; j++) + { + Assert.AreSame(expected[j], result[j], "Invalid media type at " + j + ", run " + i); + } + } + } + + [Test] + public void CompareToConsistentWithEquals() + { + MediaType m1 = MediaType.ParseMediaType("text/html; q=0.7; charset=iso-8859-1"); + MediaType m2 = MediaType.ParseMediaType("text/html; charset=iso-8859-1; q=0.7"); + + Assert.AreEqual(m1, m2, "Media types not equal"); + Assert.AreEqual(0, m1.CompareTo(m2), "compareTo() not consistent with equals"); + Assert.AreEqual(0, m2.CompareTo(m1), "compareTo() not consistent with equals"); + + m1 = MediaType.ParseMediaType("text/html; q=0.7; charset=iso-8859-1"); + m2 = MediaType.ParseMediaType("text/html; Q=0.7; charset=iso-8859-1"); + Assert.AreEqual(m1, m2, "Media types not equal"); + Assert.AreEqual(0, m1.CompareTo(m2), "compareTo() not consistent with equals"); + Assert.AreEqual(0, m2.CompareTo(m1), "compareTo() not consistent with equals"); + } + + [Test] + public void CompareToCaseSensitivity() + { + MediaType m1 = new MediaType("audio", "basic"); + MediaType m2 = new MediaType("Audio", "Basic"); + Assert.AreEqual(0, m1.CompareTo(m2), "Invalid comparison result"); + Assert.AreEqual(0, m2.CompareTo(m1), "Invalid comparison result"); + + m1 = new MediaType("audio", "basic", SingletonDictionary("foo", "bar")); + m2 = new MediaType("audio", "basic", SingletonDictionary("Foo", "bar")); + Assert.AreEqual(0, m1.CompareTo(m2), "Invalid comparison result"); + Assert.AreEqual(0, m2.CompareTo(m1), "Invalid comparison result"); + + m1 = new MediaType("audio", "basic", SingletonDictionary("foo", "bar")); + m2 = new MediaType("audio", "basic", SingletonDictionary("foo", "Bar")); + Assert.IsTrue(m1.CompareTo(m2) != 0, "Invalid comparison result"); + Assert.IsTrue(m2.CompareTo(m1) != 0, "Invalid comparison result"); + } + + [Test] + public void SpecificityComparator() + { + MediaType audioBasic = new MediaType("audio", "basic"); + MediaType audioWave = new MediaType("audio", "wave"); + MediaType audio = new MediaType("audio"); + MediaType audio03 = new MediaType("audio", "*", 0.3); + MediaType audio07 = new MediaType("audio", "*", 0.7); + MediaType audioBasicLevel = new MediaType("audio", "basic", SingletonDictionary("level", "1")); + MediaType textHtml = new MediaType("text", "html"); + MediaType all = MediaType.ALL; + + IComparer comp = MediaType.SPECIFICITY_COMPARER; + + // equal + Assert.AreEqual(0, comp.Compare(audioBasic,audioBasic), "Invalid comparison result"); + Assert.AreEqual(0, comp.Compare(audio, audio), "Invalid comparison result"); + Assert.AreEqual(0, comp.Compare(audio07, audio07), "Invalid comparison result"); + Assert.AreEqual(0, comp.Compare(audio03, audio03), "Invalid comparison result"); + Assert.AreEqual(0, comp.Compare(audioBasicLevel, audioBasicLevel), "Invalid comparison result"); + + // specific to unspecific + Assert.IsTrue(comp.Compare(audioBasic, audio) < 0, "Invalid comparison result"); + Assert.IsTrue(comp.Compare(audioBasic, all) < 0, "Invalid comparison result"); + Assert.IsTrue(comp.Compare(audio, all) < 0, "Invalid comparison result"); + + // unspecific to specific + Assert.IsTrue(comp.Compare(audio, audioBasic) > 0, "Invalid comparison result"); + Assert.IsTrue(comp.Compare(all, audioBasic) > 0, "Invalid comparison result"); + Assert.IsTrue(comp.Compare(all, audio) > 0, "Invalid comparison result"); + + // qualifiers + Assert.IsTrue(comp.Compare(audio, audio07) < 0, "Invalid comparison result"); + Assert.IsTrue(comp.Compare(audio07, audio) > 0, "Invalid comparison result"); + Assert.IsTrue(comp.Compare(audio07, audio03) < 0, "Invalid comparison result"); + Assert.IsTrue(comp.Compare(audio03, audio07) > 0, "Invalid comparison result"); + Assert.IsTrue(comp.Compare(audio03, all) < 0, "Invalid comparison result"); + Assert.IsTrue(comp.Compare(all, audio03) > 0, "Invalid comparison result"); + + // other parameters + Assert.IsTrue(comp.Compare(audioBasic, audioBasicLevel) > 0, "Invalid comparison result"); + Assert.IsTrue(comp.Compare(audioBasicLevel, audioBasic) < 0, "Invalid comparison result"); + + // different types + Assert.AreEqual(0, comp.Compare(audioBasic, textHtml), "Invalid comparison result"); + Assert.AreEqual(0, comp.Compare(textHtml, audioBasic), "Invalid comparison result"); + + // different subtypes + Assert.AreEqual(0, comp.Compare(audioBasic, audioWave), "Invalid comparison result"); + Assert.AreEqual(0, comp.Compare(audioWave, audioBasic), "Invalid comparison result"); + } + + [Test] + public void SortBySpecificityRelated() + { + MediaType audioBasic = new MediaType("audio", "basic"); + MediaType audio = new MediaType("audio"); + MediaType audio03 = new MediaType("audio", "*", 0.3); + MediaType audio07 = new MediaType("audio", "*", 0.7); + MediaType audioBasicLevel = new MediaType("audio", "basic", SingletonDictionary("level", "1")); + MediaType all = MediaType.ALL; + + List expected = new List(); + expected.Add(audioBasicLevel); + expected.Add(audioBasic); + expected.Add(audio); + expected.Add(audio07); + expected.Add(audio03); + expected.Add(all); + + List result = new List(expected); + // shuffle & sort 10 times + for (int i = 0; i < 10; i++) + { + result.Sort(ShuffleComparison); + MediaType.SortBySpecificity(result); + + for (int j = 0; j < result.Count; j++) + { + Assert.AreSame(expected[j], result[j], "Invalid media type at " + j); + } + } + } + + [Test] + [Ignore] + public void SortBySpecificityUnrelated() + { + MediaType audioBasic = new MediaType("audio", "basic"); + MediaType audioWave = new MediaType("audio", "wave"); + MediaType textHtml = new MediaType("text", "html"); + + List expected = new List(); + expected.Add(textHtml); + expected.Add(audioBasic); + expected.Add(audioWave); + + List result = new List(expected); + MediaType.SortBySpecificity(result); + + for (int i = 0; i < result.Count; i++) + { + Assert.AreSame(expected[i], result[i], "Invalid media type at " + i); + } + } + + [Test] + public void QualityComparator() + { + MediaType audioBasic = new MediaType("audio", "basic"); + MediaType audioWave = new MediaType("audio", "wave"); + MediaType audio = new MediaType("audio"); + MediaType audio03 = new MediaType("audio", "*", 0.3); + MediaType audio07 = new MediaType("audio", "*", 0.7); + MediaType audioBasicLevel = new MediaType("audio", "basic", SingletonDictionary("level", "1")); + MediaType textHtml = new MediaType("text", "html"); + MediaType all = MediaType.ALL; + + IComparer comp = MediaType.QUALITY_VALUE_COMPARER; + + // equal + Assert.AreEqual(0, comp.Compare(audioBasic, audioBasic), "Invalid comparison result"); + Assert.AreEqual(0, comp.Compare(audio, audio), "Invalid comparison result"); + Assert.AreEqual(0, comp.Compare(audio07, audio07), "Invalid comparison result"); + Assert.AreEqual(0, comp.Compare(audio03, audio03), "Invalid comparison result"); + Assert.AreEqual(0, comp.Compare(audioBasicLevel, audioBasicLevel), "Invalid comparison result"); + + // specific to unspecific + Assert.IsTrue(comp.Compare(audioBasic, audio) < 0, "Invalid comparison result"); + Assert.IsTrue(comp.Compare(audioBasic, all) < 0, "Invalid comparison result"); + Assert.IsTrue(comp.Compare(audio, all) < 0, "Invalid comparison result"); + + // unspecific to specific + Assert.IsTrue(comp.Compare(audio, audioBasic) > 0, "Invalid comparison result"); + Assert.IsTrue(comp.Compare(all, audioBasic) > 0, "Invalid comparison result"); + Assert.IsTrue(comp.Compare(all, audio) > 0, "Invalid comparison result"); + + // qualifiers + Assert.IsTrue(comp.Compare(audio, audio07) < 0, "Invalid comparison result"); + Assert.IsTrue(comp.Compare(audio07, audio) > 0, "Invalid comparison result"); + Assert.IsTrue(comp.Compare(audio07, audio03) < 0, "Invalid comparison result"); + Assert.IsTrue(comp.Compare(audio03, audio07) > 0, "Invalid comparison result"); + Assert.IsTrue(comp.Compare(audio03, all) > 0, "Invalid comparison result"); + Assert.IsTrue(comp.Compare(all, audio03) < 0, "Invalid comparison result"); + + // other parameters + Assert.IsTrue(comp.Compare(audioBasic, audioBasicLevel) > 0, "Invalid comparison result"); + Assert.IsTrue(comp.Compare(audioBasicLevel, audioBasic) < 0, "Invalid comparison result"); + + // different types + Assert.AreEqual(0, comp.Compare(audioBasic, textHtml), "Invalid comparison result"); + Assert.AreEqual(0, comp.Compare(textHtml, audioBasic), "Invalid comparison result"); + + // different subtypes + Assert.AreEqual(0, comp.Compare(audioBasic, audioWave), "Invalid comparison result"); + Assert.AreEqual(0, comp.Compare(audioWave, audioBasic), "Invalid comparison result"); + } + + [Test] + public void SortByQualityRelated() + { + MediaType audioBasic = new MediaType("audio", "basic"); + MediaType audio = new MediaType("audio"); + MediaType audio03 = new MediaType("audio", "*", 0.3); + MediaType audio07 = new MediaType("audio", "*", 0.7); + MediaType audioBasicLevel = new MediaType("audio", "basic", SingletonDictionary("level", "1")); + MediaType all = MediaType.ALL; + + List expected = new List(); + expected.Add(audioBasicLevel); + expected.Add(audioBasic); + expected.Add(audio); + expected.Add(all); + expected.Add(audio07); + expected.Add(audio03); + + List result = new List(expected); + // shuffle & sort 10 times + for (int i = 0; i < 10; i++) + { + result.Sort(ShuffleComparison); + MediaType.SortByQualityValue(result); + + for (int j = 0; j < result.Count; j++) + { + Assert.AreSame(expected[j], result[j], "Invalid media type at " + j); + } + } + } + + [Test] + [Ignore] + public void SortByQualityUnrelated() + { + MediaType audioBasic = new MediaType("audio", "basic"); + MediaType audioWave = new MediaType("audio", "wave"); + MediaType textHtml = new MediaType("text", "html"); + + List expected = new List(); + expected.Add(textHtml); + expected.Add(audioBasic); + expected.Add(audioWave); + + List result = new List(expected); + MediaType.SortBySpecificity(result); + + for (int i = 0; i < result.Count; i++) + { + Assert.AreSame(expected[i], result[i], "Invalid media type at " + i); + } + } + + //[Test] + //public void testWithConversionService() { + // ConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + // Assert.IsTrue(conversionService.canConvert(String.class, MediaType.class)); + // MediaType mediaType = MediaType.parseMediaType("application/xml"); + // Assert.AreEqual(mediaType, conversionService.convert("application/xml", MediaType.class)); + //} + + #region Utils + + private static int ShuffleComparison(MediaType mediaType1, MediaType mediaType2) + { + if (mediaType1 == mediaType2) + { + return 0; + } + + Random rd = new Random(); + return rd.Next(-1, 2); + } + + private static IDictionary SingletonDictionary(TKey key, TValue value) + { + IDictionary dictionary = new Dictionary(1); + dictionary.Add(key, value); + return dictionary; + } + + #endregion + } +} diff --git a/test/Spring/Spring.Http.Tests/Http/Rest/RestTemplateIntegrationTests.cs b/test/Spring/Spring.Http.Tests/Http/Rest/RestTemplateIntegrationTests.cs new file mode 100644 index 00000000..c578b4ea --- /dev/null +++ b/test/Spring/Spring.Http.Tests/Http/Rest/RestTemplateIntegrationTests.cs @@ -0,0 +1,486 @@ +#if NET_4_0 +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Net; +using System.IO; +using System.Text; +using System.Collections.Generic; +using System.ServiceModel; +using System.ServiceModel.Web; +using System.ServiceModel.Channels; + +using Spring.Http; +using Spring.Http.Rest.Support; + +using NUnit.Framework; + +namespace Spring.Http.Rest +{ + /// + /// Integration tests for the RestTemplate class. + /// + /// Arjen Poutsma + /// Bruno Baia (.NET) + [TestFixture] + public class RestTemplateIntegrationTests + { + #region Logging + + private static readonly Common.Logging.ILog LOG = Common.Logging.LogManager.GetLogger(typeof(RestTemplateIntegrationTests)); + + #endregion + + private WebServiceHost webServiceHost; + private string uri = "http://localhost:1337"; + private RestTemplate template; + private MediaType contentType; + + [SetUp] + public void SetUp() + { + template = new RestTemplate(uri); + contentType = new MediaType("text", "plain"); + + webServiceHost = new WebServiceHost(typeof(TestService), new Uri(uri)); + webServiceHost.Open(); + } + + [TearDown] + public void TearDownClass() + { + webServiceHost.Close(); + } + + [Test] + public void GetString() + { + string result = template.GetForObject("users"); + Assert.AreEqual("2", result, "Invalid content"); + } + + [Test] + public void GetStringVarArgsTemplateVariables() + { + string result = template.GetForObject("user/{id}", "1"); + Assert.AreEqual("Bruno Baïa", result, "Invalid content"); + } + + [Test] + public void GetStringDictionaryTemplateVariables() + { + IDictionary uriVariables = new Dictionary(1); + uriVariables.Add("id", "2"); + string result = template.GetForObject("user/{id}", uriVariables); + Assert.AreEqual("Marie Baia", result, "Invalid content"); + } + + [Test] + [ExpectedException(typeof(RestClientException), + ExpectedMessage = "The server returned 'User with id '5' not found' with the status code 404 - NotFound.")] + public void GetStringError() + { + string result = template.GetForObject("user/{id}", "5"); + } + + [Test] + public void GetStringForMessage() + { + HttpResponseMessage result = template.GetForMessage("user/{id}", "1"); + Assert.AreEqual("Bruno Baïa", result.Body, "Invalid content"); + Assert.AreEqual(contentType, MediaType.ParseMediaType(result.Headers[HttpResponseHeader.ContentType]), "Invalid content-type"); + Assert.AreEqual(HttpStatusCode.OK, result.StatusCode, "Invalid status code"); + Assert.AreEqual("OK", result.StatusDescription, "Invalid status description"); + } + + [Test] + [ExpectedException(typeof(RestClientException), + ExpectedMessage = "Could not extract response: no Content-Type found")] + public void GetStringNoResponse() + { + string result = template.GetForObject("/nothing"); + } + + [Test] + public void HeadForHeaders() + { + WebHeaderCollection result = template.HeadForHeaders("head"); + Assert.AreEqual(new MediaType("text", "plain"), MediaType.ParseMediaType(result[HttpResponseHeader.ContentType]), "Invalid content-type"); + } + + [Test] + public void PostStringForLocation() + { + Uri result = template.PostForLocation("user", "Lisa Baia"); + Assert.AreEqual(new Uri(new Uri(uri), "/user/3"), result, "Invalid location"); + } + + [Test] + public void PostStringForMessage() + { + HttpResponseMessage result = template.PostForMessage("user", "Lisa Baia"); + Assert.AreEqual(new Uri(new Uri(uri), "/user/3"), new Uri(result.Headers[HttpResponseHeader.Location]), "Invalid location"); + Assert.AreEqual(HttpStatusCode.Created, result.StatusCode, "Invalid status code"); + Assert.AreEqual("User id '3' created with 'Lisa Baia'", result.StatusDescription, "Invalid status description"); + Assert.AreEqual("3", result.Body, "Invalid content"); + } + + [Test] + public void PostStringForObject() + { + string result = template.PostForObject("user", "Lisa Baia"); + Assert.AreEqual("3", result, "Invalid content"); + } + + [Test] + [ExpectedException(typeof(RestClientException), + ExpectedMessage = "The server returned 'Content cannot be null or empty' with the status code 400 - BadRequest.")] + public void PostStringForObjectWithError() + { + string result = template.PostForObject("user", ""); + } + + [Test] + public void Put() + { + string result = template.GetForObject("user/1"); + Assert.AreEqual("Bruno Baïa", result, "Invalid content"); + + template.Put("user/1", "Bruno Baia"); + + result = template.GetForObject("user/1"); + Assert.AreEqual("Bruno Baia", result, "Invalid content"); + } + + [Test] + [ExpectedException(typeof(RestClientException), + ExpectedMessage = "The server returned 'User id '4' does not exist' with the status code 400 - BadRequest.")] + public void PutWithError() + { + template.Put("user/4", "Dinora Baia"); + } + + [Test] + public void Delete() + { + string result = template.GetForObject("users"); + Assert.AreEqual("2", result, "Invalid content"); + + template.Delete("user/2"); + + result = template.GetForObject("users"); + Assert.AreEqual("1", result, "Invalid content"); + } + + [Test] + [ExpectedException(typeof(RestClientException), + ExpectedMessage = "The server returned 'User id '10' does not exist' with the status code 400 - BadRequest.")] + public void DeleteWithError() + { + template.Delete("user/10"); + } + + [Test] + public void OptionsForAllow() + { + IList result = template.OptionsForAllow("allow"); + Assert.AreEqual(3, result.Count, "Invalid response"); + Assert.IsTrue(result.Contains(HttpMethod.GET), "Invalid response"); + Assert.IsTrue(result.Contains(HttpMethod.HEAD), "Invalid response"); + Assert.IsTrue(result.Contains(HttpMethod.PUT), "Invalid response"); + } + + [Test] + public void ExchangePost() + { + HttpResponseMessage result = template.Exchange( + "user", new HttpRequestMessage("Maryse Baia", HttpMethod.POST)); + + Assert.AreEqual("3", result.Body, "Invalid content"); + Assert.AreEqual(HttpStatusCode.Created, result.StatusCode, "Invalid status code"); + Assert.AreEqual("User id '3' created with 'Maryse Baia'", result.StatusDescription, "Invalid status description"); + } + + [Test] + public void ExchangePut() + { + HttpResponseMessage result = template.Exchange( + "user/1", new HttpRequestMessage("Bruno Baia", HttpMethod.PUT)); + + Assert.AreEqual(HttpStatusCode.OK, result.StatusCode, "Invalid status code"); + Assert.AreEqual("User id '1' updated with 'Bruno Baia'", result.StatusDescription, "Invalid status description"); + } + + [Test] + [ExpectedException(ExpectedMessage = "The server returned 'Not Found' with the status code 404 - NotFound.")] + public void ClientError() + { + template.Execute("clienterror", null, null); + } + + [Test] + [ExpectedException(ExpectedMessage = "The server returned 'Internal Server Error' with the status code 500 - InternalServerError.")] + public void ServerError() + { + template.Execute("servererror", null, null); + } + + + //@BeforeClass + //public static void startJettyServer() throws Exception { + // jettyServer = new Server(8889); + // Context jettyContext = new Context(jettyServer, "/"); + // byte[] bytes = helloWorld.getBytes("UTF-8"); + // contentType = new MediaType("text", "plain", Collections.singletonMap("charset", "utf-8")); + // jettyContext.addServlet(new ServletHolder(new GetServlet(bytes, contentType)), "/get"); + // jettyContext.addServlet(new ServletHolder(new GetServlet(new byte[0], contentType)), "/get/nothing"); + // jettyContext.addServlet( + // new ServletHolder(new PostServlet(helloWorld, URI + "/post/1", bytes, contentType)), + // "/post"); + // jettyContext.addServlet(new ServletHolder(new ErrorServlet(404)), "/errors/notfound"); + // jettyContext.addServlet(new ServletHolder(new ErrorServlet(500)), "/errors/server"); + // jettyContext.addServlet(new ServletHolder(new UriServlet()), "/uri/*"); + // jettyContext.addServlet(new ServletHolder(new MultipartServlet()), "/multipart"); + // jettyServer.start(); + //} + + //@Test + //public void uri() throws InterruptedException, URISyntaxException { + // String result = template.getForObject(URI + "/uri/{query}", String.class, "Z\u00fcrich"); + // Assert.AreEqual("Invalid request URI", "/uri/Z%C3%BCrich", result); + + // result = template.getForObject(URI + "/uri/query={query}", String.class, "foo@bar"); + // Assert.AreEqual("Invalid request URI", "/uri/query=foo@bar", result); + + // result = template.getForObject(URI + "/uri/query={query}", String.class, "T\u014dky\u014d"); + // Assert.AreEqual("Invalid request URI", "/uri/query=T%C5%8Dky%C5%8D", result); + //} + + //@Test + //public void multipart() throws UnsupportedEncodingException { + // MultiValueMap parts = new LinkedMultiValueMap(); + // parts.add("name 1", "value 1"); + // parts.add("name 2", "value 2+1"); + // parts.add("name 2", "value 2+2"); + // Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg"); + // parts.add("logo", logo); + + // template.postForLocation(URI + "/multipart", parts); + //} + + + //private static class UriServlet extends HttpServlet { + + // @Override + // protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // resp.setContentType("text/plain"); + // resp.setCharacterEncoding("UTF-8"); + // resp.getWriter().write(req.getRequestURI()); + // } + //} + + //private static class MultipartServlet extends HttpServlet { + + // @Override + // protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // assertTrue(ServletFileUpload.isMultipartContent(req)); + // FileItemFactory factory = new DiskFileItemFactory(); + // ServletFileUpload upload = new ServletFileUpload(factory); + // try { + // List items = upload.parseRequest(req); + // Assert.AreEqual(4, items.size()); + // FileItem item = (FileItem) items.get(0); + // assertTrue(item.isFormField()); + // Assert.AreEqual("name 1", item.getFieldName()); + // Assert.AreEqual("value 1", item.getString()); + + // item = (FileItem) items.get(1); + // assertTrue(item.isFormField()); + // Assert.AreEqual("name 2", item.getFieldName()); + // Assert.AreEqual("value 2+1", item.getString()); + + // item = (FileItem) items.get(2); + // assertTrue(item.isFormField()); + // Assert.AreEqual("name 2", item.getFieldName()); + // Assert.AreEqual("value 2+2", item.getString()); + + // item = (FileItem) items.get(3); + // Assert.IsFalse(item.isFormField()); + // Assert.AreEqual("logo", item.getFieldName()); + // Assert.AreEqual("logo.jpg", item.getName()); + // Assert.AreEqual("image/jpeg", item.getContentType()); + // } + // catch (FileUploadException ex) { + // throw new ServletException(ex); + // } + + // } + //} + + #region REST test service + + [ServiceContract] + [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)] + public class TestService + { + private IDictionary users; + + public TestService() + { + users = new Dictionary(); + users.Add("1", "Bruno Baïa"); + users.Add("2", "Marie Baia"); + } + + [WebGet(UriTemplate = "clienterror")] + public void ClientError() + { + WebOperationContext.Current.OutgoingResponse.SetStatusAsNotFound(); + } + + [WebGet(UriTemplate = "servererror")] + public void ServerError() + { + WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.InternalServerError; + } + + [WebInvoke(UriTemplate = "allow", Method = "OPTIONS")] + public void Allow() + { + WebOperationContext.Current.OutgoingResponse.Headers[HttpResponseHeader.Allow] = "GET, HEAD, PUT"; + } + + [WebInvoke(UriTemplate = "head", Method = "HEAD")] + public void Head() + { + WebOperationContext.Current.OutgoingResponse.Headers[HttpResponseHeader.ContentType] = "text/plain"; + } + + [WebGet(UriTemplate = "user/{id}")] + public Message GetUser(string id) + { + WebOperationContext context = WebOperationContext.Current; + + if (!users.ContainsKey(id)) + { + context.OutgoingResponse.SetStatusAsNotFound(String.Format("User with id '{0}' not found", id)); + return context.CreateTextResponse(null); + } + + return context.CreateTextResponse(users[id]); + } + + [WebGet(UriTemplate = "users")] + public Message GetUsersCount() + { + WebOperationContext context = WebOperationContext.Current; + + return context.CreateTextResponse(users.Count.ToString()); + } + + [WebGet(UriTemplate = "nothing")] + public void GetNothing() + { + WebOperationContext.Current.OutgoingResponse.SuppressEntityBody = true; + } + + [WebInvoke(UriTemplate = "user", Method = "POST")] + public Message Post(Stream stream) + { + WebOperationContext context = WebOperationContext.Current; + + UriTemplateMatch match = context.IncomingRequest.UriTemplateMatch; + UriTemplate template = new UriTemplate("/user/{id}"); + + MediaType mediaType = MediaType.ParseMediaType(context.IncomingRequest.ContentType); + + string id = (users.Count + 1).ToString(); // generate new ID + string name; + using (StreamReader reader = new StreamReader(stream, Encoding.GetEncoding(mediaType.CharSet))) + { + name = reader.ReadToEnd(); + } + + if (String.IsNullOrEmpty(name)) + { + context.OutgoingResponse.StatusCode = HttpStatusCode.BadRequest; + context.OutgoingResponse.StatusDescription = "Content cannot be null or empty"; + return WebOperationContext.Current.CreateTextResponse(""); + } + + users.Add(id, name); + + Uri uri = template.BindByPosition(match.BaseUri, id); + context.OutgoingResponse.SetStatusAsCreated(uri); + context.OutgoingResponse.StatusDescription = String.Format("User id '{0}' created with '{1}'", id, name); + + return WebOperationContext.Current.CreateTextResponse(id); + } + + [WebInvoke(UriTemplate = "user/{id}", Method = "PUT")] + public void Update(string id, Stream stream) + { + WebOperationContext context = WebOperationContext.Current; + + if (!users.ContainsKey(id)) + { + context.OutgoingResponse.StatusCode = HttpStatusCode.BadRequest; + context.OutgoingResponse.StatusDescription = String.Format("User id '{0}' does not exist", id); + return; + } + + MediaType mediaType = MediaType.ParseMediaType(context.IncomingRequest.ContentType); + + string name; + using (StreamReader reader = new StreamReader(stream, Encoding.GetEncoding(mediaType.CharSet))) + { + name = reader.ReadToEnd(); + } + users[id] = name; + + context.OutgoingResponse.StatusCode = HttpStatusCode.OK; + context.OutgoingResponse.StatusDescription = String.Format("User id '{0}' updated with '{1}'", id, name); + //context.OutgoingResponse.ContentType = "text/plain"; + } + + [WebInvoke(UriTemplate = "user/{id}", Method = "DELETE")] + public void Delete(string id) + { + WebOperationContext context = WebOperationContext.Current; + + if (!users.ContainsKey(id)) + { + context.OutgoingResponse.StatusCode = HttpStatusCode.BadRequest; + context.OutgoingResponse.StatusDescription = String.Format("User id '{0}' does not exist", id); + return; + } + + users.Remove(id); + + context.OutgoingResponse.StatusCode = HttpStatusCode.OK; + context.OutgoingResponse.StatusDescription = String.Format("User id '{0}' have been removed", id); + } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/test/Spring/Spring.Http.Tests/Http/Rest/RestTemplateTests.cs b/test/Spring/Spring.Http.Tests/Http/Rest/RestTemplateTests.cs new file mode 100644 index 00000000..46e1144e --- /dev/null +++ b/test/Spring/Spring.Http.Tests/Http/Rest/RestTemplateTests.cs @@ -0,0 +1,643 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Net; +using System.Collections.Generic; + +using Spring.Http; +using Spring.Http.Converters; + +using NUnit.Framework; +using Rhino.Mocks; + +namespace Spring.Http.Rest +{ + /// + /// Unit tests for the RestTemplate class. + /// + /// Arjen Poutsma + /// Bruno Baia (.NET) + [TestFixture] + public class RestTemplateTests + { + #region Logging + + private static readonly Common.Logging.ILog LOG = Common.Logging.LogManager.GetLogger(typeof(RestTemplateTests)); + + #endregion + + private MockRepository mocks; + private RestTemplate template; + private IHttpWebRequestFactory requestFactory; + private HttpWebRequest request; + private HttpWebResponse response; + //private ResponseErrorHandler errorHandler; + private IHttpMessageConverter converter; + + [SetUp] + public void SetUp() + { + mocks = new MockRepository(); + requestFactory = mocks.CreateMock(); + request = mocks.CreateMock(); + response = mocks.CreateMock(); + //errorHandler = createMock(ResponseErrorHandler.class); + converter = mocks.CreateMock(); + + IList messageConverters = new List(1); + messageConverters.Add(converter); + + template = new RestTemplate(); + template.RequestFactory = requestFactory; + template.MessageConverters = messageConverters; + //template.setErrorHandler(errorHandler); + } + + [TearDown] + public void TearDown() + { + mocks.VerifyAll(); + } + + [Test] + public void VarArgsTemplateVariables() + { + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com/hotels/42/bookings/21"))) + .Return(request); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + + mocks.ReplayAll(); + + template.Execute("http://example.com/hotels/{hotel}/bookings/{booking}", null, null, "42", "21"); + } + + [Test] + public void DictionaryTemplateVariables() + { + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com/hotels/42/bookings/21"))) + .Return(request); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + + mocks.ReplayAll(); + + IDictionary variables = new Dictionary(); + variables.Add("booking", "41"); + variables.Add("hotel", "42"); + template.Execute("http://example.com/hotels/{hotel}/bookings/{booking}", null, null, "42", "21"); + } + + [Test] + public void BaseAddressTemplate() + { + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com/hotels/42/bookings/21"))) + .Return(request); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + + mocks.ReplayAll(); + + template.BaseAddress = new Uri("http://example.com"); + template.Execute("hotels/{hotel}/bookings/{booking}", null, null, "42", "21"); + } + + //[Test] + //public void errorHandling() { + // Expect.Call(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.GET)).andReturn(request); + // Expect.Call(request.execute()).andReturn(response); + // Expect.Call(errorHandler.hasError(response)).andReturn(true); + // Expect.Call(response.getStatusCode()).andReturn(HttpStatus.INTERNAL_SERVER_ERROR); + // Expect.Call(response.getStatusText()).andReturn("Internal Server Error"); + // errorHandler.handleError(response); + // expectLastCall().andThrow(new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR)); + // response.close(); + + // mocks.ReplayAll(); + + // try { + // template.execute("http://example.com", HttpMethod.GET, null, null); + // fail("HttpServerErrorException expected"); + // } + // catch (HttpServerErrorException ex) { + // // expected + // } + // mocks.ReplayAll(); + //} + + [Test] + public void GetForObject() + { + Expect.Call(converter.CanRead(typeof(string), null)).Return(true); + MediaType textPlain = new MediaType("text", "plain"); + IList mediaTypes = new List(1); + mediaTypes.Add(textPlain); + Expect.Call>(converter.SupportedMediaTypes).Return(mediaTypes); + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com"))).Return(request); + Expect.Call(request.Method = "GET"); + Expect.Call(request.Accept = "text/plain"); + WebHeaderCollection requestHeaders = new WebHeaderCollection(); + Expect.Call(request.Headers).Return(requestHeaders).Repeat.Any(); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + WebHeaderCollection responseHeaders = new WebHeaderCollection(); + responseHeaders[HttpResponseHeader.ContentType] = textPlain.ToString(); + Expect.Call(response.Headers).Return(responseHeaders).Repeat.Any(); + Expect.Call(converter.CanRead(typeof(string), textPlain)).Return(true); + String expected = "Hello World"; + Expect.Call(converter.Read(response)).Return(expected); + + mocks.ReplayAll(); + + string result = template.GetForObject("http://example.com"); + Assert.AreEqual(expected, result, "Invalid GET result"); + } + + [Test] + [ExpectedException(typeof(RestClientException), + ExpectedMessage = "Could not extract response: no suitable HttpMessageConverter found for response type [System.String] and content type [bar/baz]")] + public void GetUnsupportedMediaType() + { + Expect.Call(converter.CanRead(typeof(string), null)).Return(true); + MediaType textPlain = new MediaType("foo", "bar"); + IList mediaTypes = new List(1); + mediaTypes.Add(textPlain); + Expect.Call>(converter.SupportedMediaTypes).Return(mediaTypes); + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com/resource"))).Return(request); + Expect.Call(request.Method = "GET"); + Expect.Call(request.Accept = "foo/bar"); + WebHeaderCollection requestHeaders = new WebHeaderCollection(); + Expect.Call(request.Headers).Return(requestHeaders).Repeat.Any(); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + WebHeaderCollection responseHeaders = new WebHeaderCollection(); + MediaType contentType = new MediaType("bar", "baz"); + responseHeaders[HttpResponseHeader.ContentType] = contentType.ToString(); + Expect.Call(response.Headers).Return(responseHeaders).Repeat.Any(); + Expect.Call(converter.CanRead(typeof(string), contentType)).Return(false); + + mocks.ReplayAll(); + + template.GetForObject("http://example.com/{p}", "resource"); + } + + [Test] + public void GetForMessage() + { + Expect.Call(converter.CanRead(typeof(string), null)).Return(true); + MediaType textPlain = new MediaType("text", "plain"); + IList mediaTypes = new List(1); + mediaTypes.Add(textPlain); + Expect.Call>(converter.SupportedMediaTypes).Return(mediaTypes); + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com"))).Return(request); + Expect.Call(request.Method = "GET"); + Expect.Call(request.Accept = "text/plain"); + WebHeaderCollection requestHeaders = new WebHeaderCollection(); + Expect.Call(request.Headers).Return(requestHeaders).Repeat.Any(); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + WebHeaderCollection responseHeaders = new WebHeaderCollection(); + responseHeaders[HttpResponseHeader.ContentType] = textPlain.ToString(); + Expect.Call(response.Headers).Return(responseHeaders).Repeat.Any(); + Expect.Call(converter.CanRead(typeof(string), textPlain)).Return(true); + String expected = "Hello World"; + Expect.Call(converter.Read(response)).Return(expected); + Expect.Call(response.StatusCode).Return(HttpStatusCode.OK); + Expect.Call(response.StatusDescription).Return("OK"); + + mocks.ReplayAll(); + + HttpResponseMessage result = template.GetForMessage("http://example.com"); + Assert.AreEqual(expected, result.Body, "Invalid GET result"); + Assert.AreEqual(textPlain.ToString(), result.Headers[HttpResponseHeader.ContentType], "Invalid Content-Type header"); + Assert.AreEqual(HttpStatusCode.OK, result.StatusCode, "Invalid status code"); + Assert.AreEqual("OK", result.StatusDescription, "Invalid status description"); + + mocks.ReplayAll(); + } + + [Test] + public void HeadForHeaders() + { + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com"))).Return(request); + Expect.Call(request.Method = "HEAD"); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + WebHeaderCollection responseHeaders = new WebHeaderCollection(); + Expect.Call(response.Headers).Return(responseHeaders).Repeat.Any(); + + mocks.ReplayAll(); + + WebHeaderCollection result = template.HeadForHeaders("http://example.com"); + + Assert.AreSame(responseHeaders, result, "Invalid headers returned"); + } + + [Test] + public void PostForLocation() + { + string helloWorld = "Hello World"; + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com"))).Return(request); + Expect.Call(request.Method = "POST"); + Expect.Call(converter.CanWrite(typeof(string), null)).Return(true); + converter.Write(helloWorld, null, request); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + WebHeaderCollection responseHeaders = new WebHeaderCollection(); + Uri expected = new Uri("http://example.com/hotels"); + responseHeaders[HttpResponseHeader.Location] = expected.ToString(); + Expect.Call(response.Headers).Return(responseHeaders).Repeat.Any(); + + mocks.ReplayAll(); + + Uri result = template.PostForLocation("http://example.com", helloWorld); + Assert.AreEqual(expected, result, "Invalid POST result"); + } + + [Test] + public void PostForLocationMessageContentType() + { + string helloWorld = "Hello World"; + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com"))).Return(request); + Expect.Call(request.Method = "POST"); + MediaType contentType = new MediaType("text", "plain"); + Expect.Call(converter.CanWrite(typeof(string), contentType)).Return(true); + WebHeaderCollection requestHeaders = new WebHeaderCollection(); + Expect.Call(request.Headers).Return(requestHeaders).Repeat.Any(); + converter.Write(helloWorld, contentType, request); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + WebHeaderCollection responseHeaders = new WebHeaderCollection(); + Uri expected = new Uri("http://example.com/hotels"); + responseHeaders[HttpResponseHeader.Location] = expected.ToString(); + Expect.Call(response.Headers).Return(responseHeaders).Repeat.Any(); + + mocks.ReplayAll(); + + WebHeaderCollection requestMessageHeaders = new WebHeaderCollection(); + requestMessageHeaders[HttpRequestHeader.ContentType] = contentType.ToString(); + HttpRequestMessage requestMessage = new HttpRequestMessage(helloWorld, requestMessageHeaders); + + Uri result = template.PostForLocation("http://example.com", requestMessage); + Assert.AreEqual(expected, result, "Invalid POST result"); + } + + [Test] + public void PostForLocationEntityCustomHeader() + { + string helloWorld = "Hello World"; + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com"))).Return(request); + Expect.Call(request.Method = "POST"); + Expect.Call(converter.CanWrite(typeof(string), null)).Return(true); + WebHeaderCollection requestHeaders = new WebHeaderCollection(); + Expect.Call(request.Headers).Return(requestHeaders).Repeat.Any(); + converter.Write(helloWorld, null, request); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + WebHeaderCollection responseHeaders = new WebHeaderCollection(); + Uri expected = new Uri("http://example.com/hotels"); + responseHeaders[HttpResponseHeader.Location] = expected.ToString(); + Expect.Call(response.Headers).Return(responseHeaders).Repeat.Any(); + + mocks.ReplayAll(); + + WebHeaderCollection requestMessageHeaders = new WebHeaderCollection(); + requestMessageHeaders.Add("MyHeader", "MyValue"); + HttpRequestMessage requestMessage = new HttpRequestMessage(helloWorld, requestMessageHeaders); + + Uri result = template.PostForLocation("http://example.com", requestMessage); + Assert.AreEqual(expected, result, "Invalid POST result"); + Assert.AreEqual("MyValue", requestHeaders.Get("MyHeader"), "No custom header set"); + } + + [Test] + public void PostForLocationNoLocation() + { + string helloWorld = "Hello World"; + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com"))).Return(request); + Expect.Call(request.Method = "POST"); + Expect.Call(converter.CanWrite(typeof(string), null)).Return(true); + converter.Write(helloWorld, null, request); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + WebHeaderCollection responseHeaders = new WebHeaderCollection(); + Expect.Call(response.Headers).Return(responseHeaders).Repeat.Any(); + + mocks.ReplayAll(); + + Uri result = template.PostForLocation("http://example.com", helloWorld); + Assert.IsNull(result, "Invalid POST result"); + + mocks.ReplayAll(); + } + + [Test] + public void PostForLocationNull() + { + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com"))).Return(request); + Expect.Call(request.Method = "POST"); + WebHeaderCollection requestHeaders = new WebHeaderCollection(); + Expect.Call(request.Headers).Return(requestHeaders).Repeat.Any(); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + WebHeaderCollection responseHeaders = new WebHeaderCollection(); + Expect.Call(response.Headers).Return(responseHeaders).Repeat.Any(); + + mocks.ReplayAll(); + + template.PostForLocation("http://example.com", null); + Assert.IsNull(requestHeaders[HttpRequestHeader.ContentLength], "Invalid content length"); + } + + [Test] + public void PostForObject() + { + Expect.Call(converter.CanRead(typeof(Version), null)).Return(true); + MediaType textPlain = new MediaType("text", "plain"); + IList mediaTypes = new List(1); + mediaTypes.Add(textPlain); + Expect.Call>(converter.SupportedMediaTypes).Return(mediaTypes); + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com"))).Return(request); + Expect.Call(request.Method = "POST"); + Expect.Call(request.Accept = "text/plain"); + WebHeaderCollection requestHeaders = new WebHeaderCollection(); + Expect.Call(request.Headers).Return(requestHeaders).Repeat.Any(); + string helloWorld = "Hello World"; + Expect.Call(converter.CanWrite(typeof(string), null)).Return(true); + converter.Write(helloWorld, null, request); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + WebHeaderCollection responseHeaders = new WebHeaderCollection(); + responseHeaders[HttpResponseHeader.ContentType] = textPlain.ToString(); + Expect.Call(response.Headers).Return(responseHeaders).Repeat.Any(); + Version expected = new Version(1, 0); + Expect.Call(converter.CanRead(typeof(Version), textPlain)).Return(true); + Expect.Call(converter.Read(response)).Return(expected); + + mocks.ReplayAll(); + + Version result = template.PostForObject("http://example.com", helloWorld); + Assert.AreEqual(expected, result, "Invalid POST result"); + } + + [Test] + public void PostForEntity() + { + Expect.Call(converter.CanRead(typeof(Version), null)).Return(true); + MediaType textPlain = new MediaType("text", "plain"); + IList mediaTypes = new List(1); + mediaTypes.Add(textPlain); + Expect.Call>(converter.SupportedMediaTypes).Return(mediaTypes); + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com"))).Return(request); + Expect.Call(request.Method = "POST"); + Expect.Call(request.Accept = "text/plain"); + WebHeaderCollection requestHeaders = new WebHeaderCollection(); + Expect.Call(request.Headers).Return(requestHeaders).Repeat.Any(); + string helloWorld = "Hello World"; + Expect.Call(converter.CanWrite(typeof(string), null)).Return(true); + converter.Write(helloWorld, null, request); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + WebHeaderCollection responseHeaders = new WebHeaderCollection(); + responseHeaders[HttpResponseHeader.ContentType] = textPlain.ToString(); + Expect.Call(response.Headers).Return(responseHeaders).Repeat.Any(); + Version expected = new Version(1, 0); + Expect.Call(converter.CanRead(typeof(Version), textPlain)).Return(true); + Expect.Call(converter.Read(response)).Return(expected); + Expect.Call(response.StatusCode).Return(HttpStatusCode.OK); + Expect.Call(response.StatusDescription).Return("OK"); + + mocks.ReplayAll(); + + HttpResponseMessage result = template.PostForMessage("http://example.com", helloWorld); + Assert.AreEqual(expected, result.Body, "Invalid POST result"); + Assert.AreEqual(textPlain, MediaType.ParseMediaType(result.Headers[HttpResponseHeader.ContentType]), "Invalid Content-Type"); + Assert.AreEqual(HttpStatusCode.OK, result.StatusCode, "Invalid status code"); + Assert.AreEqual("OK", result.StatusDescription, "Invalid status description"); + } + + [Test] + public void PostForObjectNull() + { + Expect.Call(converter.CanRead(typeof(Version), null)).Return(true); + MediaType textPlain = new MediaType("text", "plain"); + IList mediaTypes = new List(1); + mediaTypes.Add(textPlain); + Expect.Call>(converter.SupportedMediaTypes).Return(mediaTypes); + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com"))).Return(request); + Expect.Call(request.Method = "POST"); + Expect.Call(request.Accept = "text/plain"); + WebHeaderCollection requestHeaders = new WebHeaderCollection(); + Expect.Call(request.Headers).Return(requestHeaders).Repeat.Any(); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + WebHeaderCollection responseHeaders = new WebHeaderCollection(); + responseHeaders[HttpResponseHeader.ContentType] = textPlain.ToString(); + Expect.Call(response.Headers).Return(responseHeaders).Repeat.Any(); + Expect.Call(converter.CanRead(typeof(Version), textPlain)).Return(true); + Expect.Call(converter.Read(response)).Return(null); + + mocks.ReplayAll(); + + Version result = template.PostForObject("http://example.com", null); + Assert.IsNull(result, "Invalid POST result"); + Assert.IsNull(requestHeaders[HttpRequestHeader.ContentLength], "Invalid content length"); + } + + [Test] + public void PostForEntityNull() + { + Expect.Call(converter.CanRead(typeof(Version), null)).Return(true); + MediaType textPlain = new MediaType("text", "plain"); + IList mediaTypes = new List(1); + mediaTypes.Add(textPlain); + Expect.Call>(converter.SupportedMediaTypes).Return(mediaTypes); + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com"))).Return(request); + Expect.Call(request.Method = "POST"); + Expect.Call(request.Accept = "text/plain"); + WebHeaderCollection requestHeaders = new WebHeaderCollection(); + Expect.Call(request.Headers).Return(requestHeaders).Repeat.Any(); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + WebHeaderCollection responseHeaders = new WebHeaderCollection(); + responseHeaders[HttpResponseHeader.ContentType] = textPlain.ToString(); + Expect.Call(response.Headers).Return(responseHeaders).Repeat.Any(); + Expect.Call(converter.CanRead(typeof(Version), textPlain)).Return(true); + Expect.Call(converter.Read(response)).Return(null); + Expect.Call(response.StatusCode).Return(HttpStatusCode.OK); + Expect.Call(response.StatusDescription).Return("OK"); + + mocks.ReplayAll(); + + HttpResponseMessage result = template.PostForMessage("http://example.com", null); + Assert.IsNull(result.Body, "Invalid POST result"); + Assert.AreEqual(textPlain, MediaType.ParseMediaType(result.Headers[HttpResponseHeader.ContentType]), "Invalid Content-Type"); + Assert.IsNull(requestHeaders[HttpRequestHeader.ContentLength], "Invalid content length"); + Assert.AreEqual(HttpStatusCode.OK, result.StatusCode, "Invalid status code"); + Assert.AreEqual("OK", result.StatusDescription, "Invalid status description"); + } + + [Test] + public void Put() + { + Expect.Call(converter.CanWrite(typeof(string), null)).Return(true); + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com"))).Return(request); + Expect.Call(request.Method = "PUT"); + string helloWorld = "Hello World"; + converter.Write(helloWorld, null, request); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + + mocks.ReplayAll(); + + template.Put("http://example.com", helloWorld); + } + + [Test] + public void PutNull() + { + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com"))).Return(request); + Expect.Call(request.Method = "PUT"); + WebHeaderCollection requestHeaders = new WebHeaderCollection(); + Expect.Call(request.Headers).Return(requestHeaders).Repeat.Any(); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + + mocks.ReplayAll(); + + template.Put("http://example.com", null); + + Assert.IsNull(requestHeaders[HttpRequestHeader.ContentLength], "Invalid content length"); + } + + [Test] + public void Delete() + { + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com"))).Return(request); + Expect.Call(request.Method = "DELETE"); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + + mocks.ReplayAll(); + + template.Delete("http://example.com"); + } + + [Test] + public void OptionsForAllow() + { + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com"))).Return(request); + Expect.Call(request.Method = "OPTIONS"); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + WebHeaderCollection responseHeaders = new WebHeaderCollection(); + responseHeaders[HttpResponseHeader.Allow] = "GET,POST"; + Expect.Call(response.Headers).Return(responseHeaders).Repeat.Any(); + + mocks.ReplayAll(); + + IList result = template.OptionsForAllow("http://example.com"); + Assert.AreEqual(2, result.Count, "Invalid OPTIONS result"); + Assert.IsTrue(result.Contains(HttpMethod.GET), "Invalid OPTIONS result"); + Assert.IsTrue(result.Contains(HttpMethod.POST), "Invalid OPTIONS result"); + } + + //[Test] + //public void ioException() { + // Expect.Call(converter.canRead(String.class, null)).andReturn(true); + // MediaType mediaType = new MediaType("foo", "bar"); + // Expect.Call(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(mediaType)); + // Expect.Call(requestFactory.createRequest(new URI("http://example.com/resource"), HttpMethod.GET)).andReturn(request); + // Expect.Call(request.getHeaders()).andReturn(new HttpHeaders()); + // Expect.Call(request.execute()).andThrow(new IOException()); + + // mocks.ReplayAll(); + + // try { + // template.getForObject("http://example.com/resource", String.class); + // fail("RestClientException expected"); + // } + // catch (ResourceAccessException ex) { + // // expected + // } + + // mocks.ReplayAll(); + //} + + [Test] + public void Exchange() + { + Expect.Call(converter.CanRead(typeof(Version), null)).Return(true); + MediaType textPlain = new MediaType("text", "plain"); + IList mediaTypes = new List(1); + mediaTypes.Add(textPlain); + Expect.Call>(converter.SupportedMediaTypes).Return(mediaTypes); + Expect.Call(requestFactory.CreateRequest(new Uri("http://example.com"))).Return(request); + Expect.Call(request.Method = "POST"); + Expect.Call(request.Accept = "text/plain"); + WebHeaderCollection requestHeaders = new WebHeaderCollection(); + Expect.Call(request.Headers).Return(requestHeaders).Repeat.Any(); + string helloWorld = "Hello World"; + Expect.Call(converter.CanWrite(typeof(string), null)).Return(true); + converter.Write(helloWorld, null, request); + ExpectGetResponse(); + //Expect.Call(errorHandler.hasError(response)).andReturn(false); + WebHeaderCollection responseHeaders = new WebHeaderCollection(); + responseHeaders[HttpResponseHeader.ContentType] = textPlain.ToString(); + Expect.Call(response.Headers).Return(responseHeaders).Repeat.Any(); + Version expected = new Version(1, 0); + Expect.Call(converter.CanRead(typeof(Version), textPlain)).Return(true); + Expect.Call(converter.Read(response)).Return(expected); + Expect.Call(response.StatusCode).Return(HttpStatusCode.OK); + Expect.Call(response.StatusDescription).Return("OK"); + + mocks.ReplayAll(); + + WebHeaderCollection requestMessageHeaders = new WebHeaderCollection(); + requestMessageHeaders.Add("MyHeader", "MyValue"); + HttpRequestMessage requestMessage = new HttpRequestMessage(helloWorld, requestMessageHeaders, HttpMethod.POST); + HttpResponseMessage result = template.Exchange("http://example.com", requestMessage); + Assert.AreEqual(expected, result.Body, "Invalid POST result"); + Assert.AreEqual(textPlain, MediaType.ParseMediaType(result.Headers[HttpResponseHeader.ContentType]), "Invalid Content-Type"); + Assert.AreEqual("MyValue", requestHeaders.Get("MyHeader"), "No custom header set"); + Assert.AreEqual(HttpStatusCode.OK, result.StatusCode, "Invalid status code"); + Assert.AreEqual("OK", result.StatusDescription, "Invalid status description"); + } + + #region Utility methods + + private void ExpectGetResponse() + { + Expect.Call(request.GetResponse()).Return(response); + #region Instrumentation + if (LOG.IsDebugEnabled) + { + Expect.Call(response.StatusCode).Return(HttpStatusCode.OK); + Expect.Call(response.StatusDescription).Return("OK"); + } + #endregion + Expect.Call(response.Close); + Expect.Call(request.HaveResponse).Return(true); + } + + #endregion + } +} diff --git a/test/Spring/Spring.Http.Tests/Spring.Http.Tests.2008.csproj b/test/Spring/Spring.Http.Tests/Spring.Http.Tests.2008.csproj new file mode 100644 index 00000000..f9237789 --- /dev/null +++ b/test/Spring/Spring.Http.Tests/Spring.Http.Tests.2008.csproj @@ -0,0 +1,125 @@ + + + Local + 9.0.30729 + 2.0 + {F04CEE18-3897-A399-46BF-459437475B21} + Debug + AnyCPU + + + + + Spring.Http.Tests + + + JScript + Grid + IE50 + false + Library + Spring + OnBuildSuccess + + + v3.5 + + + ..\..\..\build\VS.Net.2008\Spring.Http.Tests\Debug\ + false + 285212672 + false + + + TRACE;DEBUG;NET_2_0;NET_3_0;NET_3_5 + + + true + 4096 + false + + + false + false + false + true + 4 + full + prompt + true + + + ..\..\..\build\VS.Net.2008\Spring.Http.Tests\Release\ + false + 285212672 + false + + + TRACE;NET_2_0;NET_3_0;NET_3_5 + + + false + 4096 + false + + + true + false + false + false + 4 + none + prompt + + + + False + ..\..\..\lib\Net\2.0\Common.Logging.dll + + + nunit.framework + ..\..\..\lib\Net\2.0\nunit.framework.dll + + + False + ..\..\..\lib\Net\2.0\Rhino.Mocks.dll + + + System + + + + + + + + + + + + + + + + + + {710961A3-0DF4-49E4-A26E-F5B9C044AC84} + Spring.Core.2008 + + + {FAC04F79-4B1F-1D13-B30F-00EE04EC0FBC} + Spring.Http.2008 + + + + + + + + + + echo "Copying .xml files for tests" +xcopy "$(ProjectDir)Data" ..\..\..\..\build\VS.Net.2008\Spring.Http.Tests\$(ConfigurationName)\ /y /s /q /d +xcopy "$(ProjectDir)$(TargetFileName).config" ..\..\..\..\build\VS.Net.2008\Spring.Http.Tests\$(ConfigurationName)\ /y /s /q + + \ No newline at end of file diff --git a/test/Spring/Spring.Http.Tests/Spring.Http.Tests.2010.csproj b/test/Spring/Spring.Http.Tests/Spring.Http.Tests.2010.csproj new file mode 100644 index 00000000..06cc735e --- /dev/null +++ b/test/Spring/Spring.Http.Tests/Spring.Http.Tests.2010.csproj @@ -0,0 +1,151 @@ + + + + Local + 9.0.30729 + 2.0 + {4594CEE7-3897-A3BF-9946-5B4374F01821} + Debug + AnyCPU + + + + + Spring.Http.Tests + + + JScript + Grid + IE50 + false + Library + Spring + OnBuildSuccess + + + v4.0 + + + 3.5 + + + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + + ..\..\..\build\VS.Net.2010\Spring.Http.Tests\Debug\ + false + 285212672 + false + + + TRACE;DEBUG;NET_2_0;NET_3_0;NET_3_5;NET_4_0 + + + true + 4096 + false + + + false + false + false + true + 4 + full + prompt + true + + + ..\..\..\build\VS.Net.2010\Spring.Http.Tests\Release\ + false + 285212672 + false + + + TRACE;NET_2_0;NET_3_0;NET_3_5 + + + false + 4096 + false + + + true + false + false + false + 4 + none + prompt + + + + False + ..\..\..\lib\Net\2.0\Common.Logging.dll + + + nunit.framework + ..\..\..\lib\Net\2.0\nunit.framework.dll + + + False + ..\..\..\lib\Net\2.0\Rhino.Mocks.dll + + + System + + + + + + + + + Code + + + + + + + + + + + + + + {710961A3-0DF4-49E4-A26E-F5B9C044AC84} + Spring.Core.2010 + + + {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Spring.Http.2010 + + + + + + + + + + echo "Copying .xml files for tests" +xcopy "$(ProjectDir)Data" ..\..\..\..\build\VS.Net.2010\Spring.Http.Tests\$(ConfigurationName)\ /y /s /q /d +xcopy "$(ProjectDir)$(TargetFileName).config" ..\..\..\..\build\VS.Net.2010\Spring.Http.Tests\$(ConfigurationName)\ /y /s /q + + \ No newline at end of file diff --git a/test/Spring/Spring.Http.Tests/Spring.Http.Tests.dll.config b/test/Spring/Spring.Http.Tests/Spring.Http.Tests.dll.config new file mode 100644 index 00000000..99706f2b --- /dev/null +++ b/test/Spring/Spring.Http.Tests/Spring.Http.Tests.dll.config @@ -0,0 +1,18 @@ + + + + + +
+ + + + + + + + + + + + diff --git a/test/Spring/Spring.Http.Tests/Util/UriTemplateTests.cs b/test/Spring/Spring.Http.Tests/Util/UriTemplateTests.cs new file mode 100644 index 00000000..abb4dfc7 --- /dev/null +++ b/test/Spring/Spring.Http.Tests/Util/UriTemplateTests.cs @@ -0,0 +1,227 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Collections.Generic; + +using NUnit.Framework; + +namespace Spring.Util +{ + /// + /// Unit tests for the UriTemplate class. + /// + /// Arjen Poutsma + /// Juergen Hoeller + /// Bruno Baia (.NET) + [TestFixture] + public class UriTemplateTests + { + [Test] + public void GetVariableNames() + { + UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); + string[] variableNames = template.VariableNames; + Assert.AreEqual(new string[] { "hotel", "booking" }, variableNames, "Invalid variable names"); + } + + [Test] + public void ExpandVarArgs() + { + // absolute + UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); + Uri result = template.Expand("1", "42"); + Assert.AreEqual(new Uri("http://example.com/hotels/1/bookings/42"), result, "Invalid expanded template"); + + // relative + template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); + result = template.Expand("1", "42"); + Assert.AreEqual(new Uri("/hotels/1/bookings/42", UriKind.Relative), result, "Invalid expanded template"); + } + + [Test] + public void MultipleExpandVarArgs() + { + UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); + Uri result = template.Expand("2", "21"); + result = template.Expand("1", "42"); + Assert.AreEqual(new Uri("http://example.com/hotels/1/bookings/42"), result, "Invalid expanded template"); + } + + [Test] + [ExpectedException( + typeof(ArgumentException), + ExpectedMessage = "Invalid amount of variables values in 'http://example.com/hotels/{hotel}/bookings/{booking}': expected 2; got 3")] + public void ExpandVarArgsInvalidAmountVariables() + { + UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); + template.Expand("1", "42", "100"); + } + + [Test] + public void ExpandVarArgsDuplicateVariables() + { + UriTemplate template = new UriTemplate("http://example.com/order/{c}/{c}/{c}"); + Assert.AreEqual(new string[] { "c" }, template.VariableNames, "Invalid variable names"); + Uri result = template.Expand("cheeseburger"); + Assert.AreEqual(new Uri("http://example.com/order/cheeseburger/cheeseburger/cheeseburger"), result, "Invalid expanded template"); + } + + [Test] + public void ExpandDictionary() + { + IDictionary uriVariables = new Dictionary(2); + uriVariables.Add("booking", "42"); + uriVariables.Add("hotel", "1"); + + // absolute + UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); + Uri result = template.Expand(uriVariables); + Assert.AreEqual(new Uri("http://example.com/hotels/1/bookings/42"), result, "Invalid expanded template"); + + // relative + template = new UriTemplate("hotels/{hotel}/bookings/{booking}"); + result = template.Expand(uriVariables); + Assert.AreEqual(new Uri("hotels/1/bookings/42", UriKind.Relative), result, "Invalid expanded template"); + } + + [Test] + public void MultipleExpandDictionary() + { + UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); + + IDictionary uriVariables = new Dictionary(2); + uriVariables.Add("booking", "21"); + uriVariables.Add("hotel", "2"); + Uri result = template.Expand(uriVariables); + + uriVariables = new Dictionary(2); + uriVariables.Add("booking", "42"); + uriVariables.Add("hotel", "1"); + result = template.Expand(uriVariables); + + Assert.AreEqual(new Uri("http://example.com/hotels/1/bookings/42"), result, "Invalid expanded template"); + } + + [Test] + [ExpectedException( + typeof(ArgumentException), + ExpectedMessage = "Invalid amount of variables values in 'http://example.com/hotels/{hotel}/bookings/{booking}': expected 2; got 1")] + public void ExpandDictionaryInvalidAmountVariables() + { + IDictionary uriVariables = new Dictionary(2); + uriVariables.Add("hotel", "1"); + + UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); + template.Expand(uriVariables); + } + + [Test] + [ExpectedException( + typeof(ArgumentException), + ExpectedMessage = "'uriVariables' dictionary has no value for 'hotel'")] + public void ExpandDictionaryUnboundVariables() + { + IDictionary uriVariables = new Dictionary(2); + uriVariables.Add("booking", "42"); + uriVariables.Add("bar", "1"); + + UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); + template.Expand(uriVariables); + } + + [Test] + public void ExpandEncoded() + { + UriTemplate template = new UriTemplate("http://example.com/hotel list/{hotel}"); + Uri result = template.Expand("Z\u00fcrich"); + Assert.AreEqual(new Uri("http://example.com/hotel%20list/Z%C3%BCrich"), result, "Invalid expanded template"); + } + + [Test] + public void Matches() + { + UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}/"); + Assert.IsTrue(template.Matches("http://example.com/hotels/1/bookings/42/"), "UriTemplate does not match"); + Assert.IsFalse(template.Matches("hhhhttp://example.com/hotels/1/bookings/42/"), "UriTemplate matches"); + Assert.IsFalse(template.Matches("http://example.com/hotels/1/bookings/42/blabla"), "UriTemplate matches"); + Assert.IsFalse(template.Matches("http://example.com/hotels/bookings/"), "UriTemplate matches"); + Assert.IsFalse(template.Matches(""), "UriTemplate matches"); + Assert.IsFalse(template.Matches(null), "UriTemplate matches"); + } + + [Test] + public void Match() + { + UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); + IDictionary result = template.Match("http://example.com/hotels/1/bookings/42"); + Assert.AreEqual(2, result.Count); + Assert.AreEqual("1", result["hotel"]); + Assert.AreEqual("42", result["booking"]); + + result = template.Match("http://example.com/hotels/1/bookings"); + Assert.AreEqual(0, result.Count); + } + + [Test] + public void matchDuplicate() + { + UriTemplate template = new UriTemplate("/order/{c}/{c}/{c}"); + IDictionary result = template.Match("/order/cheeseburger/cheeseburger/cheeseburger"); + Assert.AreEqual(1, result.Count); + Assert.AreEqual("cheeseburger", result["c"]); + } + + [Test] + public void MatchMultipleInOneSegment() + { + UriTemplate template = new UriTemplate("/{foo}-{bar}"); + IDictionary result = template.Match("/12-34"); + Assert.AreEqual(2, result.Count); + Assert.AreEqual("12", result["foo"]); + Assert.AreEqual("34", result["bar"]); + } + + [Test] + public void MatchesQueryVariables() + { + UriTemplate template = new UriTemplate("/search?q={query}"); + Assert.IsTrue(template.Matches("/search?q=foo")); + } + + [Test] + public void MatchesFragments() + { + UriTemplate template = new UriTemplate("/search#{fragment}"); + Assert.IsTrue(template.Matches("/search#foo")); + + template = new UriTemplate("/search?query={query}#{fragment}"); + Assert.IsTrue(template.Matches("/search?query=foo#bar")); + } + + [Test] + public void ExpandWithAtSign() + { + UriTemplate template = new UriTemplate("http://localhost/query={query}"); + Uri uri = template.Expand("foo@bar"); + Assert.AreEqual("http://localhost/query=foo@bar", uri.ToString()); + } + } +}