354 lines
14 KiB
C#
354 lines
14 KiB
C#
/*
|
|
Copyright 2013 Google Inc
|
|
|
|
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.
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Net.Http;
|
|
using System.Threading.Tasks;
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
using Google.Apis.Discovery;
|
|
using Google.Apis.Http;
|
|
using Google.Apis.Json;
|
|
using Google.Apis.Logging;
|
|
using Google.Apis.Requests;
|
|
using Google.Apis.Util;
|
|
using Google.Apis.Testing;
|
|
|
|
namespace Google.Apis.Services
|
|
{
|
|
/// <summary>
|
|
/// A base class for a client service which provides common mechanism for all services, like
|
|
/// serialization and GZip support. It should be safe to use a single service instance to make server requests
|
|
/// concurrently from multiple threads.
|
|
/// This class adds a special <see cref="Google.Apis.Http.IHttpExecuteInterceptor"/> to the
|
|
/// <see cref="Google.Apis.Http.ConfigurableMessageHandler"/> execute interceptor list, which uses the given
|
|
/// Authenticator. It calls to its applying authentication method, and injects the "Authorization" header in the
|
|
/// request.
|
|
/// If the given Authenticator implements <see cref="Google.Apis.Http.IHttpUnsuccessfulResponseHandler"/>, this
|
|
/// class adds the Authenticator to the <see cref="Google.Apis.Http.ConfigurableMessageHandler"/>'s unsuccessful
|
|
/// response handler list.
|
|
/// </summary>
|
|
public abstract class BaseClientService : IClientService
|
|
{
|
|
/// <summary>The class logger.</summary>
|
|
private static readonly ILogger Logger = ApplicationContext.Logger.ForType<BaseClientService>();
|
|
|
|
/// <summary>The default maximum allowed length of a URL string for GET requests.</summary>
|
|
[VisibleForTestOnly]
|
|
public const uint DefaultMaxUrlLength = 2048;
|
|
|
|
#region Initializer
|
|
|
|
/// <summary>An initializer class for the client service.</summary>
|
|
public class Initializer
|
|
{
|
|
/// <summary>
|
|
/// Gets or sets the factory for creating <see cref="System.Net.Http.HttpClient"/> instance. If this
|
|
/// property is not set the service uses a new <see cref="Google.Apis.Http.HttpClientFactory"/> instance.
|
|
/// </summary>
|
|
public IHttpClientFactory HttpClientFactory { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a HTTP client initializer which is able to customize properties on
|
|
/// <see cref="Google.Apis.Http.ConfigurableHttpClient"/> and
|
|
/// <see cref="Google.Apis.Http.ConfigurableMessageHandler"/>.
|
|
/// </summary>
|
|
public IConfigurableHttpClientInitializer HttpClientInitializer { get; set; }
|
|
|
|
/// <summary>
|
|
/// Get or sets the exponential back-off policy used by the service. Default value is
|
|
/// <c>UnsuccessfulResponse503</c>, which means that exponential back-off is used on 503 abnormal HTTP
|
|
/// response.
|
|
/// If the value is set to <c>None</c>, no exponential back-off policy is used, and it's up to the user to
|
|
/// configure the <see cref="Google.Apis.Http.ConfigurableMessageHandler"/> in an
|
|
/// <see cref="Google.Apis.Http.IConfigurableHttpClientInitializer"/> to set a specific back-off
|
|
/// implementation (using <see cref="Google.Apis.Http.BackOffHandler"/>).
|
|
/// </summary>
|
|
public ExponentialBackOffPolicy DefaultExponentialBackOffPolicy { get; set; }
|
|
|
|
/// <summary>Gets or sets whether this service supports GZip. Default value is <c>true</c>.</summary>
|
|
public bool GZipEnabled { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the serializer. Default value is <see cref="Google.Apis.Json.NewtonsoftJsonSerializer"/>.
|
|
/// </summary>
|
|
public ISerializer Serializer { get; set; }
|
|
|
|
/// <summary>Gets or sets the API Key. Default value is <c>null</c>.</summary>
|
|
public string ApiKey { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets Application name to be used in the User-Agent header. Default value is <c>null</c>.
|
|
/// </summary>
|
|
public string ApplicationName { get; set; }
|
|
|
|
/// <summary>
|
|
/// Maximum allowed length of a URL string for GET requests. Default value is <c>2048</c>. If the value is
|
|
/// set to <c>0</c>, requests will never be modified due to URL string length.
|
|
/// </summary>
|
|
public uint MaxUrlLength { get; set; }
|
|
|
|
/// <summary>Constructs a new initializer with default values.</summary>
|
|
public Initializer()
|
|
{
|
|
GZipEnabled = true;
|
|
Serializer = new NewtonsoftJsonSerializer();
|
|
DefaultExponentialBackOffPolicy = ExponentialBackOffPolicy.UnsuccessfulResponse503;
|
|
MaxUrlLength = DefaultMaxUrlLength;
|
|
}
|
|
|
|
internal void Validate()
|
|
{
|
|
// TODO: Validate ApplicationName
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>Constructs a new base client with the specified initializer.</summary>
|
|
protected BaseClientService(Initializer initializer)
|
|
{
|
|
initializer.Validate();
|
|
// Set the right properties by the initializer's properties.
|
|
GZipEnabled = initializer.GZipEnabled;
|
|
Serializer = initializer.Serializer;
|
|
ApiKey = initializer.ApiKey;
|
|
ApplicationName = initializer.ApplicationName;
|
|
if (ApplicationName == null)
|
|
{
|
|
Logger.Warning("Application name is not set. Please set Initializer.ApplicationName property");
|
|
}
|
|
HttpClientInitializer = initializer.HttpClientInitializer;
|
|
|
|
// Create a HTTP client for this service.
|
|
HttpClient = CreateHttpClient(initializer);
|
|
}
|
|
|
|
/// <summary>Returns <c>true</c> if this service contains the specified feature.</summary>
|
|
private bool HasFeature(Features feature)
|
|
{
|
|
return Features.Contains(Utilities.GetEnumStringValue(feature));
|
|
}
|
|
|
|
private ConfigurableHttpClient CreateHttpClient(Initializer initializer)
|
|
{
|
|
// If factory wasn't set use the default HTTP client factory.
|
|
var factory = initializer.HttpClientFactory ?? new HttpClientFactory();
|
|
var args = new CreateHttpClientArgs
|
|
{
|
|
GZipEnabled = GZipEnabled,
|
|
ApplicationName = ApplicationName,
|
|
};
|
|
|
|
// Add the user's input initializer.
|
|
if (HttpClientInitializer != null)
|
|
{
|
|
args.Initializers.Add(HttpClientInitializer);
|
|
}
|
|
|
|
// Add exponential back-off initializer if necessary.
|
|
if (initializer.DefaultExponentialBackOffPolicy != ExponentialBackOffPolicy.None)
|
|
{
|
|
args.Initializers.Add(new ExponentialBackOffInitializer(initializer.DefaultExponentialBackOffPolicy,
|
|
CreateBackOffHandler));
|
|
}
|
|
|
|
var httpClient = factory.CreateHttpClient(args);
|
|
if (initializer.MaxUrlLength > 0)
|
|
{
|
|
httpClient.MessageHandler.AddExecuteInterceptor(new MaxUrlLengthInterceptor(initializer.MaxUrlLength));
|
|
}
|
|
return httpClient;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the back-off handler with <see cref="Google.Apis.Util.ExponentialBackOff"/>.
|
|
/// Overrides this method to change the default behavior of back-off handler (e.g. you can change the maximum
|
|
/// waited request's time span, or create a back-off handler with you own implementation of
|
|
/// <see cref="Google.Apis.Util.IBackOff"/>).
|
|
/// </summary>
|
|
protected virtual BackOffHandler CreateBackOffHandler()
|
|
{
|
|
// TODO(peleyal): consider return here interface and not the concrete class
|
|
return new BackOffHandler(new ExponentialBackOff());
|
|
}
|
|
|
|
#region IClientService Members
|
|
|
|
/// <inheritdoc/>
|
|
public ConfigurableHttpClient HttpClient { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public IConfigurableHttpClientInitializer HttpClientInitializer { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public bool GZipEnabled { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public string ApiKey { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public string ApplicationName { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public void SetRequestSerailizedContent(HttpRequestMessage request, object body)
|
|
{
|
|
request.SetRequestSerailizedContent(this, body, GZipEnabled);
|
|
}
|
|
|
|
#region Serialization
|
|
|
|
/// <inheritdoc/>
|
|
public ISerializer Serializer { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public virtual string SerializeObject(object obj)
|
|
{
|
|
if (HasFeature(Discovery.Features.LegacyDataResponse))
|
|
{
|
|
// Legacy path
|
|
var request = new StandardResponse<object> { Data = obj };
|
|
return Serializer.Serialize(request);
|
|
}
|
|
return Serializer.Serialize(obj);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async Task<T> DeserializeResponse<T>(HttpResponseMessage response)
|
|
{
|
|
var text = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
|
|
|
// If a string is request, don't parse the response.
|
|
if (Type.Equals(typeof(T), typeof(string)))
|
|
{
|
|
return (T)(object)text;
|
|
}
|
|
|
|
// Check if there was an error returned. The error node is returned in both paths
|
|
// Deserialize the stream based upon the format of the stream.
|
|
if (HasFeature(Discovery.Features.LegacyDataResponse))
|
|
{
|
|
// Legacy path (deprecated!)
|
|
StandardResponse<T> sr = null;
|
|
try
|
|
{
|
|
sr = Serializer.Deserialize<StandardResponse<T>>(text);
|
|
}
|
|
catch (JsonReaderException ex)
|
|
{
|
|
throw new GoogleApiException(Name,
|
|
"Failed to parse response from server as json [" + text + "]", ex);
|
|
}
|
|
|
|
if (sr.Error != null)
|
|
{
|
|
throw new GoogleApiException(Name, "Server error - " + sr.Error)
|
|
{
|
|
Error = sr.Error
|
|
};
|
|
}
|
|
|
|
if (sr.Data == null)
|
|
{
|
|
throw new GoogleApiException(Name, "The response could not be deserialized.");
|
|
}
|
|
return sr.Data;
|
|
}
|
|
|
|
// New path: Deserialize the object directly.
|
|
T result = default(T);
|
|
try
|
|
{
|
|
result = Serializer.Deserialize<T>(text);
|
|
}
|
|
catch (JsonReaderException ex)
|
|
{
|
|
throw new GoogleApiException(Name, "Failed to parse response from server as json [" + text + "]", ex);
|
|
}
|
|
|
|
// TODO(peleyal): is this the right place to check ETag? it isn't part of deserialization!
|
|
// If this schema/object provides an error container, check it.
|
|
var eTag = response.Headers.ETag != null ? response.Headers.ETag.Tag : null;
|
|
if (result is IDirectResponseSchema && eTag != null)
|
|
{
|
|
(result as IDirectResponseSchema).ETag = eTag;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async Task<RequestError> DeserializeError(HttpResponseMessage response)
|
|
{
|
|
StandardResponse<object> errorResponse = null;
|
|
try
|
|
{
|
|
var str = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
|
errorResponse = Serializer.Deserialize<StandardResponse<object>>(str);
|
|
if (errorResponse.Error == null)
|
|
{
|
|
throw new GoogleApiException(Name, "error response is null");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// exception will be thrown in case the response content is empty or it can't be deserialized to
|
|
// Standard response (which contains data and error properties)
|
|
throw new GoogleApiException(Name,
|
|
"An Error occurred, but the error response could not be deserialized", ex);
|
|
}
|
|
|
|
return errorResponse.Error;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Abstract Members
|
|
|
|
/// <inheritdoc/>
|
|
public abstract string Name { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public abstract string BaseUri { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public abstract string BasePath { get; }
|
|
|
|
/// <summary>The URI used for batch operations.</summary>
|
|
public virtual string BatchUri { get { return null; } }
|
|
|
|
/// <summary>The path used for batch operations.</summary>
|
|
public virtual string BatchPath { get { return null; } }
|
|
|
|
/// <inheritdoc/>
|
|
public abstract IList<string> Features { get; }
|
|
|
|
#endregion
|
|
|
|
#endregion
|
|
|
|
/// <inheritdoc/>
|
|
public virtual void Dispose()
|
|
{
|
|
if (HttpClient != null)
|
|
{
|
|
HttpClient.Dispose();
|
|
}
|
|
}
|
|
}
|
|
}
|