Google Api

This commit is contained in:
Paul Schneider 2017-06-20 02:47:52 +02:00
commit 83f9fc1bd8
123 changed files with 12961 additions and 60 deletions

View file

@ -0,0 +1,383 @@
/*
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.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Http;
using Google.Apis.Services;
using Google.Apis.Testing;
namespace Google.Apis.Requests
{
/// <summary>
/// A batch request which represents individual requests to Google servers. You should add a single service
/// request using the <see cref="Queue"/> method and execute all individual requests using
/// <see cref="ExecuteAsync()"/>. More information about the batch protocol is available in
/// https://developers.google.com/storage/docs/json_api/v1/how-tos/batch.
/// <remarks>
/// Current implementation doesn't retry on unsuccessful individual response and doesn't support requests with
/// different access tokens (different users or scopes).
/// </remarks>
/// </summary>
public sealed class BatchRequest
{
private const string DefaultBatchUrl = "https://www.googleapis.com/batch";
private const int QueueLimit = 1000;
private readonly IList<InnerRequest> allRequests = new List<InnerRequest>();
private readonly string batchUrl;
private readonly IClientService service;
// For testing
internal string BatchUrl => batchUrl;
/// <summary>A concrete type callback for an individual response.</summary>
/// <typeparam name="TResponse">The response type.</typeparam>
/// <param name="content">The content response or <c>null</c> if the request failed.</param>
/// <param name="error">Error or <c>null</c> if the request succeeded.</param>
/// <param name="index">The request index.</param>
/// <param name="message">The HTTP individual response.</param>
public delegate void OnResponse<in TResponse>
(TResponse content, RequestError error, int index, HttpResponseMessage message) where TResponse : class;
#region Inner Request
/// <summary>This inner class represents an individual inner request.</summary>
private class InnerRequest
{
/// <summary>Gets or sets the client service request.</summary>
public IClientServiceRequest ClientRequest { get; set; }
/// <summary>Gets or sets the response class type.</summary>
public Type ResponseType { get; set; }
/// <summary>A callback method which will be called after an individual response was parsed.</summary>
/// <param name="content">The content response or <c>null</c> if the request failed.</param>
/// <param name="error">Error or <c>null</c> if the request succeeded.</param>
/// <param name="index">The request index.</param>
/// <param name="message">The HTTP individual response.</param>
public virtual void OnResponse(object content, RequestError error, int index, HttpResponseMessage message)
{
// Set ETag on the response.
var eTagValue = message.Headers.ETag != null ? message.Headers.ETag.Tag : null;
var eTagContainer = content as IDirectResponseSchema;
if (eTagContainer != null && eTagContainer.ETag == null && eTagValue != null)
{
eTagContainer.ETag = eTagValue;
}
}
}
/// <summary>
/// This generic inner class represents an individual inner request with a generic response type.
/// </summary>
private class InnerRequest<TResponse> : InnerRequest
where TResponse : class
{
/// <summary>Gets or sets a concrete type callback for an individual response. </summary>
public OnResponse<TResponse> OnResponseCallback { get; set; }
public override void OnResponse(object content, RequestError error, int index,
HttpResponseMessage message)
{
base.OnResponse(content, error, index, message);
if (OnResponseCallback == null)
return;
OnResponseCallback(content as TResponse, error, index, message);
}
}
#endregion
/// <summary>
/// Constructs a new batch request using the given service. See
/// <see cref="BatchRequest(IClientService, string)"/> for more information.
/// </summary>
public BatchRequest(IClientService service)
: this(service, (service as BaseClientService)?.BatchUri ?? DefaultBatchUrl) { }
/// <summary>
/// Constructs a new batch request using the given service. The service's HTTP client is used to create a
/// request to the given server URL and its serializer members are used to serialize the request and
/// deserialize the response.
/// </summary>
public BatchRequest(IClientService service, string batchUrl)
{
this.batchUrl = batchUrl;
this.service = service;
}
/// <summary>Gets the count of all queued requests.</summary>
public int Count
{
get { return allRequests.Count; }
}
/// <summary>Queues an individual request.</summary>
/// <typeparam name="TResponse">The response's type.</typeparam>
/// <param name="request">The individual request.</param>
/// <param name="callback">A callback which will be called after a response was parsed.</param>
public void Queue<TResponse>(IClientServiceRequest request, OnResponse<TResponse> callback)
where TResponse : class
{
if (Count > QueueLimit)
{
throw new InvalidOperationException("A batch request cannot contain more than 1000 single requests");
}
allRequests.Add(new InnerRequest<TResponse>
{
ClientRequest = request,
ResponseType = typeof(TResponse),
OnResponseCallback = callback,
});
}
/// <summary>Asynchronously executes the batch request.</summary>
public Task ExecuteAsync()
{
return ExecuteAsync(CancellationToken.None);
}
/// <summary>Asynchronously executes the batch request.</summary>
/// <param name="cancellationToken">Cancellation token to cancel operation.</param>
public async Task ExecuteAsync(CancellationToken cancellationToken)
{
if (Count < 1)
return;
ConfigurableHttpClient httpClient = service.HttpClient;
var requests = from r in allRequests
select r.ClientRequest;
HttpContent outerContent = await CreateOuterRequestContent(requests).ConfigureAwait(false);
var result = await httpClient.PostAsync(new Uri(batchUrl), outerContent, cancellationToken)
.ConfigureAwait(false);
result.EnsureSuccessStatusCode();
// Get the boundary separator.
const string boundaryKey = "boundary=";
var fullContent = await result.Content.ReadAsStringAsync().ConfigureAwait(false);
var contentType = result.Content.Headers.GetValues("Content-Type").First();
var boundary = contentType.Substring(contentType.IndexOf(boundaryKey) + boundaryKey.Length);
int requestIndex = 0;
// While there is still content to read, parse the current HTTP response.
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
var startIndex = fullContent.IndexOf("--" + boundary);
if (startIndex == -1)
{
break;
}
fullContent = fullContent.Substring(startIndex + boundary.Length + 2);
var endIndex = fullContent.IndexOf("--" + boundary);
if (endIndex == -1)
{
break;
}
HttpResponseMessage responseMessage = ParseAsHttpResponse(fullContent.Substring(0, endIndex));
if (responseMessage.IsSuccessStatusCode)
{
// Parse the current content object.
var responseContent = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
var content = service.Serializer.Deserialize(responseContent,
allRequests[requestIndex].ResponseType);
allRequests[requestIndex].OnResponse(content, null, requestIndex, responseMessage);
}
else
{
// Parse the error from the current response.
var error = await service.DeserializeError(responseMessage).ConfigureAwait(false);
allRequests[requestIndex].OnResponse(null, error, requestIndex, responseMessage);
}
requestIndex++;
fullContent = fullContent.Substring(endIndex);
}
}
/// <summary>Parses the given string content to a HTTP response.</summary>
[VisibleForTestOnly]
internal static HttpResponseMessage ParseAsHttpResponse(string content)
{
var response = new HttpResponseMessage();
using (var reader = new StringReader(content))
{
string line = reader.ReadLine();
// Extract empty lines.
while (string.IsNullOrEmpty(line))
line = reader.ReadLine();
// Extract the outer header.
while (!string.IsNullOrEmpty(line))
line = reader.ReadLine();
// Extract the status code.
line = reader.ReadLine();
while (string.IsNullOrEmpty(line))
line = reader.ReadLine();
int code = int.Parse(line.Split(' ')[1]);
response.StatusCode = (HttpStatusCode)code;
// Extract the headers.
IDictionary<string, string> headersDic = new Dictionary<string, string>();
while (!string.IsNullOrEmpty((line = reader.ReadLine())))
{
var separatorIndex = line.IndexOf(':');
var key = line.Substring(0, separatorIndex).Trim();
var value = line.Substring(separatorIndex + 1).Trim();
// Check if the header already exists, and if so append its value
// to the existing value. Fixes issue #548.
if (headersDic.ContainsKey(key)) {
headersDic[key] = headersDic[key] + ", " + value;
} else {
headersDic.Add(key, value);
}
}
// Set the content.
string mediaType = null;
if (headersDic.ContainsKey("Content-Type"))
{
mediaType = headersDic["Content-Type"].Split(';', ' ')[0];
headersDic.Remove("Content-Type");
}
response.Content = new StringContent(reader.ReadToEnd(), Encoding.UTF8, mediaType);
// Add the headers to the response.
foreach (var keyValue in headersDic)
{
HttpHeaders headers = response.Headers;
// Check if we need to add the current header to the content headers.
if (typeof(HttpContentHeaders).GetProperty(keyValue.Key.Replace("-", "")) != null)
{
headers = response.Content.Headers;
}
// Use TryAddWithoutValidation rather than Add because Mono's validation is
// improperly strict. https://bugzilla.xamarin.com/show_bug.cgi?id=39569
if (!headers.TryAddWithoutValidation(keyValue.Key, keyValue.Value))
{
throw new FormatException(String.Format(
"Could not parse header {0} from batch reply", keyValue.Key));
}
}
// TODO(peleyal): ContentLength header is x while the "real" content that we read from the stream is
// Content.ReadStringAsAsync().Length is x+2
}
return response;
}
/// <summary>
/// Creates the batch outer request content which includes all the individual requests to Google servers.
/// </summary>
[VisibleForTestOnly]
internal async static Task<HttpContent> CreateOuterRequestContent(IEnumerable<IClientServiceRequest> requests)
{
var mixedContent = new MultipartContent("mixed");
foreach (var request in requests)
{
mixedContent.Add(await CreateIndividualRequest(request).ConfigureAwait(false));
}
// Batch request currently doesn't support GZip. Uncomment when the issue will be resolved.
// https://code.google.com/p/google-api-dotnet-client/issues/detail?id=409
/*if (service.GZipEnabled)
{
var content = HttpServiceExtenstions.CreateZipContent(await mixedContent.ReadAsStringAsync()
.ConfigureAwait(false));
content.Headers.ContentType = mixedContent.Headers.ContentType;
return content;
}*/
return mixedContent;
}
/// <summary>Creates the individual server request.</summary>
[VisibleForTestOnly]
internal static async Task<HttpContent> CreateIndividualRequest(IClientServiceRequest request)
{
HttpRequestMessage requestMessage = request.CreateRequest(false);
string requestContent = await CreateRequestContentString(requestMessage).ConfigureAwait(false);
var content = new StringContent(requestContent);
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/http");
return content;
}
/// <summary>
/// Creates a string representation that includes the request's headers and content based on the input HTTP
/// request message.
/// </summary>
[VisibleForTestOnly]
internal static async Task<string> CreateRequestContentString(HttpRequestMessage requestMessage)
{
var sb = new StringBuilder();
sb.AppendFormat("{0} {1}", requestMessage.Method, requestMessage.RequestUri.AbsoluteUri);
// Add Headers.
foreach (var otherHeader in requestMessage.Headers)
{
sb.Append(Environment.NewLine)
.AppendFormat(("{0}: {1}"), otherHeader.Key, String.Join(", ", otherHeader.Value.ToArray()));
}
// Add content headers.
if (requestMessage.Content != null)
{
foreach (var contentHeader in requestMessage.Content.Headers)
{
sb.Append(Environment.NewLine)
.AppendFormat("{0}: {1}", contentHeader.Key, String.Join(", ", contentHeader.Value.ToArray()));
}
}
// Content.
if (requestMessage.Content != null)
{
sb.Append(Environment.NewLine);
var content = await requestMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
sb.Append("Content-Length: ").Append(content.Length);
sb.Append(Environment.NewLine).Append(Environment.NewLine).Append(content);
}
return sb.Append(Environment.NewLine).ToString();
}
}
}

View file

@ -0,0 +1,366 @@
/*
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 System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Discovery;
using Google.Apis.Http;
using Google.Apis.Logging;
using Google.Apis.Services;
using Google.Apis.Testing;
using Google.Apis.Util;
using Google.Apis.Requests.Parameters;
namespace Google.Apis.Requests
{
/// <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> : IClientServiceRequest<TResponse>
{
/// <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; }
#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.
throw aex.InnerException;
}
catch (Exception ex)
{
throw ex;
}
}
/// <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;
}
catch (Exception ex)
{
throw ex;
}
}
/// <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.ToString())
{
Error = error,
HttpStatusCode = response.StatusCode
};
}
#endregion
#endregion
/// <inheritdoc/>
public HttpRequestMessage CreateRequest(Nullable<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);
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()
{
return CreateBuilder().BuildUri().ToString();
}
/// <summary>Returns the body of this request.</summary>
/// <returns>The body of this request.</returns>
protected virtual object GetBody()
{
return 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;
try
{
switch (action)
{
case ETagAction.IfMatch:
request.Headers.IfMatch.Add(new EntityTagHeaderValue(etag));
break;
case ETagAction.IfNoneMatch:
request.Headers.IfNoneMatch.Add(new EntityTagHeaderValue(etag));
break;
}
}
// When ETag is invalid we are going to create a request anyway.
// See https://code.google.com/p/google-api-dotnet-client/issues/detail?id=464 for more details.
catch (FormatException ex)
{
Logger.Error(ex, "Can't set {0}. Etag is: {1}.", action, etag);
}
}
}
/// <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)
{
foreach (var parameter in inputParameters)
{
IParameter parameterDefinition;
if (!RequestParameters.TryGetValue(parameter.Key, out parameterDefinition))
{
throw new GoogleApiException(Service.Name,
String.Format("Invalid parameter \"{0}\" was specified", parameter.Key));
}
string value = parameter.Value;
if (!ParameterValidator.ValidateParameter(parameterDefinition, value))
{
throw new GoogleApiException(Service.Name,
string.Format("Parameter validation failed for \"{0}\"", parameterDefinition.Name));
}
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,
string.Format("Unsupported parameter type \"{0}\" for \"{1}\"",
parameterDefinition.ParameterType, 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,
string.Format("Parameter \"{0}\" is missing", parameter.Name));
}
}
}
#endregion
}
}

View file

@ -0,0 +1,97 @@
/*
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.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.IO.Compression;
using Google.Apis.Services;
namespace Google.Apis.Requests
{
/// <summary>Extension methods to <see cref="System.Net.Http.HttpRequestMessage"/>.</summary>
static class HttpRequestMessageExtenstions
{
/// <summary>
/// Sets the content of the request by the given body and the the required GZip configuration.
/// </summary>
/// <param name="request">The request.</param>
/// <param name="service">The service.</param>
/// <param name="body">The body of the future request. If <c>null</c> do nothing.</param>
/// <param name="gzipEnabled">
/// Indicates if the content will be wrapped in a GZip stream, or a regular string stream will be used.
/// </param>
internal static void SetRequestSerailizedContent(this HttpRequestMessage request,
IClientService service, object body, bool gzipEnabled)
{
if (body == null)
{
return;
}
HttpContent content = null;
var mediaType = "application/" + service.Serializer.Format;
var serializedObject = service.SerializeObject(body);
if (gzipEnabled)
{
content = CreateZipContent(serializedObject);
content.Headers.ContentType = new MediaTypeHeaderValue(mediaType)
{
CharSet = Encoding.UTF8.WebName
};
}
else
{
content = new StringContent(serializedObject, Encoding.UTF8, mediaType);
}
request.Content = content;
}
/// <summary>Creates a GZip content based on the given content.</summary>
/// <param name="content">Content to GZip.</param>
/// <returns>GZiped HTTP content.</returns>
internal static HttpContent CreateZipContent(string content)
{
var stream = CreateGZipStream(content);
var sc = new StreamContent(stream);
sc.Headers.ContentEncoding.Add("gzip");
return sc;
}
/// <summary>Creates a GZip stream by the given serialized object.</summary>
private static Stream CreateGZipStream(string serializedObject)
{
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(serializedObject);
using (System.IO.MemoryStream ms = new System.IO.MemoryStream())
{
using (GZipStream gzip = new GZipStream(ms, CompressionMode.Compress, true))
{
gzip.Write(bytes, 0, bytes.Length);
}
// Reset the stream to the beginning. It doesn't work otherwise!
ms.Position = 0;
byte[] compressed = new byte[ms.Length];
ms.Read(compressed, 0, compressed.Length);
return new MemoryStream(compressed);
}
}
}
}

View file

@ -0,0 +1,80 @@
/*
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 System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Discovery;
using Google.Apis.Services;
namespace Google.Apis.Requests
{
/// <summary>A client service request which supports both sync and async execution to get the stream.</summary>
public interface IClientServiceRequest
{
/// <summary>Gets the name of the method to which this request belongs.</summary>
string MethodName { get; }
/// <summary>Gets the rest path of this request.</summary>
string RestPath { get; }
/// <summary>Gets the HTTP method of this request.</summary>
string HttpMethod { get; }
/// <summary>Gets the parameters information for this specific request.</summary>
IDictionary<string, IParameter> RequestParameters { get; }
/// <summary>Gets the service which is related to this request.</summary>
IClientService Service { get; }
/// <summary>Creates a HTTP request message with all path and query parameters, ETag, etc.</summary>
/// <param name="overrideGZipEnabled">
/// If <c>null</c> use the service default GZip behavior. Otherwise indicates if GZip is enabled or disabled.
/// </param>
HttpRequestMessage CreateRequest(Nullable<bool> overrideGZipEnabled = null);
/// <summary>Executes the request asynchronously and returns the result stream.</summary>
Task<Stream> ExecuteAsStreamAsync();
/// <summary>Executes the request asynchronously and returns the result stream.</summary>
/// <param name="cancellationToken">A cancellation token to cancel operation.</param>
Task<Stream> ExecuteAsStreamAsync(CancellationToken cancellationToken);
/// <summary>Executes the request and returns the result stream.</summary>
Stream ExecuteAsStream();
}
/// <summary>
/// A client service request which inherits from <see cref="IClientServiceRequest"/> and represents a specific
/// service request with the given response type. It supports both sync and async execution to get the response.
/// </summary>
public interface IClientServiceRequest<TResponse> : IClientServiceRequest
{
/// <summary>Executes the request asynchronously and returns the result object.</summary>
Task<TResponse> ExecuteAsync();
/// <summary>Executes the request asynchronously and returns the result object.</summary>
/// <param name="cancellationToken">A cancellation token to cancel operation.</param>
Task<TResponse> ExecuteAsync(CancellationToken cancellationToken);
/// <summary>Executes the request and returns the result object.</summary>
TResponse Execute();
}
}

View file

@ -0,0 +1,35 @@
/*
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.
*/
namespace Google.Apis.Requests
{
/// <summary>
/// Interface containing additional response-properties which will be added to every schema type which is
/// a direct response to a request.
/// </summary>
public interface IDirectResponseSchema
{
/// <summary>
/// The e-tag of this response.
/// </summary>
/// <remarks>
/// Will be set by the service deserialization method,
/// or the by json response parser if implemented on service.
/// </remarks>
string ETag { get; set; }
}
}

View file

@ -0,0 +1,159 @@
/*
Copyright 2016 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.Threading;
using System.Threading.Tasks;
namespace Google.Apis.Requests
{
// TODO(jskeet): Make sure one of our samples uses this.
/// <summary>
/// A page streamer is a helper to provide both synchronous and asynchronous page streaming
/// of a listable or queryable resource.
/// </summary>
/// <remarks>
/// <para>
/// The expected usage pattern is to create a single paginator for a resource collection,
/// and then use the instance methods to obtain paginated results.
/// </para>
/// </remarks>
/// <example>
/// To construct a page streamer to return snippets from the YouTube v3 Data API, you might use code
/// such as the following. The pattern for other APIs would be very similar, with the <c>request.PageToken</c>,
/// <c>response.NextPageToken</c> and <c>response.Items</c> properties potentially having different names. Constructing
/// the page streamer doesn't require any service references or authentication, so it's completely safe to perform this
/// in a type initializer.
/// <code><![CDATA[
/// using Google.Apis.YouTube.v3;
/// using Google.Apis.YouTube.v3.Data;
/// ...
/// private static readonly snippetPageStreamer = new PageStreamer<SearchResult, SearchResource.ListRequest, SearchListResponse, string>(
/// (request, token) => request.PageToken = token,
/// response => response.NextPageToken,
/// response => response.Items);
/// ]]></code>
/// </example>
/// <typeparam name="TResource">The type of resource being paginated</typeparam>
/// <typeparam name="TRequest">The type of request used to fetch pages</typeparam>
/// <typeparam name="TResponse">The type of response obtained when fetching pages</typeparam>
/// <typeparam name="TToken">The type of the "next page token", which must be a reference type;
/// a null reference for a token indicates the end of a stream of pages.</typeparam>
public sealed class PageStreamer<TResource, TRequest, TResponse, TToken>
where TToken : class
where TRequest : IClientServiceRequest<TResponse>
{
// Simple way of avoiding NullReferenceException if the response extractor returns null.
private static readonly TResource[] emptyResources = new TResource[0];
private readonly Action<TRequest, TToken> requestModifier;
private readonly Func<TResponse, TToken> tokenExtractor;
private readonly Func<TResponse, IEnumerable<TResource>> resourceExtractor;
/// <summary>
/// Creates a paginator for later use.
/// </summary>
/// <param name="requestModifier">Action to modify a request to include the specified page token.
/// Must not be null.</param>
/// <param name="tokenExtractor">Function to extract the next page token from a response.
/// Must not be null.</param>
/// <param name="resourceExtractor">Function to extract a sequence of resources from a response.
/// Must not be null, although it can return null if it is passed a response which contains no
/// resources.</param>
public PageStreamer(
Action<TRequest, TToken> requestModifier,
Func<TResponse, TToken> tokenExtractor,
Func<TResponse, IEnumerable<TResource>> resourceExtractor)
{
if (requestModifier == null)
{
throw new ArgumentNullException("requestProvider");
}
if (tokenExtractor == null)
{
throw new ArgumentNullException("tokenExtractor");
}
if (resourceExtractor == null)
{
throw new ArgumentNullException("resourceExtractor");
}
this.requestModifier = requestModifier;
this.tokenExtractor = tokenExtractor;
this.resourceExtractor = resourceExtractor;
}
/// <summary>
/// Lazily fetches resources a page at a time.
/// </summary>
/// <param name="request">The initial request to send. If this contains a page token,
/// that token is maintained. This will be modified with new page tokens over time, and should not
/// be changed by the caller. (The caller should clone the request if they want an independent object
/// to use in other calls or to modify.) Must not be null.</param>
/// <returns>A sequence of resources, which are fetched a page at a time. Must not be null.</returns>
public IEnumerable<TResource> Fetch(TRequest request)
{
if (request == null)
{
throw new ArgumentNullException("request");
}
TToken token;
do
{
TResponse response = request.Execute();
token = tokenExtractor(response);
requestModifier(request, token);
foreach (var item in resourceExtractor(response) ?? emptyResources)
{
yield return item;
}
} while (token != null);
}
/// <summary>
/// Asynchronously (but eagerly) fetches a complete set of resources, potentially making multiple requests.
/// </summary>
/// <param name="request">The initial request to send. If this contains a page token,
/// that token is maintained. This will be modified with new page tokens over time, and should not
/// be changed by the caller. (The caller should clone the request if they want an independent object
/// to use in other calls or to modify.) Must not be null.</param>
/// <returns>A sequence of resources, which are fetched asynchronously and a page at a time.</returns>
/// <param name="cancellationToken"></param>
/// <returns>A task whose result (when complete) is the complete set of results fetched starting with the given
/// request, and continuing to make further requests until a response has no "next page" token.</returns>
public async Task<IList<TResource>> FetchAllAsync(
TRequest request,
CancellationToken cancellationToken)
{
if (request == null)
{
throw new ArgumentNullException("request");
}
var results = new List<TResource>();
TToken token;
do
{
cancellationToken.ThrowIfCancellationRequested();
TResponse response = await request.ExecuteAsync(cancellationToken).ConfigureAwait(false);
token = tokenExtractor(response);
requestModifier(request, token);
results.AddRange(resourceExtractor(response) ?? emptyResources);
} while (token != null);
return results;
}
}
}