Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License. See License.txt in the project root for
license information.
/**
* 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.
Type parameters: - <T> – the type of the resource the operation returns.
/**
* 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. /** The HTTP method used to initiate the long running operation. **/
private String initialHttpMethod;
The polling status. /** The polling status. */
private String status;
The HTTP status code. /** The HTTP status code. */
private int statusCode = DEFAULT_STATUS_CODE;
The link in 'Azure-AsyncOperation' header. /** The link in 'Azure-AsyncOperation' header. */
private String azureAsyncOperationHeaderLink;
The link in 'Location' Header. /** The link in 'Location' Header. */
private String locationHeaderLink;
The default timeout interval between two polling operations. /** The default timeout interval between two polling operations. */
private int defaultRetryTimeout;
The timeout interval between two polling operation. /** The timeout interval between two polling operation. **/
private int retryTimeout;
The resource uri on which PUT or PATCH operation is applied. /** The resource uri on which PUT or PATCH operation is applied. **/
private String putOrPatchResourceUri;
The logging context. /** The logging context. **/
private String loggingContext;
indicate how to retrieve the final state of LRO. /** indicate how to retrieve the final state of LRO. **/
private LongRunningFinalState finalStateVia;
// Non-serializable properties
//
The logging context header name. /** 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. /** The statusCode that is used when no statusCode has been set. */
@JsonIgnore
private static final int DEFAULT_STATUS_CODE = 0;
The Retrofit response object. /** The Retrofit response object. */
@JsonIgnore
private Response<ResponseBody> response;
The response resource object. /** The response resource object. */
@JsonIgnore
private T resource;
The type of the response resource object. /** The type of the response resource object. */
@JsonIgnore
private Type resourceType;
The error during the polling operations. /** The error during the polling operations. */
@JsonIgnore
private CloudError error;
The adapter for a custom serializer. /** The adapter for a custom serializer. */
@JsonIgnore
private SerializerAdapter<?> serializerAdapter;
Default constructor.
/**
* Default constructor.
*/
PollingState() {
}
Creates a polling state.
Params: - response – the response from Retrofit REST call that initiate the long running operation.
- lroOptions – long running operation options.
- defaultRetryTimeout – the long running operation retry timeout.
- resourceType – the type of the resource the long running operation returns
- serializerAdapter – the adapter for the Jackson object mapper
Type parameters: - <T> – the result type
Throws: - IOException – thrown by deserialization
Returns: the polling state
/**
* 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.
Params: - serializedPollingState – polling state as json string
Type parameters: - <ResultT> – the result that the poll operation produces
Returns: the polling state
/**
* 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.
Params: - other – other polling state
- result – the final result of the LRO
Type parameters: - <ResultT> – the result that the poll operation produces
Returns: the polling state
/**
* 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;
}
Returns: the polling state in json string format
/**
* @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.
Returns: the resource.
/**
* Gets the resource.
*
* @return the resource.
*/
public T resource() {
return resource;
}
Gets the operation response.
Returns: the operation response.
/**
* Gets the operation response.
*
* @return the operation response.
*/
public Response<ResponseBody> response() {
return this.response;
}
Gets the polling status.
Returns: the polling status.
/**
* Gets the polling status.
*
* @return the polling status.
*/
public String status() {
return status;
}
Gets the polling HTTP status code.
Returns: the polling HTTP status code.
/**
* 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.
Returns: the link in the header.
/**
* 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.
Returns: the link in the header.
/**
* 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.
Params: - response – the response from Retrofit REST call
Throws: - CloudException – thrown if the response is invalid
- IOException – thrown by deserialization
/**
* 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.
Params: - response – the response from Retrofit REST call
Throws: - IOException – thrown by deserialization
/**
* 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.
Returns: the delay in milliseconds.
/**
* 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;
}
Returns: the uri of the resource on which the LRO PUT or PATCH applied.
/**
* @return the uri of the resource on which the LRO PUT or PATCH applied.
*/
String putOrPatchResourceUri() {
return this.putOrPatchResourceUri;
}
Returns: true if the status this state hold represents terminal status.
/**
* @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;
}
Returns: true if the status this state hold is represents failed status.
/**
* @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;
}
Returns: true if the status this state represents is succeeded status.
/**
* @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.
Returns: the logging context
/**
* Gets the logging context.
*
* @return the logging context
*/
String loggingContext() {
return loggingContext;
}
Sets the polling status.
Params: - status – the polling status.
Throws: - IllegalArgumentException – thrown if status is null.
/**
* 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.
Params: - status – the polling status.
- statusCode – the HTTP status code
Throws: - IllegalArgumentException – thrown if status is null.
/**
* 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.
Params: - response – the last operation response.
/**
* 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.
Params: - resource – the resource.
/**
* Sets the resource.
*
* @param resource the resource.
*/
PollingState<T> withResource(T resource) {
this.resource = resource;
return this;
}
Returns: the resource type
/**
* @return the resource type
*/
Type resourceType() {
return resourceType;
}
Returns: describes how to retrieve the final result of long running operation.
/**
* @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
/**
* Sets resource type.
*
* param resourceType the resource type
*/
PollingState<T> withResourceType(Type resourceType) {
this.resourceType = resourceType;
return this;
}
Gets CloudError
from current instance. Returns: the cloud error.
/**
* Gets {@link CloudError} from current instance.
*
* @return the cloud error.
*/
CloudError errorBody() {
return error;
}
Sets CloudError
from current instance. Params: - error – the cloud 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.
Params: - serializerAdapter – the serializer adapter.
/**
* Sets the serializer adapter.
*
* @param serializerAdapter the serializer adapter.
*/
PollingState<T> withSerializerAdapter(SerializerAdapter<?> serializerAdapter) {
this.serializerAdapter = serializerAdapter;
return this;
}
Returns: the http method used to initiate the long running operation.
/**
* @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.
/**
* 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.
Params: - mapper – the mapper to initialize
Returns: the initialized mapper
/**
* 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.
/**
* 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. /** Inner properties object. */
@JsonProperty(value = "properties")
private Properties properties;
Inner properties class.
/**
* Inner properties class.
*/
private static class Properties {
The provisioning state of the resource. /** The provisioning state of the resource. */
@JsonProperty(value = "provisioningState")
private String provisioningState;
}
}
}