| using Lucene.Net.Support; |
| using Newtonsoft.Json; |
| using Newtonsoft.Json.Linq; |
| using System; |
| using System.IO; |
| using System.Linq; |
| using System.Net; |
| using System.Net.Http; |
| using System.Text; |
| |
| namespace Lucene.Net.Replicator.Http |
| { |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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. |
| */ |
| |
| /// <summary> |
| /// Base class for Http clients. |
| /// </summary> |
| /// <remarks> |
| /// @lucene.experimental |
| /// </remarks> |
| public abstract class HttpClientBase : IDisposable |
| { |
| /// <summary> |
| /// Default connection timeout for this client, in milliseconds. |
| /// <see cref="ConnectionTimeout"/> |
| /// </summary> |
| public const int DEFAULT_CONNECTION_TIMEOUT = 1000; |
| |
| // TODO compression? |
| |
| /// <summary> |
| /// The URL to execute requests against. |
| /// </summary> |
| protected string Url { get; private set; } |
| |
| private volatile bool isDisposed = false; |
| private readonly HttpClient httpc; |
| |
| /// <summary> |
| /// Creates a new <see cref="HttpClientBase"/> with the given host, port and path. |
| /// </summary> |
| /// <remarks> |
| /// The host, port and path parameters are normalized to <c>http://{host}:{port}{path}</c>, |
| /// if path is <c>null</c> or <c>empty</c> it defaults to <c>/</c>. |
| /// <para/> |
| /// A <see cref="HttpMessageHandler"/> is taken as an optional parameter as well, if this is not provided it defaults to <c>null</c>. |
| /// In this case the internal <see cref="HttpClient"/> will default to use a <see cref="HttpClientHandler"/>. |
| /// </remarks> |
| /// <param name="host">The host that the client should retrieve data from.</param> |
| /// <param name="port">The port to be used to connect on.</param> |
| /// <param name="path">The path to the replicator on the host.</param> |
| /// <param name="messageHandler">Optional, The HTTP handler stack to use for sending requests, defaults to <c>null</c>.</param> |
| protected HttpClientBase(string host, int port, string path, HttpMessageHandler messageHandler = null) |
| : this(NormalizedUrl(host, port, path), messageHandler) |
| { |
| } |
| |
| /// <summary> |
| /// Creates a new <see cref="HttpClientBase"/> with the given <paramref name="url"/>. |
| /// </summary> |
| /// <remarks> |
| /// A <see cref="HttpMessageHandler"/> is taken as an optional parameter as well, if this is not provided it defaults to <c>null</c>. |
| /// In this case the internal <see cref="HttpClient"/> will default to use a <see cref="HttpClientHandler"/>. |
| /// </remarks> |
| /// <param name="url">The full url, including with host, port and path.</param> |
| /// <param name="messageHandler">Optional, The HTTP handler stack to use for sending requests.</param> |
| //Note: LUCENENET Specific |
| protected HttpClientBase(string url, HttpMessageHandler messageHandler = null) |
| : this(url, new HttpClient(messageHandler ?? new HttpClientHandler()) { Timeout = TimeSpan.FromMilliseconds(DEFAULT_CONNECTION_TIMEOUT) }) |
| { |
| } |
| |
| /// <summary> |
| /// Creates a new <see cref="HttpClientBase"/> with the given <paramref name="url"/> and <see cref="HttpClient"/>. |
| /// </summary> |
| /// <remarks> |
| /// This allows full controll over how the <see cref="HttpClient"/> is created, |
| /// prefer the <see cref="HttpClientBase(string, HttpMessageHandler)"/> over this unless you know you need the control of the <see cref="HttpClient"/>. |
| /// </remarks> |
| /// <param name="url"></param> |
| /// <param name="client">The <see cref="HttpClient"/> to use make remote http calls.</param> |
| //Note: LUCENENET Specific |
| protected HttpClientBase(string url, HttpClient client) |
| { |
| Url = url; |
| httpc = client; |
| ConnectionTimeout = DEFAULT_CONNECTION_TIMEOUT; |
| } |
| |
| /// <summary> |
| /// Gets or Sets the connection timeout for this client, in milliseconds. This setting |
| /// is used to modify <see cref="HttpClient.Timeout"/>. |
| /// </summary> |
| public virtual int ConnectionTimeout |
| { |
| get => (int)httpc.Timeout.TotalMilliseconds; |
| set => httpc.Timeout = TimeSpan.FromMilliseconds(value); |
| } |
| |
| /// <summary> |
| /// Throws <see cref="ObjectDisposedException"/> if this client is already disposed. |
| /// </summary> |
| /// <exception cref="ObjectDisposedException">client is already disposed.</exception> |
| protected void EnsureOpen() |
| { |
| if (IsDisposed) |
| { |
| throw new ObjectDisposedException("HttpClient already disposed"); |
| } |
| } |
| |
| /// <summary> |
| /// Create a URL out of the given parameters, translate an empty/null path to '/' |
| /// </summary> |
| private static string NormalizedUrl(string host, int port, string path) |
| { |
| if (string.IsNullOrEmpty(path)) |
| path = "/"; |
| return string.Format("http://{0}:{1}{2}", host, port, path); |
| } |
| |
| /// <summary> |
| /// <b>Internal:</b> Verifies the response status and if not successful throws an exception. |
| /// </summary> |
| /// <exception cref="IOException">IO Error happened at the server, check inner exception for details.</exception> |
| /// <exception cref="HttpRequestException">Unknown error received from the server.</exception> |
| protected virtual void VerifyStatus(HttpResponseMessage response) |
| { |
| if (!response.IsSuccessStatusCode) |
| { |
| ThrowKnownError(response); |
| } |
| } |
| |
| /// <summary> |
| /// Throws an exception for any errors. |
| /// </summary> |
| /// <exception cref="IOException">IO Error happened at the server, check inner exception for details.</exception> |
| /// <exception cref="HttpRequestException">Unknown error received from the server.</exception> |
| protected virtual void ThrowKnownError(HttpResponseMessage response) |
| { |
| Stream input; |
| try |
| { |
| //.NET Note: Bridging from Async to Sync, this is not ideal and we could consider changing the interface to be Async or provide Async overloads |
| // and have these Sync methods with their caveats. |
| input = response.Content.ReadAsStreamAsync().ConfigureAwait(false).GetAwaiter().GetResult(); |
| } |
| catch (Exception) |
| { |
| // the response stream is not an exception - could be an error in servlet.init(). |
| response.EnsureSuccessStatusCode(); |
| //Note: This is unreachable, but the compiler and resharper cant see that EnsureSuccessStatusCode always |
| // throws an exception in this scenario. So it complains later on in the method. |
| throw; |
| } |
| |
| Exception exception; |
| try |
| { |
| TextReader reader = new StreamReader(input); |
| JsonSerializer serializer = JsonSerializer.Create(ReplicationService.JSON_SERIALIZER_SETTINGS); |
| exception = (Exception)serializer.Deserialize(new JsonTextReader(reader)); |
| } |
| catch (Exception e) |
| { |
| //not likely |
| throw new HttpRequestException(string.Format("Failed to read exception object: {0} {1}", response.StatusCode, response.ReasonPhrase), e); |
| } |
| finally |
| { |
| input.Dispose(); |
| } |
| |
| if (exception is IOException) |
| { |
| //NOTE: Preserve server stacktrace, but there are probably better options. |
| throw new IOException(exception.Message, exception); |
| } |
| throw new HttpRequestException(string.Format("unknown exception: {0} {1}", response.StatusCode, response.ReasonPhrase), exception); |
| } |
| |
| /// <summary> |
| /// <b>Internal:</b> Execute a request and return its result. |
| /// The <paramref name="parameters"/> argument is treated as: name1,value1,name2,value2,... |
| /// </summary> |
| protected virtual HttpResponseMessage ExecutePost(string request, object entity, params string[] parameters) |
| { |
| EnsureOpen(); |
| //.NET Note: No headers? No ContentType?... Bad use of Http? |
| HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, QueryString(request, parameters)); |
| |
| req.Content = new StringContent(JToken.FromObject(entity, JsonSerializer.Create(ReplicationService.JSON_SERIALIZER_SETTINGS)) |
| .ToString(Formatting.None), Encoding.UTF8, "application/json"); |
| |
| //.NET Note: Bridging from Async to Sync, this is not ideal and we could consider changing the interface to be Async or provide Async overloads |
| // and have these Sync methods with their caveats. |
| HttpResponseMessage response = httpc.SendAsync(req).ConfigureAwait(false).GetAwaiter().GetResult(); |
| VerifyStatus(response); |
| return response; |
| } |
| |
| /// <summary> |
| /// <b>Internal:</b> Execute a request and return its result. |
| /// The <paramref name="parameters"/> argument is treated as: name1,value1,name2,value2,... |
| /// </summary> |
| protected virtual HttpResponseMessage ExecuteGet(string request, params string[] parameters) |
| { |
| EnsureOpen(); |
| //Note: No headers? No ContentType?... Bad use of Http? |
| HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Get, QueryString(request, parameters)); |
| //.NET Note: Bridging from Async to Sync, this is not ideal and we could consider changing the interface to be Async or provide Async overloads |
| // and have these Sync methods with their caveats. |
| HttpResponseMessage response = httpc.SendAsync(req).ConfigureAwait(false).GetAwaiter().GetResult(); |
| VerifyStatus(response); |
| return response; |
| } |
| |
| private string QueryString(string request, params string[] parameters) |
| { |
| return parameters == null |
| ? string.Format("{0}/{1}", Url, request) |
| : string.Format("{0}/{1}?{2}", Url, request, string |
| .Join("&", parameters.Select(WebUtility.UrlEncode).InPairs((key, val) => string.Format("{0}={1}", key, val)))); |
| } |
| |
| /// <summary> |
| /// Internal utility: input stream of the provided response. |
| /// </summary> |
| /// <exception cref="IOException"></exception> |
| public virtual Stream ResponseInputStream(HttpResponseMessage response) |
| { |
| return ResponseInputStream(response, false); |
| } |
| |
| // TODO: can we simplify this Consuming !?!?!? |
| /// <summary> |
| /// Internal utility: input stream of the provided response, which optionally |
| /// consumes the response's resources when the input stream is exhausted. |
| /// </summary> |
| /// <exception cref="IOException"></exception> |
| public virtual Stream ResponseInputStream(HttpResponseMessage response, bool consume) |
| { |
| return response.Content.ReadAsStreamAsync().ConfigureAwait(false).GetAwaiter().GetResult(); |
| } |
| |
| /// <summary> |
| /// Returns <c>true</c> if this instance was <see cref="Dispose(bool)"/>ed, otherwise |
| /// returns <c>false</c>. Note that if you override <see cref="Dispose(bool)"/>, you must call |
| /// <see cref="Dispose(bool)"/> on the base class, in order for this instance to be properly disposed. |
| /// </summary> |
| public bool IsDisposed => isDisposed; |
| |
| /// <summary> |
| /// Calls the overload <see cref="DoAction{T}(HttpResponseMessage, bool, Func{T})"/> passing <c>true</c> to consume. |
| /// </summary> |
| protected virtual T DoAction<T>(HttpResponseMessage response, Func<T> call) |
| { |
| return DoAction(response, true, call); |
| } |
| |
| /// <summary> |
| /// Do a specific action and validate after the action that the status is still OK, |
| /// and if not, attempt to extract the actual server side exception. Optionally |
| /// release the response at exit, depending on <paramref name="consume"/> parameter. |
| /// </summary> |
| protected virtual T DoAction<T>(HttpResponseMessage response, bool consume, Func<T> call) |
| { |
| Exception error = new NotImplementedException(); |
| try |
| { |
| return call(); |
| } |
| catch (IOException e) |
| { |
| error = e; |
| } |
| catch (Exception e) |
| { |
| error = new IOException(e.Message, e); |
| } |
| finally |
| { |
| //JAVA: Had a TryCatch here and then used a EntityUtils class to consume the response, |
| //JAVA: Unsure of what that was trying to achieve it was left out. |
| //JAVA: This also means that right now this overload does nothing more than support the signature given by the Java ver. |
| //JAVA: Overall from a .NET perspective, this method is overly suspicious. |
| VerifyStatus(response); |
| } |
| throw error; // should not get here |
| } |
| |
| /// <summary> |
| /// Disposes this <see cref="HttpClientBase"/>. |
| /// When called with <code>true</code>, this disposes the underlying <see cref="HttpClient"/>. |
| /// </summary> |
| protected virtual void Dispose(bool disposing) |
| { |
| if (disposing) |
| { |
| httpc.Dispose(); |
| } |
| isDisposed = true; |
| } |
| |
| /// <summary> |
| /// Disposes this <see cref="HttpClientBase"/>. |
| /// This disposes the underlying <see cref="HttpClient"/>. |
| /// </summary> |
| public void Dispose() |
| { |
| Dispose(true); |
| GC.SuppressFinalize(this); |
| } |
| } |
| } |