/** * 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.azure; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.joda.JodaModule; import com.microsoft.rest.protocol.SerializerAdapter; import com.microsoft.rest.serializer.Base64UrlSerializer; import com.microsoft.rest.serializer.ByteArraySerializer; import com.microsoft.rest.serializer.DateTimeRfc1123Serializer; import com.microsoft.rest.serializer.DateTimeSerializer; import com.microsoft.rest.serializer.HeadersSerializer; import okhttp3.ResponseBody; import retrofit2.Response; import java.io.IOException; import java.lang.reflect.Type; /** * An instance of this class defines polling status of a long running operation. * * @param <T> the type of the resource the operation returns. */ public class PollingState<T> { /** The HTTP method used to initiate the long running operation. **/ private String initialHttpMethod; /** The polling status. */ private String status; /** The HTTP status code. */ private int statusCode = DEFAULT_STATUS_CODE; /** The link in 'Azure-AsyncOperation' header. */ private String azureAsyncOperationHeaderLink; /** The link in 'Location' Header. */ private String locationHeaderLink; /** The default timeout interval between two polling operations. */ private int defaultRetryTimeout; /** The timeout interval between two polling operation. **/ private int retryTimeout; /** The resource uri on which PUT or PATCH operation is applied. **/ private String putOrPatchResourceUri; /** The logging context. **/ private String loggingContext; /** indicate how to retrieve the final state of LRO. **/ private LongRunningFinalState finalStateVia; // Non-serializable properties // /** The logging context header name. **/ @JsonIgnore private static final String LOGGING_HEADER = "x-ms-logging-context"; /** The statusCode that is used when no statusCode has been set. */ @JsonIgnore private static final int DEFAULT_STATUS_CODE = 0; /** The Retrofit response object. */ @JsonIgnore private Response<ResponseBody> response; /** The response resource object. */ @JsonIgnore private T resource; /** The type of the response resource object. */ @JsonIgnore private Type resourceType; /** The error during the polling operations. */ @JsonIgnore private CloudError error; /** The adapter for a custom serializer. */ @JsonIgnore private SerializerAdapter<?> serializerAdapter; /** * Default constructor. */ PollingState() { } /** * Creates a polling state. * * @param response the response from Retrofit REST call that initiate the long running operation. * @param lroOptions long running operation options. * @param defaultRetryTimeout the long running operation retry timeout. * @param resourceType the type of the resource the long running operation returns * @param serializerAdapter the adapter for the Jackson object mapper * @param <T> the result type * @return the polling state * @throws IOException thrown by deserialization */ public static <T> PollingState<T> create(Response<ResponseBody> response, LongRunningOperationOptions lroOptions, int defaultRetryTimeout, Type resourceType, SerializerAdapter<?> serializerAdapter) throws IOException { PollingState<T> pollingState = new PollingState<>(); pollingState.initialHttpMethod = response.raw().request().method(); pollingState.defaultRetryTimeout = defaultRetryTimeout; pollingState.withResponse(response); pollingState.resourceType = resourceType; pollingState.serializerAdapter = serializerAdapter; pollingState.loggingContext = response.raw().request().header(LOGGING_HEADER); pollingState.finalStateVia = lroOptions.finalStateVia(); String responseContent = null; PollingResource resource = null; if (response.body() != null) { responseContent = response.body().string(); response.body().close(); } if (responseContent != null && !responseContent.isEmpty()) { pollingState.resource = serializerAdapter.deserialize(responseContent, resourceType); resource = serializerAdapter.deserialize(responseContent, PollingResource.class); } final int statusCode = pollingState.response.code(); if (resource != null && resource.properties != null && resource.properties.provisioningState != null) { pollingState.withStatus(resource.properties.provisioningState, statusCode); } else { switch (statusCode) { case 202: pollingState.withStatus(AzureAsyncOperation.IN_PROGRESS_STATUS, statusCode); break; case 204: case 201: case 200: pollingState.withStatus(AzureAsyncOperation.SUCCESS_STATUS, statusCode); break; default: pollingState.withStatus(AzureAsyncOperation.FAILED_STATUS, statusCode); } } return pollingState; } /** * Creates PollingState from the json string. * * @param serializedPollingState polling state as json string * @param <ResultT> the result that the poll operation produces * @return the polling state */ public static <ResultT> PollingState<ResultT> createFromJSONString(String serializedPollingState) { ObjectMapper mapper = initMapper(new ObjectMapper()); PollingState<ResultT> pollingState; try { pollingState = mapper.readValue(serializedPollingState, PollingState.class); } catch (IOException exception) { throw new RuntimeException(exception); } return pollingState; } /** * Creates PollingState from another polling state. * * @param other other polling state * @param result the final result of the LRO * @param <ResultT> the result that the poll operation produces * @return the polling state */ public static <ResultT> PollingState<ResultT> createFromPollingState(PollingState<?> other, ResultT result) { PollingState<ResultT> pollingState = new PollingState<>(); pollingState.resource = result; pollingState.initialHttpMethod = other.initialHttpMethod(); pollingState.status = other.status(); pollingState.statusCode = other.statusCode(); pollingState.azureAsyncOperationHeaderLink = other.azureAsyncOperationHeaderLink(); pollingState.locationHeaderLink = other.locationHeaderLink(); pollingState.putOrPatchResourceUri = other.putOrPatchResourceUri(); pollingState.defaultRetryTimeout = other.defaultRetryTimeout; pollingState.retryTimeout = other.retryTimeout; pollingState.loggingContext = other.loggingContext; pollingState.finalStateVia = other.finalStateVia; return pollingState; } /** * @return the polling state in json string format */ public String serialize() { ObjectMapper mapper = initMapper(new ObjectMapper()); try { return mapper.writeValueAsString(this); } catch (JsonProcessingException exception) { throw new RuntimeException(exception); } } /** * Gets the resource. * * @return the resource. */ public T resource() { return resource; } /** * Gets the operation response. * * @return the operation response. */ public Response<ResponseBody> response() { return this.response; } /** * Gets the polling status. * * @return the polling status. */ public String status() { return status; } /** * Gets the polling HTTP status code. * * @return the polling HTTP status code. */ public int statusCode() { return statusCode; } /** * Gets the value captured from Azure-AsyncOperation header. * * @return the link in the header. */ public String azureAsyncOperationHeaderLink() { if (azureAsyncOperationHeaderLink != null && !azureAsyncOperationHeaderLink.isEmpty()) { return azureAsyncOperationHeaderLink; } return null; } /** * Gets the value captured from Location header. * * @return the link in the header. */ public String locationHeaderLink() { if (locationHeaderLink != null && !locationHeaderLink.isEmpty()) { return locationHeaderLink; } return null; } /** * Updates the polling state from a PUT or PATCH operation. * * @param response the response from Retrofit REST call * @throws CloudException thrown if the response is invalid * @throws IOException thrown by deserialization */ void updateFromResponseOnPutPatch(Response<ResponseBody> response) throws CloudException, IOException { String responseContent = null; if (response.body() != null) { responseContent = response.body().string(); response.body().close(); } if (responseContent == null || responseContent.isEmpty()) { throw new CloudException("polling response does not contain a valid body", response); } PollingResource resource = serializerAdapter.deserialize(responseContent, PollingResource.class); final int statusCode = response.code(); if (resource != null && resource.properties != null && resource.properties.provisioningState != null) { this.withStatus(resource.properties.provisioningState, statusCode); } else { this.withStatus(AzureAsyncOperation.SUCCESS_STATUS, statusCode); } CloudError error = new CloudError(); this.withErrorBody(error); error.withCode(this.status()); error.withMessage("Long running operation failed"); this.withResponse(response); this.withResource(serializerAdapter.<T>deserialize(responseContent, resourceType)); } /** * Updates the polling state from a DELETE or POST operation. * * @param response the response from Retrofit REST call * @throws IOException thrown by deserialization */ void updateFromResponseOnDeletePost(Response<ResponseBody> response) throws IOException { this.withResponse(response); String responseContent = null; if (response.body() != null) { responseContent = response.body().string(); response.body().close(); } this.withResource(serializerAdapter.<T>deserialize(responseContent, resourceType)); withStatus(AzureAsyncOperation.SUCCESS_STATUS, response.code()); } /** * Gets long running operation delay in milliseconds. * * @return the delay in milliseconds. */ int delayInMilliseconds() { if (this.retryTimeout >= 0) { return this.retryTimeout; } if (this.defaultRetryTimeout >= 0) { return this.defaultRetryTimeout * 1000; } return AzureAsyncOperation.DEFAULT_DELAY * 1000; } /** * @return the uri of the resource on which the LRO PUT or PATCH applied. */ String putOrPatchResourceUri() { return this.putOrPatchResourceUri; } /** * @return true if the status this state hold represents terminal status. */ boolean isStatusTerminal() { for (String terminalStatus : AzureAsyncOperation.terminalStatuses()) { if (terminalStatus.equalsIgnoreCase(this.status())) { return true; } } return false; } /** * @return true if the status this state hold is represents failed status. */ boolean isStatusFailed() { for (String failedStatus : AzureAsyncOperation.failedStatuses()) { if (failedStatus.equalsIgnoreCase(this.status())) { return true; } } return false; } /** * @return true if the status this state represents is succeeded status. */ boolean isStatusSucceeded() { return AzureAsyncOperation.SUCCESS_STATUS.equalsIgnoreCase(this.status()); } boolean resourcePending() { boolean resourcePending = statusCode() != 204 && isStatusSucceeded() && resource() == null && resourceType() != Void.class && locationHeaderLink() != null; if (resourcePending) { // Keep current behaviour for backward-compact return true; } else { return this.finalStateVia() == LongRunningFinalState.LOCATION; } } /** * Gets the logging context. * * @return the logging context */ String loggingContext() { return loggingContext; } /** * Sets the polling status. * * @param status the polling status. * @throws IllegalArgumentException thrown if status is null. */ PollingState<T> withStatus(String status) throws IllegalArgumentException { return withStatus(status, DEFAULT_STATUS_CODE); } /** * Sets the polling status. * * @param status the polling status. * @param statusCode the HTTP status code * @throws IllegalArgumentException thrown if status is null. */ PollingState<T> withStatus(String status, int statusCode) throws IllegalArgumentException { if (status == null) { throw new IllegalArgumentException("Status is null."); } this.status = status; this.statusCode = statusCode; return this; } /** * Sets the last operation response. * * @param response the last operation response. */ PollingState<T> withResponse(Response<ResponseBody> response) { this.response = response; withPollingUrlFromResponse(response); withPollingRetryTimeoutFromResponse(response); return this; } PollingState<T> withPollingUrlFromResponse(Response<ResponseBody> response) { if (response != null) { String asyncHeader = response.headers().get("Azure-AsyncOperation"); String locationHeader = response.headers().get("Location"); if (asyncHeader != null) { this.azureAsyncOperationHeaderLink = asyncHeader; } if (locationHeader != null) { this.locationHeaderLink = locationHeader; } } return this; } PollingState<T> withPollingRetryTimeoutFromResponse(Response<ResponseBody> response) { if (this.response != null && response.headers().get("Retry-After") != null) { retryTimeout = Integer.parseInt(response.headers().get("Retry-After")) * 1000; return this; } this.retryTimeout = -1; return this; } PollingState<T> withPutOrPatchResourceUri(final String uri) { this.putOrPatchResourceUri = uri; return this; } /** * Sets the resource. * * @param resource the resource. */ PollingState<T> withResource(T resource) { this.resource = resource; return this; } /** * @return the resource type */ Type resourceType() { return resourceType; } /** * @return describes how to retrieve the final result of long running operation. */ LongRunningFinalState finalStateVia() { if (this.initialHttpMethod().equalsIgnoreCase("POST") && resourceType() != Void.class) { // FinalStateVia is supported only for POST LRO at the moment. // if (this.locationHeaderLink() != null && this.azureAsyncOperationHeaderLink() != null) { // Consider final-state-via option only if both headers are provided on the wire otherwise // there is nothing to disambiguate. // if (this.finalStateVia == LongRunningFinalState.LOCATION) { // A POST LRO can be tracked only using Location or AsyncOperation Header. // If AsyncOperationHeader is present then anyway polling will be performed using // it and there is no point in making one additional call to retrieve state using // async operation uri anyway. Hence we consider only LongRunningFinalState.LOCATION. // return LongRunningFinalState.LOCATION; } } } return LongRunningFinalState.DEFAULT; } /** * Sets resource type. * * param resourceType the resource type */ PollingState<T> withResourceType(Type resourceType) { this.resourceType = resourceType; return this; } /** * Gets {@link CloudError} from current instance. * * @return the cloud error. */ CloudError errorBody() { return error; } /** * Sets {@link CloudError} from current instance. * * @param error the cloud error. */ PollingState<T> withErrorBody(CloudError error) { this.error = error; return this; } /** * Sets the serializer adapter. * * @param serializerAdapter the serializer adapter. */ PollingState<T> withSerializerAdapter(SerializerAdapter<?> serializerAdapter) { this.serializerAdapter = serializerAdapter; return this; } /** * @return the http method used to initiate the long running operation. */ String initialHttpMethod() { return this.initialHttpMethod; } /** * If status is in failed state then throw CloudException. */ void throwCloudExceptionIfInFailedState() { if (this.isStatusFailed()) { if (this.errorBody() != null) { throw new CloudException("Async operation failed with provisioning state: " + this.status(), this.response(), this.errorBody()); } else { throw new CloudException("Async operation failed with provisioning state: " + this.status(), this.response()); } } } /** * Initializes an object mapper. * * @param mapper the mapper to initialize * @return the initialized mapper */ private static ObjectMapper initMapper(ObjectMapper mapper) { mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) .configure(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS, true) .configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true) .setSerializationInclusion(JsonInclude.Include.NON_NULL) .registerModule(new JodaModule()) .registerModule(ByteArraySerializer.getModule()) .registerModule(Base64UrlSerializer.getModule()) .registerModule(DateTimeSerializer.getModule()) .registerModule(DateTimeRfc1123Serializer.getModule()) .registerModule(HeadersSerializer.getModule()); mapper.setVisibility(mapper.getSerializationConfig().getDefaultVisibilityChecker() .withFieldVisibility(JsonAutoDetect.Visibility.ANY) .withSetterVisibility(JsonAutoDetect.Visibility.NONE) .withGetterVisibility(JsonAutoDetect.Visibility.NONE) .withIsGetterVisibility(JsonAutoDetect.Visibility.NONE)); return mapper; } /** * An instance of this class describes the status of a long running operation * and is returned from server each time. */ private static class PollingResource { /** Inner properties object. */ @JsonProperty(value = "properties") private Properties properties; /** * Inner properties class. */ private static class Properties { /** The provisioning state of the resource. */ @JsonProperty(value = "provisioningState") private String provisioningState; } } }