diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureAsyncOperationPollStrategy.java b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureAsyncOperationPollStrategy.java index fc0bba09d3076..b68ebde9e1535 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureAsyncOperationPollStrategy.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureAsyncOperationPollStrategy.java @@ -6,8 +6,8 @@ package com.microsoft.azure.v2; -import com.microsoft.rest.v2.RestProxy; import com.microsoft.rest.v2.SwaggerMethodParser; +import com.microsoft.rest.v2.RestProxy; import com.microsoft.rest.v2.http.HttpMethod; import com.microsoft.rest.v2.http.HttpRequest; import com.microsoft.rest.v2.http.HttpResponse; @@ -15,6 +15,7 @@ import io.reactivex.functions.Function; import java.io.IOException; +import java.io.Serializable; import java.net.MalformedURLException; import java.net.URL; @@ -23,12 +24,7 @@ * running operation. */ public final class AzureAsyncOperationPollStrategy extends PollStrategy { - private final URL operationResourceUrl; - private final URL originalResourceUrl; - - private boolean pollingCompleted; - private boolean pollingSucceeded; - private boolean gotResourceResponse; + private AzureAsyncOperationPollStrategyData data; /** * The name of the header that indicates that a long running operation will use the @@ -39,27 +35,55 @@ public final class AzureAsyncOperationPollStrategy extends PollStrategy { /** * Create a new AzureAsyncOperationPollStrategy object that will poll the provided operation * resource URL. - * @param operationResourceUrl The URL of the operation resource this pollStrategy will poll. - * @param originalResourceUrl The URL of the resource that the long running operation is - * operating on. - * @param delayInMilliseconds The delay (in milliseconds) that the pollStrategy will use when - * polling. + * @param data The AzureAsyncOperationPollStrategyData data object. + */ + private AzureAsyncOperationPollStrategy(AzureAsyncOperationPollStrategyData data) { + super(data); + this.data = data; + } + + /** + * The AzureAsyncOperationPollStrategy data. */ - private AzureAsyncOperationPollStrategy(RestProxy restProxy, SwaggerMethodParser methodParser, URL operationResourceUrl, URL originalResourceUrl, long delayInMilliseconds) { - super(restProxy, methodParser, delayInMilliseconds); + private static class AzureAsyncOperationPollStrategyData extends PollStrategyData { + private boolean pollingCompleted; + private boolean pollingSucceeded; + private boolean gotResourceResponse; + + URL operationResourceUrl; + URL originalResourceUrl; + + /** + * Create a new AzureAsyncOperationPollStrategyData object that will poll the provided operation + * resource URL. + * @param operationResourceUrl The URL of the operation resource this pollStrategy will poll. + * @param originalResourceUrl The URL of the resource that the long running operation is + * operating on. + * @param delayInMilliseconds The delay (in milliseconds) that the pollStrategy will use when + * polling. + */ + AzureAsyncOperationPollStrategyData(RestProxy restProxy, SwaggerMethodParser methodParser, URL operationResourceUrl, URL originalResourceUrl, long delayInMilliseconds) { + super(restProxy, methodParser, delayInMilliseconds); + this.operationResourceUrl = operationResourceUrl; + this.originalResourceUrl = originalResourceUrl; + } - this.operationResourceUrl = operationResourceUrl; - this.originalResourceUrl = originalResourceUrl; + PollStrategy initializeStrategy(RestProxy restProxy, + SwaggerMethodParser methodParser) { + this.restProxy = restProxy; + this.methodParser = methodParser; + return new AzureAsyncOperationPollStrategy(this); + } } @Override public HttpRequest createPollRequest() { URL pollUrl; - if (!pollingCompleted) { - pollUrl = operationResourceUrl; + if (!data.pollingCompleted) { + pollUrl = data.operationResourceUrl; } - else if (pollingSucceeded) { - pollUrl = originalResourceUrl; + else if (data.pollingSucceeded) { + pollUrl = data.originalResourceUrl; } else { throw new IllegalStateException("Polling is completed and did not succeed. Cannot create a polling request."); } @@ -76,7 +100,7 @@ public Single apply(HttpResponse response) { updateDelayInMillisecondsFrom(response); Single result; - if (!pollingCompleted) { + if (!data.pollingCompleted) { final HttpResponse bufferedHttpPollResponse = response.buffer(); result = bufferedHttpPollResponse.bodyAsStringAsync() .map(new Function() { @@ -97,17 +121,17 @@ public HttpResponse apply(String bodyString) { final String status = operationResource.status(); setStatus(status); - pollingCompleted = OperationState.isCompleted(status); - if (pollingCompleted) { - pollingSucceeded = OperationState.SUCCEEDED.equalsIgnoreCase(status); + data.pollingCompleted = OperationState.isCompleted(status); + if (data.pollingCompleted) { + data.pollingSucceeded = OperationState.SUCCEEDED.equalsIgnoreCase(status); clearDelayInMilliseconds(); - if (!pollingSucceeded) { + if (!data.pollingSucceeded) { throw new CloudException("Async operation failed with provisioning state: " + status, bufferedHttpPollResponse); } if (operationResource.id() != null) { - gotResourceResponse = true; + data.gotResourceResponse = true; } } } @@ -117,8 +141,8 @@ public HttpResponse apply(String bodyString) { }); } else { - if (pollingSucceeded) { - gotResourceResponse = true; + if (data.pollingSucceeded) { + data.gotResourceResponse = true; } result = Single.just(response); @@ -131,7 +155,7 @@ public HttpResponse apply(String bodyString) { @Override public boolean isDone() { - return pollingCompleted && (!pollingSucceeded || !expectsResourceResponse() || gotResourceResponse); + return data.pollingCompleted && (!data.pollingSucceeded || !expectsResourceResponse() || data.gotResourceResponse); } /** @@ -159,11 +183,17 @@ static PollStrategy tryToCreate(RestProxy restProxy, SwaggerMethodParser methodP } return azureAsyncOperationUrl != null - ? new AzureAsyncOperationPollStrategy(restProxy, methodParser, azureAsyncOperationUrl, originalHttpRequest.url(), delayInMilliseconds) + ? new AzureAsyncOperationPollStrategy( + new AzureAsyncOperationPollStrategyData(restProxy, methodParser, azureAsyncOperationUrl, originalHttpRequest.url(), delayInMilliseconds)) : null; } static String getHeader(HttpResponse httpResponse) { return httpResponse.headerValue(HEADER_NAME); } + + @Override + public Serializable strategyData() { + return this.data; + } } \ No newline at end of file diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureProxy.java b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureProxy.java index 1c19c3f61bfb0..3265ffea2d441 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureProxy.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureProxy.java @@ -14,6 +14,7 @@ import com.microsoft.rest.v2.RestProxy; import com.microsoft.rest.v2.SwaggerInterfaceParser; import com.microsoft.rest.v2.SwaggerMethodParser; +import com.microsoft.rest.v2.OperationDescription; import com.microsoft.rest.v2.credentials.ServiceClientCredentials; import com.microsoft.rest.v2.http.HttpClient; import com.microsoft.rest.v2.http.HttpMethod; @@ -258,7 +259,8 @@ protected Object handleAsyncHttpResponse(final HttpRequest httpRequest, Single apply(HttpResponse httpResponse) throws Exception { final HttpResponse bufferedHttpResponse = httpResponse.buffer(); - return createPollStrategy(httpRequest, Single.just(bufferedHttpResponse), methodParser).flatMapObservable(new Function>>() { + return createPollStrategy(httpRequest, Single.just(bufferedHttpResponse), methodParser) + .flatMapObservable(new Function>>() { @Override public ObservableSource> apply(final PollStrategy pollStrategy) throws Exception { Observable> first = handleBodyReturnTypeAsync(bufferedHttpResponse, methodParser, operationStatusResultType) @@ -295,6 +297,24 @@ public Single apply(PollStrategy pollStrategy) { return result; } + @Override + protected Object handleResumeOperation(final HttpRequest httpRequest, + OperationDescription operationDescription, + final SwaggerMethodParser methodParser, + Type returnType) + throws Exception { + final Type operationStatusType = ((ParameterizedType) returnType).getActualTypeArguments()[0]; + final TypeToken operationStatusTypeToken = TypeToken.of(operationStatusType); + if (!operationStatusTypeToken.isSubtypeOf(OperationStatus.class)) { + throw new InvalidReturnTypeException("AzureProxy only supports swagger interface methods that return Observable (such as " + methodParser.fullyQualifiedMethodName() + "()) if the Observable's inner type that is OperationStatus (not " + returnType.toString() + ")."); + } + + PollStrategy.PollStrategyData pollStrategyData = + (PollStrategy.PollStrategyData) operationDescription.pollStrategyData(); + PollStrategy pollStrategy = pollStrategyData.initializeStrategy(this, methodParser); + return pollStrategy.pollUntilDoneWithStatusUpdates(httpRequest, methodParser, operationStatusType); + } + private Single createPollStrategy(final HttpRequest originalHttpRequest, final Single asyncOriginalHttpResponse, final SwaggerMethodParser methodParser) { return asyncOriginalHttpResponse .flatMap(new Function>() { @@ -349,7 +369,8 @@ else if (originalHttpRequestMethod == HttpMethod.PUT || originalHttpRequestMetho } if (pollStrategy == null && result == null) { - pollStrategy = new CompletedPollStrategy(AzureProxy.this, methodParser, originalHttpResponse); + pollStrategy = new CompletedPollStrategy( + new CompletedPollStrategy.CompletedPollStrategyData(AzureProxy.this, methodParser, originalHttpResponse)); } if (pollStrategy != null) { @@ -371,7 +392,8 @@ private Single createProvisioningStateOrCompletedPollStrategy(fina || httpRequestMethod == HttpMethod.GET || httpRequestMethod == HttpMethod.HEAD || !methodParser.expectsResponseBody()) { - result = Single.just(new CompletedPollStrategy(AzureProxy.this, methodParser, httpResponse)); + result = Single.just(new CompletedPollStrategy( + new CompletedPollStrategy.CompletedPollStrategyData(AzureProxy.this, methodParser, httpResponse))); } else { final HttpResponse bufferedOriginalHttpResponse = httpResponse.buffer(); result = bufferedOriginalHttpResponse.bodyAsStringAsync() @@ -387,9 +409,13 @@ public PollStrategy apply(String originalHttpResponseBody) { final SerializerAdapter serializer = serializer(); final ResourceWithProvisioningState resource = serializer.deserialize(originalHttpResponseBody, ResourceWithProvisioningState.class, SerializerEncoding.JSON); if (resource != null && resource.properties() != null && !OperationState.isCompleted(resource.properties().provisioningState())) { - result = new ProvisioningStatePollStrategy(AzureProxy.this, methodParser, httpRequest, resource.properties().provisioningState(), delayInMilliseconds); + result = new ProvisioningStatePollStrategy( + new ProvisioningStatePollStrategy.ProvisioningStatePollStrategyData( + AzureProxy.this, methodParser, httpRequest, resource.properties().provisioningState(), delayInMilliseconds)); } else { - result = new CompletedPollStrategy(AzureProxy.this, methodParser, bufferedOriginalHttpResponse); + result = new CompletedPollStrategy( + new CompletedPollStrategy.CompletedPollStrategyData( + AzureProxy.this, methodParser, bufferedOriginalHttpResponse)); } } catch (IOException e) { throw Exceptions.propagate(e); diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/CompletedPollStrategy.java b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/CompletedPollStrategy.java index 1a8615006323e..c4bc7c66e258e 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/CompletedPollStrategy.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/CompletedPollStrategy.java @@ -14,6 +14,7 @@ import io.reactivex.Observable; import io.reactivex.Single; +import java.io.Serializable; import java.lang.reflect.Type; /** @@ -22,20 +23,46 @@ */ public class CompletedPollStrategy extends PollStrategy { private final BufferedHttpResponse firstHttpResponse; + private CompletedPollStrategyData data; /** * Create a new CompletedPollStrategy. - * @param restProxy The RestProxy that created this PollStrategy. - * @param methodParser The method parser that describes the service interface method that - * initiated the long running operation. - * @param firstHttpResponse The HTTP response to the original HTTP request. + * @param data The poll strategy data. */ - public CompletedPollStrategy(RestProxy restProxy, SwaggerMethodParser methodParser, HttpResponse firstHttpResponse) { - super(restProxy, methodParser, 0); - this.firstHttpResponse = firstHttpResponse.buffer(); + public CompletedPollStrategy(CompletedPollStrategyData data) { + super(data); + this.firstHttpResponse = data.firstHttpResponse.buffer(); setStatus(OperationState.SUCCEEDED); + this.data = data; } + /** + * The CompletedPollStrategy data. + */ + public static class CompletedPollStrategyData extends PollStrategyData { + HttpResponse firstHttpResponse; + + /** + * Create a new CompletedPollStrategyData. + * @param restProxy The RestProxy that created this PollStrategy. + * @param methodParser The method parser that describes the service interface method that + * initiated the long running operation. + * @param firstHttpResponse The HTTP response to the original HTTP request. + */ + public CompletedPollStrategyData(RestProxy restProxy, SwaggerMethodParser methodParser, HttpResponse firstHttpResponse) { + super(restProxy, methodParser, 0); + this.firstHttpResponse = firstHttpResponse; + } + + PollStrategy initializeStrategy(RestProxy restProxy, + SwaggerMethodParser methodParser) { + this.restProxy = restProxy; + this.methodParser = methodParser; + return new CompletedPollStrategy(this); + } + } + + public @Override HttpRequest createPollRequest() { throw new UnsupportedOperationException(); @@ -58,4 +85,9 @@ Observable> pollUntilDoneWithStatusUpdates(final HttpReq Single pollUntilDone() { return Single.just(firstHttpResponse); } + + @Override + public Serializable strategyData() { + return this.data; + } } diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/LocationPollStrategy.java b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/LocationPollStrategy.java index 136cccce269b9..a854cf25a66af 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/LocationPollStrategy.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/LocationPollStrategy.java @@ -14,6 +14,7 @@ import io.reactivex.Single; import io.reactivex.functions.Function; +import java.io.Serializable; import java.net.MalformedURLException; import java.net.URL; @@ -22,8 +23,7 @@ * operation. */ public final class LocationPollStrategy extends PollStrategy { - private URL locationUrl; - private boolean done; + LocationPollStrategyData data; /** * The name of the header that indicates that a long running operation will use the Location @@ -31,15 +31,53 @@ public final class LocationPollStrategy extends PollStrategy { */ public static final String HEADER_NAME = "Location"; - private LocationPollStrategy(RestProxy restProxy, SwaggerMethodParser methodParser, URL locationUrl, long delayInMilliseconds) { - super(restProxy, methodParser, delayInMilliseconds); + private LocationPollStrategy(LocationPollStrategyData data) { + super(data); + this.data = data; + } + + /** + * The LocationPollStrategy data. + */ + public static class LocationPollStrategyData extends PollStrategyData { + URL locationUrl; + boolean done; + + /** + * Create a new LocationPollStrategyData. + */ + public LocationPollStrategyData() { + super(null, null, 0); + this.locationUrl = null; + } - this.locationUrl = locationUrl; + /** + * Create a new LocationPollStrategyData. + * @param restProxy The RestProxy that created this PollStrategy. + * @param methodParser The method parser that describes the service interface method that + * initiated the long running operation. + * @param locationUrl The location url. + * @param delayInMilliseconds The delay value. + */ + public LocationPollStrategyData(RestProxy restProxy, + SwaggerMethodParser methodParser, + URL locationUrl, + long delayInMilliseconds) { + super(restProxy, methodParser, delayInMilliseconds); + this.locationUrl = locationUrl; + } + + PollStrategy initializeStrategy(RestProxy restProxy, + SwaggerMethodParser methodParser) { + this.restProxy = restProxy; + this.methodParser = methodParser; + return new LocationPollStrategy(this); + } } @Override public HttpRequest createPollRequest() { - return new HttpRequest(fullyQualifiedMethodName(), HttpMethod.GET, locationUrl, createResponseDecoder()); + return new HttpRequest(fullyQualifiedMethodName(), HttpMethod.GET, data.locationUrl, createResponseDecoder()); } @Override @@ -55,11 +93,11 @@ public HttpResponse apply(HttpResponse response) throws MalformedURLException { if (httpStatusCode == 202) { String newLocationUrl = getHeader(response); if (newLocationUrl != null) { - locationUrl = new URL(newLocationUrl); + data.locationUrl = new URL(newLocationUrl); } } else { - done = true; + data.done = true; } return response; } @@ -68,7 +106,7 @@ public HttpResponse apply(HttpResponse response) throws MalformedURLException { @Override public boolean isDone() { - return done; + return data.done; } /** @@ -108,10 +146,16 @@ static PollStrategy tryToCreate(RestProxy restProxy, SwaggerMethodParser methodP return pollUrl == null ? null - : new LocationPollStrategy(restProxy, methodParser, pollUrl, delayInMilliseconds); + : new LocationPollStrategy( + new LocationPollStrategyData(restProxy, methodParser, pollUrl, delayInMilliseconds)); } static String getHeader(HttpResponse httpResponse) { return httpResponse.headerValue(HEADER_NAME); } + + @Override + public Serializable strategyData() { + return this.data; + } } \ No newline at end of file diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/OperationStatus.java b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/OperationStatus.java index ea65142094ce0..131563fc49bc1 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/OperationStatus.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/OperationStatus.java @@ -6,7 +6,9 @@ package com.microsoft.azure.v2; +import com.microsoft.rest.v2.OperationDescription; import com.microsoft.rest.v2.RestException; +import com.microsoft.rest.v2.http.HttpRequest; /** * The current state of polling for the result of a long running operation. @@ -14,6 +16,7 @@ */ public class OperationStatus { private final PollStrategy pollStrategy; + private final HttpRequest originalHttpRequest; private final T result; private final RestException error; private final String status; @@ -23,7 +26,8 @@ public class OperationStatus { * @param pollStrategy The polling strategy that the OperationStatus will use to check the * progress of a long running operation. */ - OperationStatus(PollStrategy pollStrategy) { + OperationStatus(PollStrategy pollStrategy, HttpRequest originalHttpRequest) { + this.originalHttpRequest = originalHttpRequest; this.pollStrategy = pollStrategy; this.result = null; this.error = null; @@ -36,6 +40,7 @@ public class OperationStatus { */ OperationStatus(T result, String provisioningState) { this.pollStrategy = null; + this.originalHttpRequest = null; this.result = result; this.error = null; this.status = provisioningState; @@ -43,6 +48,7 @@ public class OperationStatus { OperationStatus(RestException error, String provisioningState) { this.pollStrategy = null; + this.originalHttpRequest = null; this.result = null; this.error = error; this.status = provisioningState; @@ -79,4 +85,20 @@ public T result() { public RestException error() { return error; } + + /** + * Builds an object that can be used to resume the polling of the operation. + * @return The OperationDescription. + */ + public OperationDescription buildDescription() { + if (this.isDone()) { + return null; + } + + return new OperationDescription( + this.pollStrategy.methodParser().fullyQualifiedMethodName(), + this.pollStrategy.strategyData(), + this.originalHttpRequest); + } + } diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/PollStrategy.java b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/PollStrategy.java index b11e683f4b207..7fa8f22adcd7a 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/PollStrategy.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/PollStrategy.java @@ -20,6 +20,7 @@ import io.reactivex.functions.Predicate; import java.io.IOException; +import java.io.Serializable; import java.lang.reflect.Type; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; @@ -35,10 +36,28 @@ abstract class PollStrategy { private long delayInMilliseconds; private String status; - PollStrategy(RestProxy restProxy, SwaggerMethodParser methodParser, long delayInMilliseconds) { - this.restProxy = restProxy; - this.methodParser = methodParser; - this.delayInMilliseconds = delayInMilliseconds; + PollStrategy(PollStrategyData data) { + this.restProxy = data.restProxy; + this.methodParser = data.methodParser; + this.delayInMilliseconds = data.delayInMilliseconds; + } + + abstract static class PollStrategyData implements Serializable { + transient RestProxy restProxy; + transient SwaggerMethodParser methodParser; + long delayInMilliseconds; + + PollStrategyData(RestProxy restProxy, + SwaggerMethodParser methodParser, + long delayInMilliseconds) { + this.restProxy = restProxy; + this.methodParser = methodParser; + this.delayInMilliseconds = delayInMilliseconds; + } + + + abstract PollStrategy initializeStrategy(RestProxy restProxy, + SwaggerMethodParser methodParser); } @SuppressWarnings("unchecked") @@ -172,7 +191,7 @@ public Single apply(HttpResponse response) { Observable> createOperationStatusObservable(HttpRequest httpRequest, HttpResponse httpResponse, SwaggerMethodParser methodParser, Type operationStatusResultType) { OperationStatus operationStatus; if (!isDone()) { - operationStatus = new OperationStatus<>(this); + operationStatus = new OperationStatus<>(this, httpRequest); } else { try { @@ -213,4 +232,13 @@ public boolean test(HttpResponse ignored) { }) .lastOrError(); } + + /** + * @erturn The data for the strategy. + */ + public abstract Serializable strategyData(); + + SwaggerMethodParser methodParser() { + return this.methodParser; + } } \ No newline at end of file diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/ProvisioningStatePollStrategy.java b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/ProvisioningStatePollStrategy.java index 0525c15e2ddac..21f9ff48dd078 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/ProvisioningStatePollStrategy.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/ProvisioningStatePollStrategy.java @@ -15,26 +15,58 @@ import io.reactivex.functions.Function; import java.io.IOException; +import java.io.Serializable; /** * A PollStrategy that will continue to poll a resource's URL until the resource's provisioning * state property is in a completed state. */ public final class ProvisioningStatePollStrategy extends PollStrategy { - private final HttpRequest originalRequest; - private final SwaggerMethodParser methodParser; + private ProvisioningStatePollStrategyData data; + ProvisioningStatePollStrategy(ProvisioningStatePollStrategyData data) { + super(data); + setStatus(data.provisioningState); + this.data = data; + } + + /** + * The ProvisioningStatePollStrategy data. + */ + public static class ProvisioningStatePollStrategyData extends PollStrategy.PollStrategyData { + HttpRequest originalRequest; + String provisioningState; + + /** + * Create a new ProvisioningStatePollStrategyData. + * @param restProxy The RestProxy that created this PollStrategy. + * @param methodParser The method parser that describes the service interface method that + * initiated the long running operation. + * @param originalRequest The HTTP response to the original HTTP request. + * @param provisioningState The provisioning state. + * @param delayInMilliseconds The delay value. + */ + public ProvisioningStatePollStrategyData(RestProxy restProxy, + SwaggerMethodParser methodParser, + HttpRequest originalRequest, + String provisioningState, + long delayInMilliseconds) { + super(restProxy, methodParser, delayInMilliseconds); + this.originalRequest = originalRequest; + this.provisioningState = provisioningState; + } - ProvisioningStatePollStrategy(RestProxy restProxy, SwaggerMethodParser methodParser, HttpRequest originalRequest, String provisioningState, long delayInMilliseconds) { - super(restProxy, methodParser, delayInMilliseconds); + PollStrategy initializeStrategy(RestProxy restProxy, + SwaggerMethodParser methodParser) { + this.restProxy = restProxy; + this.methodParser = methodParser; + return new ProvisioningStatePollStrategy(this); + } - this.originalRequest = originalRequest; - this.methodParser = methodParser; - setStatus(provisioningState); } @Override HttpRequest createPollRequest() { - return new HttpRequest(originalRequest.callerMethod(), HttpMethod.GET, originalRequest.url(), createResponseDecoder()); + return new HttpRequest(data.originalRequest.callerMethod(), HttpMethod.GET, data.originalRequest.url(), createResponseDecoder()); } @Override @@ -74,4 +106,9 @@ else if (OperationState.isFailedOrCanceled(resource.properties().provisioningSta boolean isDone() { return OperationState.isCompleted(status()); } + + @Override + public Serializable strategyData() { + return this.data; + } } \ No newline at end of file diff --git a/azure-client-runtime/src/test/java/com/microsoft/azure/v2/AzureProxyTests.java b/azure-client-runtime/src/test/java/com/microsoft/azure/v2/AzureProxyTests.java index dfa8e605daa75..173b719e2fb22 100644 --- a/azure-client-runtime/src/test/java/com/microsoft/azure/v2/AzureProxyTests.java +++ b/azure-client-runtime/src/test/java/com/microsoft/azure/v2/AzureProxyTests.java @@ -1,7 +1,12 @@ package com.microsoft.azure.v2; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.microsoft.azure.v2.http.MockAzureHttpClient; import com.microsoft.azure.v2.http.MockAzureHttpResponse; +import com.microsoft.rest.v2.OperationDescription; import com.microsoft.rest.v2.http.HttpPipeline; import com.microsoft.rest.v2.RestException; import com.microsoft.rest.v2.http.HttpRequest; @@ -16,6 +21,7 @@ import com.microsoft.rest.v2.annotations.Host; import com.microsoft.rest.v2.annotations.PUT; import com.microsoft.rest.v2.annotations.PathParam; +import com.microsoft.rest.v2.annotations.ResumeOperation; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -24,6 +30,7 @@ import io.reactivex.Single; import io.reactivex.functions.Consumer; +import java.io.IOException; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.Assert.*; @@ -120,6 +127,10 @@ private interface MockResourceService { @ExpectedResponses({200}) Observable> beginCreateAsyncWithLocationAndPolls(@PathParam("subscriptionId") String subscriptionId, @PathParam("resourceGroupName") String resourceGroupName, @PathParam("mockResourceName") String mockResourceName, @PathParam("pollsRemaining") int pollsUntilResource); + @ExpectedResponses({200}) + @ResumeOperation + Observable> resumeCreateAsyncWithLocationAndPolls(OperationDescription operationDescription); + @PUT("subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/mockprovider/mockresources/{mockResourceName}?PollType=Azure-AsyncOperation&PollsRemaining={pollsRemaining}") @ExpectedResponses({200}) Observable> beginCreateAsyncWithAzureAsyncOperationAndPolls(@PathParam("subscriptionId") String subscriptionId, @PathParam("resourceGroupName") String resourceGroupName, @PathParam("mockResourceName") String mockResourceName, @PathParam("pollsRemaining") int pollsUntilResource); @@ -515,6 +526,76 @@ public void accept(OperationStatus operationStatus) { assertEquals(3, httpClient.pollRequests()); } + @Test + public void beginAndResumeCreateAsyncWithLocationAndPolls() { + final MockAzureHttpClient httpClient = new MockAzureHttpClient(); + + final AtomicInteger inProgressCount = new AtomicInteger(); + final Value resource = new Value<>(); + final StringBuffer data = new StringBuffer(); + final ObjectMapper mapper = new ObjectMapper(); + mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); + mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + mapper.enableDefaultTyping(); + + createMockService(MockResourceService.class, httpClient) + .beginCreateAsyncWithLocationAndPolls("1", "mine", "c", 10) + .take(2) + .subscribe(new Consumer>() { + @Override + public void accept(OperationStatus operationStatus) { + if (!operationStatus.isDone()) { + OperationDescription operationDescription = operationStatus.buildDescription(); + try { + data.append(mapper.writeValueAsString(operationDescription)); + } catch (JsonProcessingException e) { + fail("Error serializing OperationDescription object"); + e.printStackTrace(); + } + inProgressCount.incrementAndGet(); + } + else { + resource.set(operationStatus.result()); + } + } + }); + + OperationDescription operationDescription = null; + PollStrategy.PollStrategyData pollData = null; + try { + operationDescription = mapper.readValue(data.toString(), OperationDescription.class); + pollData = (PollStrategy.PollStrategyData)operationDescription.pollStrategyData(); + } catch (IOException e) { + fail("Error deserializing OperationDescription object"); + e.printStackTrace(); + } + + assertNotNull(operationDescription); + assertNotNull(pollData); + + createMockService(MockResourceService.class, httpClient) + .resumeCreateAsyncWithLocationAndPolls(operationDescription) + .subscribe(new Consumer>() { + @Override + public void accept(OperationStatus operationStatus) { + if (!operationStatus.isDone()) { + OperationDescription operationDescription = operationStatus.buildDescription(); + try { + data.append(mapper.writeValueAsString(operationDescription)); + } catch (JsonProcessingException e) { + fail("Error serializing OperationDescription object"); + e.printStackTrace(); + } + inProgressCount.incrementAndGet(); + } + else { + resource.set(operationStatus.result()); + } + } + }); + + } + @Test public void beginCreateAsyncWithAzureAsyncOperationAndPolls() { final MockAzureHttpClient httpClient = new MockAzureHttpClient(); diff --git a/client-runtime/src/main/java/com/microsoft/rest/v2/OperationDescription.java b/client-runtime/src/main/java/com/microsoft/rest/v2/OperationDescription.java new file mode 100644 index 0000000000000..188babf5ad8a0 --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/v2/OperationDescription.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.rest.v2; + +import com.microsoft.rest.v2.http.HttpRequest; + +import java.io.Serializable; +import java.net.URL; +import java.util.Map; + +/** + * This class contains the data from an originating operation + * that can be used to resume the polling of the original operation. + */ +public class OperationDescription implements Serializable { + private Serializable pollStrategyData; + private Map headers; + private String httpMethod; + private URL url; + private String fullyQualifiedMethodName; + + /** + * Create a new OperationDescription. + */ + public OperationDescription() { + this.fullyQualifiedMethodName = null; + this.pollStrategyData = null; + this.headers = null; + this.url = null; + this.httpMethod = null; + } + + /** + * Create a new Substitution. + * @param fullyQualifiedMethodName The fully qualified method name from the originating call. + * @param pollStrategyData The data for the originating methods polling strategy. + * @param originalHttpRequest The initial http request from the originating call. + */ + public OperationDescription(String fullyQualifiedMethodName, + Serializable pollStrategyData, + HttpRequest originalHttpRequest) { + this.fullyQualifiedMethodName = fullyQualifiedMethodName; + this.pollStrategyData = pollStrategyData; + this.headers = originalHttpRequest.headers().toMap(); + this.url = originalHttpRequest.url(); + this.httpMethod = originalHttpRequest.httpMethod().toString(); + } + + /** + * @return the Serializable poll strategy data. + */ + public Serializable pollStrategyData() { + return this.pollStrategyData; + } + + /** + * @return the originating requests url. + */ + public URL url() { + return this.url; + } + + /** + * @return the originating requests http method. + */ + public String httpMethod() { + return this.httpMethod; + } + + /** + * @return the originating requests headers. + */ + public Map headers() { + return this.headers; + } + + /** + * @return the originating method name. + */ + String methodName() { + int lastIndex = this.fullyQualifiedMethodName.lastIndexOf("."); + return this.fullyQualifiedMethodName.substring(lastIndex + 1); + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/v2/RestProxy.java b/client-runtime/src/main/java/com/microsoft/rest/v2/RestProxy.java index c9197b3ce1c25..226c6f70391be 100644 --- a/client-runtime/src/main/java/com/microsoft/rest/v2/RestProxy.java +++ b/client-runtime/src/main/java/com/microsoft/rest/v2/RestProxy.java @@ -7,6 +7,7 @@ package com.microsoft.rest.v2; import com.google.common.reflect.TypeToken; +import com.microsoft.rest.v2.annotations.ResumeOperation; import com.microsoft.rest.v2.credentials.ServiceClientCredentials; import com.microsoft.rest.v2.http.ContentType; import com.microsoft.rest.v2.http.HttpHeader; @@ -104,14 +105,32 @@ public Single sendHttpRequestAsync(HttpRequest request) { @Override public Object invoke(Object proxy, final Method method, Object[] args) { try { - final SwaggerMethodParser methodParser = methodParser(method); + SwaggerMethodParser methodParser = null; + HttpRequest request = null; + if (method.isAnnotationPresent(ResumeOperation.class)) { + OperationDescription opDesc = (OperationDescription) args[0]; + Method resumeMethod = null; + Method[] methods = method.getDeclaringClass().getMethods(); + for (Method origMethod : methods) { + if (origMethod.getName().equals(opDesc.methodName())) { + resumeMethod = origMethod; + break; + } + } - final HttpRequest request = createHttpRequest(methodParser, args); + methodParser = methodParser(resumeMethod); + request = createHttpRequest(opDesc, methodParser, args); + final Type returnType = methodParser.returnType(); + return handleResumeOperation(request, opDesc, methodParser, returnType); - final Single asyncResponse = sendHttpRequestAsync(request); + } else { + methodParser = methodParser(method); + request = createHttpRequest(methodParser, args); + final Single asyncResponse = sendHttpRequestAsync(request); + final Type returnType = methodParser.returnType(); + return handleAsyncHttpResponse(request, asyncResponse, methodParser, returnType); + } - final Type returnType = methodParser.returnType(); - return handleAsyncHttpResponse(request, asyncResponse, methodParser, returnType); } catch (Exception e) { throw Exceptions.propagate(e); } @@ -215,6 +234,78 @@ else if (bodyContentObject instanceof String) { return request; } + /** + * Create a HttpRequest for the provided Swagger method using the provided arguments. + * @param methodParser The Swagger method parser to use. + * @param args The arguments to use to populate the method's annotation values. + * @return A HttpRequest. + * @throws IOException Thrown if the body contents cannot be serialized. + */ + @SuppressWarnings("unchecked") + private HttpRequest createHttpRequest(OperationDescription operationDescription, SwaggerMethodParser methodParser, Object[] args) throws IOException { + final HttpRequest request = new HttpRequest( + methodParser.fullyQualifiedMethodName(), + methodParser.httpMethod(), + operationDescription.url(), + new HttpResponseDecoder(methodParser, serializer)); + + final Object bodyContentObject = methodParser.body(args); + if (bodyContentObject == null) { + request.headers().set("Content-Length", "0"); + } else { + String contentType = methodParser.bodyContentType(); + if (contentType == null || contentType.isEmpty()) { + if (bodyContentObject instanceof byte[] || bodyContentObject instanceof String) { + contentType = ContentType.APPLICATION_OCTET_STREAM; + } + else { + contentType = ContentType.APPLICATION_JSON; + } + } + + request.headers().set("Content-Type", contentType); + + boolean isJson = false; + final String[] contentTypeParts = contentType.split(";"); + for (String contentTypePart : contentTypeParts) { + if (contentTypePart.trim().equalsIgnoreCase(ContentType.APPLICATION_JSON)) { + isJson = true; + break; + } + } + + if (isJson) { + final String bodyContentString = serializer.serialize(bodyContentObject, SerializerEncoding.JSON); + request.withBody(bodyContentString); + } + else if (FlowableUtil.isFlowableByteBuffer(TypeToken.of(methodParser.bodyJavaType()))) { + // Content-Length or Transfer-Encoding: chunked must be provided by a user-specified header when a Flowable is given for the body. + //noinspection ConstantConditions + request.withBody((Flowable) bodyContentObject); + } + else if (bodyContentObject instanceof byte[]) { + request.withBody((byte[]) bodyContentObject); + } + else if (bodyContentObject instanceof String) { + final String bodyContentString = (String) bodyContentObject; + if (!bodyContentString.isEmpty()) { + request.withBody(bodyContentString); + } + } + else { + final String bodyContentString = serializer.serialize(bodyContentObject, SerializerEncoding.fromHeaders(request.headers())); + request.withBody(bodyContentString); + } + } + + // Headers from Swagger method arguments always take precedence over inferred headers from body types + for (final String headerName : operationDescription.headers().keySet()) { + request.withHeader(headerName, operationDescription.headers().get(headerName)); + } + + return request; + } + private Exception instantiateUnexpectedException(SwaggerMethodParser methodParser, HttpResponse response, String responseContent) { final int responseStatusCode = response.statusCode(); final Class exceptionType = methodParser.exceptionType(); @@ -357,6 +448,11 @@ protected Object handleAsyncHttpResponse(HttpRequest httpRequest, Single> WARN_MISSING_DECODING = new Function>() { @Override public Single apply(Throwable throwable) throws Exception { diff --git a/client-runtime/src/main/java/com/microsoft/rest/v2/annotations/ResumeOperation.java b/client-runtime/src/main/java/com/microsoft/rest/v2/annotations/ResumeOperation.java new file mode 100644 index 0000000000000..d9b024148e6ee --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/v2/annotations/ResumeOperation.java @@ -0,0 +1,20 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.rest.v2.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Defines the a continuation method. + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ResumeOperation { +} \ No newline at end of file