blob: 2d21b3691fb9d57caa2d88e483af5905149ea53c [file] [log] [blame]
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);
}
}
}