/* Copyright 2011 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 Google.Apis.Discovery; using Google.Apis.Http; using Google.Apis.Logging; using Google.Apis.Requests.Parameters; using Google.Apis.Services; using Google.Apis.Testing; using Google.Apis.Util; using System; using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; namespace Google.Apis.Requests { /// /// Represents an abstract request base class to make requests to a service. /// public abstract class ClientServiceRequest { /// Unsuccessful response handlers for this request only. protected List _unsuccessfulResponseHandlers; /// Exception handlers for this request only. protected List _exceptionHandlers; /// Execute interceptors for this request only. protected List _executeInterceptors; /// /// Credential to use for this request. /// If implements /// then it will also be included as a handler of an unsuccessful response. /// public IHttpExecuteInterceptor Credential { get; set; } /// /// Add an unsuccessful response handler for this request only. /// /// The unsuccessful response handler. Must not be null. public void AddUnsuccessfulResponseHandler(IHttpUnsuccessfulResponseHandler handler) { handler.ThrowIfNull(nameof(handler)); if (_unsuccessfulResponseHandlers == null) { _unsuccessfulResponseHandlers = new List(); } _unsuccessfulResponseHandlers.Add(handler); } /// /// Add an exception handler for this request only. /// /// The exception handler. Must not be null. public void AddExceptionHandler(IHttpExceptionHandler handler) { handler.ThrowIfNull(nameof(handler)); if (_exceptionHandlers == null) { _exceptionHandlers = new List(); } _exceptionHandlers.Add(handler); } /// /// Add an execute interceptor for this request only. /// If the request is retried, the interceptor will be called on each attempt. /// /// The execute interceptor. Must not be null. public void AddExecuteInterceptor(IHttpExecuteInterceptor handler) { handler.ThrowIfNull(nameof(handler)); if (_executeInterceptors == null) { _executeInterceptors = new List(); } _executeInterceptors.Add(handler); } } /// /// Represents an abstract, strongly typed request base class to make requests to a service. /// Supports a strongly typed response. /// /// The type of the response object public abstract class ClientServiceRequest : ClientServiceRequest, IClientServiceRequest { internal const string ApiVersionHeaderName = "x-goog-api-version"; /// The class logger. private static readonly ILogger Logger = ApplicationContext.Logger.ForType>(); /// The service on which this request will be executed. private readonly IClientService service; /// Defines whether the E-Tag will be used in a specified way or be ignored. public ETagAction ETagAction { get; set; } /// /// Gets or sets the callback for modifying HTTP requests made by this service request. /// public Action ModifyRequest { get; set; } /// /// Override for service-wide validation configuration in . /// If this is null (the default) then the value from the service initializer is used to determine /// whether or not parameters should be validated client-side. If this is non-null, it overrides /// whatever value is specified in the service. /// public bool? ValidateParameters { get; set; } /// /// The precise API version represented by this request. /// Subclasses generated from a specific known version should override this property, /// which will result in an x-api-version header being sent on the HTTP request. /// public virtual string ApiVersion => null; #region IClientServiceRequest Properties /// public abstract string MethodName { get; } /// public abstract string RestPath { get; } /// public abstract string HttpMethod { get; } /// public IDictionary RequestParameters { get; private set; } /// public IClientService Service { get { return service; } } #endregion /// Creates a new service request. protected ClientServiceRequest(IClientService service) { this.service = service; } /// /// Initializes request's parameters. Inherited classes MUST override this method to add parameters to the /// dictionary. /// protected virtual void InitParameters() { RequestParameters = new Dictionary(); } #region Execution /// public TResponse Execute() { try { using (var response = ExecuteUnparsedAsync(CancellationToken.None).Result) { return ParseResponse(response).Result; } } catch (AggregateException aex) { // If an exception was thrown during the tasks, unwrap and throw it. ExceptionDispatchInfo.Capture(aex.InnerException ?? aex).Throw(); // Won't get here, but compiler requires it throw; } } /// public Stream ExecuteAsStream() { // TODO(peleyal): should we copy the stream, and dispose the response? try { // Sync call. var response = ExecuteUnparsedAsync(CancellationToken.None).Result; return response.Content.ReadAsStreamAsync().Result; } catch (AggregateException aex) { // If an exception was thrown during the tasks, unwrap and throw it. throw aex.InnerException; } // Any other exception will just bubble up. } /// public async Task ExecuteAsync() { return await ExecuteAsync(CancellationToken.None).ConfigureAwait(false); } /// public async Task ExecuteAsync(CancellationToken cancellationToken) { using (var response = await ExecuteUnparsedAsync(cancellationToken).ConfigureAwait(false)) { cancellationToken.ThrowIfCancellationRequested(); return await ParseResponse(response).ConfigureAwait(false); } } /// public async Task ExecuteAsStreamAsync() { return await ExecuteAsStreamAsync(CancellationToken.None).ConfigureAwait(false); } /// public async Task ExecuteAsStreamAsync(CancellationToken cancellationToken) { // TODO(peleyal): should we copy the stream, and dispose the response? var response = await ExecuteUnparsedAsync(cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); } #region Helpers /// Sync executes the request without parsing the result. private async Task ExecuteUnparsedAsync(CancellationToken cancellationToken) { using (var request = CreateRequest()) { return await service.HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); } } /// Parses the response and deserialize the content into the requested response object. private async Task ParseResponse(HttpResponseMessage response) { if (response.IsSuccessStatusCode) { return await service.DeserializeResponse(response).ConfigureAwait(false); } var error = await service.DeserializeError(response).ConfigureAwait(false); throw new GoogleApiException(service.Name) { Error = error, HttpStatusCode = response.StatusCode }; } #endregion #endregion /// public HttpRequestMessage CreateRequest(bool? overrideGZipEnabled = null) { var builder = CreateBuilder(); var request = builder.CreateRequest(); object body = GetBody(); request.SetRequestSerailizedContent(service, body, overrideGZipEnabled.HasValue ? overrideGZipEnabled.Value : service.GZipEnabled); AddETag(request); if (_unsuccessfulResponseHandlers != null) { request.SetOption(ConfigurableMessageHandler.UnsuccessfulResponseHandlerKey, _unsuccessfulResponseHandlers); } if (_exceptionHandlers != null) { request.SetOption(ConfigurableMessageHandler.ExceptionHandlerKey, _exceptionHandlers); } if (_executeInterceptors != null) { request.SetOption(ConfigurableMessageHandler.ExecuteInterceptorKey, _executeInterceptors); } if (Credential != null) { request.SetOption(ConfigurableMessageHandler.CredentialKey, Credential); } if (ApiVersion is string apiVersion) { request.Headers.Add(ApiVersionHeaderName, apiVersion); } ModifyRequest?.Invoke(request); return request; } /// /// Creates the which is used to generate a request. /// /// /// A new builder instance which contains the HTTP method and the right Uri with its path and query parameters. /// private RequestBuilder CreateBuilder() { var builder = new RequestBuilder() { BaseUri = new Uri(Service.BaseUri), Path = RestPath, Method = HttpMethod, }; // Init parameters. if (service.ApiKey != null) { builder.AddParameter(RequestParameterType.Query, "key", service.ApiKey); } var parameters = ParameterUtils.CreateParameterDictionary(this); AddParameters(builder, ParameterCollection.FromDictionary(parameters)); return builder; } /// Generates the right URL for this request. protected string GenerateRequestUri() => CreateBuilder().BuildUri().AbsoluteUri; /// Returns the body of this request. /// The body of this request. protected virtual object GetBody() => null; #region ETag /// /// Adds the right ETag action (e.g. If-Match) header to the given HTTP request if the body contains ETag. /// private void AddETag(HttpRequestMessage request) { IDirectResponseSchema body = GetBody() as IDirectResponseSchema; if (body != null && !string.IsNullOrEmpty(body.ETag)) { var etag = body.ETag; ETagAction action = ETagAction == ETagAction.Default ? GetDefaultETagAction(HttpMethod) : ETagAction; // TODO: ETag-related headers are added without validation at the moment, because it is known // that some services are returning unquoted etags (see rfc7232). // Once all services are fixed, change back to the commented-out code that validates the header. switch (action) { case ETagAction.IfMatch: //request.Headers.IfMatch.Add(new EntityTagHeaderValue(etag)); request.Headers.TryAddWithoutValidation("If-Match", etag); break; case ETagAction.IfNoneMatch: //request.Headers.IfNoneMatch.Add(new EntityTagHeaderValue(etag)); request.Headers.TryAddWithoutValidation("If-None-Match", etag); break; } } } /// Returns the default ETagAction for a specific HTTP verb. [VisibleForTestOnly] public static ETagAction GetDefaultETagAction(string httpMethod) { switch (httpMethod) { // Incoming data should only be updated if it has been changed on the server. case HttpConsts.Get: return ETagAction.IfNoneMatch; // Outgoing data should only be committed if it hasn't been changed on the server. case HttpConsts.Put: case HttpConsts.Post: case HttpConsts.Patch: case HttpConsts.Delete: return ETagAction.IfMatch; default: return ETagAction.Ignore; } } #endregion #region Parameters /// Adds path and query parameters to the given requestBuilder. private void AddParameters(RequestBuilder requestBuilder, ParameterCollection inputParameters) { bool validateParameters = ValidateParameters ?? (Service as BaseClientService)?.ValidateParameters ?? true; foreach (var parameter in inputParameters) { if (!RequestParameters.TryGetValue(parameter.Key, out IParameter parameterDefinition)) { throw new GoogleApiException(Service.Name, $"Invalid parameter \"{parameter.Key}\" was specified"); } string value = parameter.Value; if (validateParameters && !ParameterValidator.ValidateParameter(parameterDefinition, value, out string error)) { throw new GoogleApiException(Service.Name, $"Parameter validation failed for \"{parameterDefinition.Name}\" : {error}"); } if (value == null) // If the parameter is null, use the default value. { value = parameterDefinition.DefaultValue; } switch (parameterDefinition.ParameterType) { case "path": requestBuilder.AddParameter(RequestParameterType.Path, parameter.Key, value); break; case "query": // If the parameter is optional and no value is given, don't add to url. if (!Object.Equals(value, parameterDefinition.DefaultValue) || parameterDefinition.IsRequired) { requestBuilder.AddParameter(RequestParameterType.Query, parameter.Key, value); } break; default: throw new GoogleApiException(service.Name, $"Unsupported parameter type \"{parameterDefinition.ParameterType}\" for \"{parameterDefinition.Name}\""); } } // Check if there is a required parameter which wasn't set. foreach (var parameter in RequestParameters.Values) { if (parameter.IsRequired && !inputParameters.ContainsKey(parameter.Name)) { throw new GoogleApiException(service.Name, $"Parameter \"{parameter.Name}\" is missing"); } } } #endregion } }