Google Api
This commit is contained in:
parent
e307811d89
commit
83f9fc1bd8
123 changed files with 12961 additions and 60 deletions
383
Yavsc/GoogleApiSupport/Google.Apis/Requests/BatchRequest.cs
Normal file
383
Yavsc/GoogleApiSupport/Google.Apis/Requests/BatchRequest.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
159
Yavsc/GoogleApiSupport/Google.Apis/Requests/PageStreamer.cs
Normal file
159
Yavsc/GoogleApiSupport/Google.Apis/Requests/PageStreamer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue