File size: 18,150 Bytes
7b715bc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 | /*
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
{
/// <summary>
/// Represents an abstract request base class to make requests to a service.
/// </summary>
public abstract class ClientServiceRequest
{
/// <summary>Unsuccessful response handlers for this request only.</summary>
protected List<IHttpUnsuccessfulResponseHandler> _unsuccessfulResponseHandlers;
/// <summary>Exception handlers for this request only.</summary>
protected List<IHttpExceptionHandler> _exceptionHandlers;
/// <summary>Execute interceptors for this request only.</summary>
protected List<IHttpExecuteInterceptor> _executeInterceptors;
/// <summary>
/// Credential to use for this request.
/// If <see cref="Credential"/> implements <see cref="IHttpUnsuccessfulResponseHandler"/>
/// then it will also be included as a handler of an unsuccessful response.
/// </summary>
public IHttpExecuteInterceptor Credential { get; set; }
/// <summary>
/// Add an unsuccessful response handler for this request only.
/// </summary>
/// <param name="handler">The unsuccessful response handler. Must not be <c>null</c>.</param>
public void AddUnsuccessfulResponseHandler(IHttpUnsuccessfulResponseHandler handler)
{
handler.ThrowIfNull(nameof(handler));
if (_unsuccessfulResponseHandlers == null)
{
_unsuccessfulResponseHandlers = new List<IHttpUnsuccessfulResponseHandler>();
}
_unsuccessfulResponseHandlers.Add(handler);
}
/// <summary>
/// Add an exception handler for this request only.
/// </summary>
/// <param name="handler">The exception handler. Must not be <c>null</c>.</param>
public void AddExceptionHandler(IHttpExceptionHandler handler)
{
handler.ThrowIfNull(nameof(handler));
if (_exceptionHandlers == null)
{
_exceptionHandlers = new List<IHttpExceptionHandler>();
}
_exceptionHandlers.Add(handler);
}
/// <summary>
/// Add an execute interceptor for this request only.
/// If the request is retried, the interceptor will be called on each attempt.
/// </summary>
/// <param name="handler">The execute interceptor. Must not be <c>null</c>.</param>
public void AddExecuteInterceptor(IHttpExecuteInterceptor handler)
{
handler.ThrowIfNull(nameof(handler));
if (_executeInterceptors == null)
{
_executeInterceptors = new List<IHttpExecuteInterceptor>();
}
_executeInterceptors.Add(handler);
}
}
/// <summary>
/// Represents an abstract, strongly typed request base class to make requests to a service.
/// Supports a strongly typed response.
/// </summary>
/// <typeparam name="TResponse">The type of the response object</typeparam>
public abstract class ClientServiceRequest<TResponse> : ClientServiceRequest, IClientServiceRequest<TResponse>
{
internal const string ApiVersionHeaderName = "x-goog-api-version";
/// <summary>The class logger.</summary>
private static readonly ILogger Logger = ApplicationContext.Logger.ForType<ClientServiceRequest<TResponse>>();
/// <summary>The service on which this request will be executed.</summary>
private readonly IClientService service;
/// <summary>Defines whether the E-Tag will be used in a specified way or be ignored.</summary>
public ETagAction ETagAction { get; set; }
/// <summary>
/// Gets or sets the callback for modifying HTTP requests made by this service request.
/// </summary>
public Action<HttpRequestMessage> ModifyRequest { get; set; }
/// <summary>
/// Override for service-wide validation configuration in <see cref="BaseClientService.Initializer.ValidateParameters"/>.
/// 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.
/// </summary>
public bool? ValidateParameters { get; set; }
/// <summary>
/// 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.
/// </summary>
public virtual string ApiVersion => null;
#region IClientServiceRequest Properties
/// <inheritdoc/>
public abstract string MethodName { get; }
/// <inheritdoc/>
public abstract string RestPath { get; }
/// <inheritdoc/>
public abstract string HttpMethod { get; }
/// <inheritdoc/>
public IDictionary<string, IParameter> RequestParameters { get; private set; }
/// <inheritdoc/>
public IClientService Service
{
get { return service; }
}
#endregion
/// <summary>Creates a new service request.</summary>
protected ClientServiceRequest(IClientService service)
{
this.service = service;
}
/// <summary>
/// Initializes request's parameters. Inherited classes MUST override this method to add parameters to the
/// <see cref="RequestParameters"/> dictionary.
/// </summary>
protected virtual void InitParameters()
{
RequestParameters = new Dictionary<string, IParameter>();
}
#region Execution
/// <inheritdoc/>
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;
}
}
/// <inheritdoc/>
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.
}
/// <inheritdoc/>
public async Task<TResponse> ExecuteAsync()
{
return await ExecuteAsync(CancellationToken.None).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<TResponse> ExecuteAsync(CancellationToken cancellationToken)
{
using (var response = await ExecuteUnparsedAsync(cancellationToken).ConfigureAwait(false))
{
cancellationToken.ThrowIfCancellationRequested();
return await ParseResponse(response).ConfigureAwait(false);
}
}
/// <inheritdoc/>
public async Task<Stream> ExecuteAsStreamAsync()
{
return await ExecuteAsStreamAsync(CancellationToken.None).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<Stream> 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
/// <summary>Sync executes the request without parsing the result. </summary>
private async Task<HttpResponseMessage> ExecuteUnparsedAsync(CancellationToken cancellationToken)
{
using (var request = CreateRequest())
{
return await service.HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}
/// <summary>Parses the response and deserialize the content into the requested response object. </summary>
private async Task<TResponse> ParseResponse(HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
{
return await service.DeserializeResponse<TResponse>(response).ConfigureAwait(false);
}
var error = await service.DeserializeError(response).ConfigureAwait(false);
throw new GoogleApiException(service.Name)
{
Error = error,
HttpStatusCode = response.StatusCode
};
}
#endregion
#endregion
/// <inheritdoc/>
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;
}
/// <summary>
/// Creates the <see cref="Google.Apis.Requests.RequestBuilder"/> which is used to generate a request.
/// </summary>
/// <returns>
/// A new builder instance which contains the HTTP method and the right Uri with its path and query parameters.
/// </returns>
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;
}
/// <summary>Generates the right URL for this request.</summary>
protected string GenerateRequestUri() => CreateBuilder().BuildUri().AbsoluteUri;
/// <summary>Returns the body of this request.</summary>
/// <returns>The body of this request.</returns>
protected virtual object GetBody() => null;
#region ETag
/// <summary>
/// Adds the right ETag action (e.g. If-Match) header to the given HTTP request if the body contains ETag.
/// </summary>
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;
}
}
}
/// <summary>Returns the default ETagAction for a specific HTTP verb.</summary>
[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
/// <summary>Adds path and query parameters to the given <c>requestBuilder</c>.</summary>
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
}
}
|