diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/OperationResourcePollingStrategy.java b/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/OperationResourcePollingStrategy.java index 70e1cb5657f67..0fa2d4ce5ce49 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/OperationResourcePollingStrategy.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/OperationResourcePollingStrategy.java @@ -5,31 +5,30 @@ import com.azure.core.exception.AzureException; import com.azure.core.http.HttpHeader; +import com.azure.core.http.HttpHeaderName; import com.azure.core.http.HttpMethod; import com.azure.core.http.HttpPipeline; import com.azure.core.http.HttpRequest; import com.azure.core.http.HttpResponse; import com.azure.core.http.rest.Response; import com.azure.core.implementation.ImplUtils; -import com.azure.core.implementation.http.HttpHeadersHelper; import com.azure.core.implementation.serializer.DefaultJsonSerializer; import com.azure.core.util.BinaryData; import com.azure.core.util.Context; import com.azure.core.util.CoreUtils; import com.azure.core.util.FluxUtil; import com.azure.core.util.logging.ClientLogger; +import com.azure.core.util.polling.implementation.PollResult; import com.azure.core.util.polling.implementation.PollingConstants; import com.azure.core.util.polling.implementation.PollingUtils; import com.azure.core.util.serializer.ObjectSerializer; import com.azure.core.util.serializer.TypeReference; -import com.fasterxml.jackson.annotation.JsonSetter; import reactor.core.publisher.Mono; import java.net.MalformedURLException; import java.net.URL; import java.time.Duration; import java.time.OffsetDateTime; -import java.util.Locale; import java.util.Objects; import static com.azure.core.util.polling.implementation.PollingUtils.getAbsolutePath; @@ -43,14 +42,15 @@ */ public class OperationResourcePollingStrategy implements PollingStrategy { private static final ClientLogger LOGGER = new ClientLogger(OperationResourcePollingStrategy.class); - private static final String DEFAULT_OPERATION_LOCATION_HEADER = "Operation-Location"; - private static final String DEFAULT_OPERATION_LOCATION_HEADER_LOWER_CASE = "operation-location"; + private static final HttpHeaderName DEFAULT_OPERATION_LOCATION_HEADER + = HttpHeaderName.fromString("Operation-Location"); + private static final TypeReference POLL_RESULT_TYPE_REFERENCE + = TypeReference.createInstance(PollResult.class); private final HttpPipeline httpPipeline; private final ObjectSerializer serializer; private final String endpoint; - private final String operationLocationHeaderName; - private final String operationLocationHeaderNameLowerCase; + private final HttpHeaderName operationLocationHeaderName; private final Context context; /** @@ -60,7 +60,7 @@ public class OperationResourcePollingStrategy implements PollingStrategy canPoll(Response initialResponse) { - HttpHeader operationLocationHeader = HttpHeadersHelper.getNoKeyFormatting(initialResponse.getHeaders(), - operationLocationHeaderNameLowerCase); + HttpHeader operationLocationHeader = initialResponse.getHeaders().get(operationLocationHeaderName); if (operationLocationHeader != null) { try { new URL(getAbsolutePath(operationLocationHeader.getValue(), endpoint, LOGGER)); @@ -128,12 +129,10 @@ public Mono canPoll(Response initialResponse) { @Override public Mono> onInitialResponse(Response response, PollingContext pollingContext, TypeReference pollResponseType) { - HttpHeader operationLocationHeader = HttpHeadersHelper.getNoKeyFormatting(response.getHeaders(), - operationLocationHeaderNameLowerCase); - HttpHeader locationHeader = HttpHeadersHelper.getNoKeyFormatting(response.getHeaders(), - PollingConstants.LOCATION_LOWER_CASE); + HttpHeader operationLocationHeader = response.getHeaders().get(operationLocationHeaderName); + HttpHeader locationHeader = response.getHeaders().get(HttpHeaderName.LOCATION); if (operationLocationHeader != null) { - pollingContext.setData(operationLocationHeaderName, + pollingContext.setData(operationLocationHeaderName.getCaseSensitiveName(), getAbsolutePath(operationLocationHeader.getValue(), endpoint, LOGGER)); } if (locationHeader != null) { @@ -161,12 +160,12 @@ public Mono> onInitialResponse(Response response, PollingCont @Override public Mono> poll(PollingContext pollingContext, TypeReference pollResponseType) { - HttpRequest request = new HttpRequest(HttpMethod.GET, pollingContext.getData(operationLocationHeaderName)); + HttpRequest request = new HttpRequest(HttpMethod.GET, pollingContext.getData(operationLocationHeaderName + .getCaseSensitiveName())); return FluxUtil.withContext(context1 -> httpPipeline.send(request, CoreUtils.mergeContexts(context1, this.context))).flatMap(response -> response.getBodyAsByteArray() .map(BinaryData::fromBytes) - .flatMap(binaryData -> PollingUtils.deserializeResponse( - binaryData, serializer, new TypeReference() { }) + .flatMap(binaryData -> PollingUtils.deserializeResponse(binaryData, serializer, POLL_RESULT_TYPE_REFERENCE) .map(pollResult -> { final String resourceLocation = pollResult.getResourceLocation(); if (resourceLocation != null) { @@ -217,77 +216,4 @@ public Mono getResult(PollingContext pollingContext, TypeReference resu .flatMap(binaryData -> PollingUtils.deserializeResponse(binaryData, serializer, resultType)); } } - - /** - * A simple structure representing the partial response received from an operation location URL, containing the - * information of the status of the long running operation. - */ - private static class PollResult { - private LongRunningOperationStatus status; - private String resourceLocation; - - /** - * Gets the status of the long running operation. - * @return the status represented as a {@link LongRunningOperationStatus} - */ - public LongRunningOperationStatus getStatus() { - return status; - } - - /** - * Sets the long running operation status in the format of a string returned by the service. This is called by - * the deserializer when a response is received. - * - * @param status the status of the long running operation - * @return the modified PollResult instance - */ - @JsonSetter - public PollResult setStatus(String status) { - if (PollingConstants.STATUS_NOT_STARTED.equalsIgnoreCase(status)) { - this.status = LongRunningOperationStatus.NOT_STARTED; - } else if (PollingConstants.STATUS_IN_PROGRESS.equalsIgnoreCase(status) - || PollingConstants.STATUS_RUNNING.equalsIgnoreCase(status)) { - this.status = LongRunningOperationStatus.IN_PROGRESS; - } else if (PollingConstants.STATUS_SUCCEEDED.equalsIgnoreCase(status)) { - this.status = LongRunningOperationStatus.SUCCESSFULLY_COMPLETED; - } else if (PollingConstants.STATUS_FAILED.equalsIgnoreCase(status)) { - this.status = LongRunningOperationStatus.FAILED; - } else { - this.status = LongRunningOperationStatus.fromString(status, true); - } - return this; - } - - /** - * Sets the long running operation status in the format of the {@link LongRunningOperationStatus} enum. - * - * @param status the status of the long running operation - * @return the modified PollResult instance - */ - public PollResult setStatus(LongRunningOperationStatus status) { - this.status = status; - return this; - } - - /** - * Gets the resource location URL to get the final result. This is often available in the response when the - * long running operation has been successfully completed. - * - * @return the resource location URL to get he final result - */ - public String getResourceLocation() { - return resourceLocation; - } - - /** - * Sets the resource location URL. this should only be called by the deserializer when a response is received. - * - * @param resourceLocation the resource location URL - * @return the modified PollResult instance - */ - public PollResult setResourceLocation(String resourceLocation) { - this.resourceLocation = resourceLocation; - return this; - } - } } diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncChainedPollingStrategy.java b/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncChainedPollingStrategy.java new file mode 100644 index 0000000000000..fde7a291f5614 --- /dev/null +++ b/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncChainedPollingStrategy.java @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.util.polling; + +import com.azure.core.http.rest.Response; +import com.azure.core.util.logging.ClientLogger; +import com.azure.core.util.serializer.TypeReference; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * A synchronous polling strategy that chains multiple synchronous polling strategies, finds the first strategy that can + * poll the current long-running operation, and polls with that strategy. + * + * @param the type of the response type from a polling call, or BinaryData if raw response body should be kept + * @param the type of the final result object to deserialize into, or BinaryData if raw response body should be + * kept + */ +public final class SyncChainedPollingStrategy implements SyncPollingStrategy { + private static final ClientLogger LOGGER = new ClientLogger(SyncChainedPollingStrategy.class); + + private final List> pollingStrategies; + private SyncPollingStrategy pollableStrategy = null; + + /** + * Creates a synchronous chained polling strategy with a list of polling strategies. + * + * @param strategies the list of synchronous polling strategies + * @throws NullPointerException If {@code strategies} is null. + * @throws IllegalArgumentException If {@code strategies} is an empty list. + */ + public SyncChainedPollingStrategy(List> strategies) { + Objects.requireNonNull(strategies, "'strategies' cannot be null."); + if (strategies.isEmpty()) { + throw LOGGER.logExceptionAsError(new IllegalArgumentException("'strategies' cannot be empty.")); + } + this.pollingStrategies = Collections.unmodifiableList(strategies); + } + + @Override + public boolean canPoll(Response initialResponse) { + // Find the first strategy that can poll in series so that + // pollableStrategy is only set once + for (SyncPollingStrategy strategy : pollingStrategies) { + if (strategy.canPoll(initialResponse)) { + this.pollableStrategy = strategy; + return true; + } + } + + return false; + } + + /** + * {@inheritDoc} + * + * @throws NullPointerException if {@link #canPoll(Response)} is not called prior to this, or if it returns false. + */ + @Override + public U getResult(PollingContext context, TypeReference resultType) { + return pollableStrategy.getResult(context, resultType); + } + + /** + * {@inheritDoc} + * + * @throws NullPointerException if {@link #canPoll(Response)} is not called prior to this, or if it returns false. + */ + @Override + public PollResponse onInitialResponse(Response response, PollingContext pollingContext, + TypeReference pollResponseType) { + return pollableStrategy.onInitialResponse(response, pollingContext, pollResponseType); + } + + /** + * {@inheritDoc} + * + * @throws NullPointerException if {@link #canPoll(Response)} is not called prior to this, or if it returns false. + */ + @Override + public PollResponse poll(PollingContext context, TypeReference pollResponseType) { + return pollableStrategy.poll(context, pollResponseType); + } + + /** + * {@inheritDoc} + * + * @throws NullPointerException if {@link #canPoll(Response)} is not called prior to this, or if it returns false. + */ + @Override + public T cancel(PollingContext pollingContext, PollResponse initialResponse) { + return pollableStrategy.cancel(pollingContext, initialResponse); + } +} diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncDefaultPollingStrategy.java b/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncDefaultPollingStrategy.java new file mode 100644 index 0000000000000..8a79bc252462c --- /dev/null +++ b/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncDefaultPollingStrategy.java @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.util.polling; + +import com.azure.core.http.HttpPipeline; +import com.azure.core.http.rest.Response; +import com.azure.core.implementation.serializer.DefaultJsonSerializer; +import com.azure.core.util.Context; +import com.azure.core.util.serializer.JsonSerializer; +import com.azure.core.util.serializer.TypeReference; + +import java.util.Arrays; + +/** + * The default synchronous polling strategy to use with Azure data plane services. The default polling strategy will + * attempt three known strategies, {@link SyncOperationResourcePollingStrategy}, {@link SyncLocationPollingStrategy}, + * and {@link SyncStatusCheckPollingStrategy}, in this order. The first strategy that can poll on the initial response + * will be used. The created chained polling strategy is capable of handling most of the polling scenarios in Azure. + * + * @param the type of the response type from a polling call, or BinaryData if raw response body should be kept + * @param the type of the final result object to deserialize into, or BinaryData if raw response body should be + * kept + */ +public final class SyncDefaultPollingStrategy implements SyncPollingStrategy { + private final SyncChainedPollingStrategy chainedPollingStrategy; + + /** + * Creates a synchronous chained polling strategy with three known polling strategies, + * {@link SyncOperationResourcePollingStrategy}, {@link SyncLocationPollingStrategy}, and + * {@link SyncStatusCheckPollingStrategy}, in this order, with a JSON serializer. + * + * @param httpPipeline an instance of {@link HttpPipeline} to send requests with + * @throws NullPointerException If {@code httpPipeline} is null. + */ + public SyncDefaultPollingStrategy(HttpPipeline httpPipeline) { + this(httpPipeline, new DefaultJsonSerializer(), Context.NONE); + } + + /** + * Creates a synchronous chained polling strategy with three known polling strategies, + * {@link SyncOperationResourcePollingStrategy}, {@link SyncLocationPollingStrategy}, and + * {@link SyncStatusCheckPollingStrategy}, in this order, with a JSON serializer. + * + * @param httpPipeline an instance of {@link HttpPipeline} to send requests with + * @param serializer a custom serializer for serializing and deserializing polling responses + * @throws NullPointerException If {@code httpPipeline} is null. + */ + public SyncDefaultPollingStrategy(HttpPipeline httpPipeline, JsonSerializer serializer) { + this(httpPipeline, serializer, Context.NONE); + } + + /** + * Creates a synchronous chained polling strategy with three known polling strategies, + * {@link SyncOperationResourcePollingStrategy}, {@link SyncLocationPollingStrategy}, and + * {@link SyncStatusCheckPollingStrategy}, in this order, with a JSON serializer. + * + * @param httpPipeline an instance of {@link HttpPipeline} to send requests with + * @param serializer a custom serializer for serializing and deserializing polling responses + * @param context an instance of {@link Context} + * @throws NullPointerException If {@code httpPipeline} is null. + */ + public SyncDefaultPollingStrategy(HttpPipeline httpPipeline, JsonSerializer serializer, Context context) { + this(httpPipeline, null, serializer, context); + } + + /** + * Creates a synchronous chained polling strategy with three known polling strategies, + * {@link SyncOperationResourcePollingStrategy}, {@link SyncLocationPollingStrategy}, and + * {@link SyncStatusCheckPollingStrategy}, in this order, with a JSON serializer. + * + * @param httpPipeline an instance of {@link HttpPipeline} to send requests with. + * @param endpoint an endpoint for creating an absolute path when the path itself is relative. + * @param serializer a custom serializer for serializing and deserializing polling responses. + * @param context an instance of {@link Context}. + * @throws NullPointerException If {@code httpPipeline} is null. + */ + public SyncDefaultPollingStrategy(HttpPipeline httpPipeline, String endpoint, JsonSerializer serializer, + Context context) { + this.chainedPollingStrategy = new SyncChainedPollingStrategy<>(Arrays.asList( + new SyncOperationResourcePollingStrategy<>(httpPipeline, endpoint, serializer, null, context), + new SyncLocationPollingStrategy<>(httpPipeline, endpoint, serializer, context), + new SyncStatusCheckPollingStrategy<>(serializer))); + } + + @Override + public U getResult(PollingContext pollingContext, TypeReference resultType) { + return chainedPollingStrategy.getResult(pollingContext, resultType); + } + + @Override + public boolean canPoll(Response initialResponse) { + return chainedPollingStrategy.canPoll(initialResponse); + } + + @Override + public PollResponse onInitialResponse(Response response, PollingContext pollingContext, + TypeReference pollResponseType) { + return chainedPollingStrategy.onInitialResponse(response, pollingContext, pollResponseType); + } + + @Override + public PollResponse poll(PollingContext pollingContext, TypeReference pollResponseType) { + return chainedPollingStrategy.poll(pollingContext, pollResponseType); + } + + @Override + public T cancel(PollingContext pollingContext, PollResponse initialResponse) { + return chainedPollingStrategy.cancel(pollingContext, initialResponse); + } +} diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncLocationPollingStrategy.java b/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncLocationPollingStrategy.java new file mode 100644 index 0000000000000..522465f05d7af --- /dev/null +++ b/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncLocationPollingStrategy.java @@ -0,0 +1,200 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.util.polling; + +import com.azure.core.exception.AzureException; +import com.azure.core.http.HttpHeader; +import com.azure.core.http.HttpHeaderName; +import com.azure.core.http.HttpMethod; +import com.azure.core.http.HttpPipeline; +import com.azure.core.http.HttpRequest; +import com.azure.core.http.HttpResponse; +import com.azure.core.http.rest.Response; +import com.azure.core.implementation.ImplUtils; +import com.azure.core.implementation.serializer.DefaultJsonSerializer; +import com.azure.core.util.BinaryData; +import com.azure.core.util.Context; +import com.azure.core.util.logging.ClientLogger; +import com.azure.core.util.polling.implementation.PollingConstants; +import com.azure.core.util.polling.implementation.PollingUtils; +import com.azure.core.util.serializer.ObjectSerializer; +import com.azure.core.util.serializer.TypeReference; + +import java.net.MalformedURLException; +import java.net.URL; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.Objects; + +import static com.azure.core.util.polling.implementation.PollingUtils.getAbsolutePath; +import static com.azure.core.util.polling.implementation.PollingUtils.serializeResponseSync; + +/** + * Implements a synchronous Location polling strategy. + * + * @param the type of the response type from a polling call, or BinaryData if raw response body should be kept + * @param the type of the final result object to deserialize into, or BinaryData if raw response body should be + * kept + */ +public class SyncLocationPollingStrategy implements SyncPollingStrategy { + private static final ObjectSerializer DEFAULT_SERIALIZER = new DefaultJsonSerializer(); + + private static final ClientLogger LOGGER = new ClientLogger(SyncLocationPollingStrategy.class); + + private final String endpoint; + private final HttpPipeline httpPipeline; + private final ObjectSerializer serializer; + private final Context context; + + /** + * Creates an instance of the location polling strategy using a JSON serializer. + * + * @param httpPipeline an instance of {@link HttpPipeline} to send requests with + * @throws NullPointerException If {@code httpPipeline} is null. + */ + public SyncLocationPollingStrategy(HttpPipeline httpPipeline) { + this(httpPipeline, DEFAULT_SERIALIZER, Context.NONE); + } + + /** + * Creates an instance of the location polling strategy. + * + * @param httpPipeline an instance of {@link HttpPipeline} to send requests with + * @param serializer a custom serializer for serializing and deserializing polling responses + * @throws NullPointerException If {@code httpPipeline} is null. + */ + public SyncLocationPollingStrategy(HttpPipeline httpPipeline, ObjectSerializer serializer) { + this(httpPipeline, serializer, Context.NONE); + } + + /** + * Creates an instance of the location polling strategy. + * + * @param httpPipeline an instance of {@link HttpPipeline} to send requests with + * @param serializer a custom serializer for serializing and deserializing polling responses + * @param context an instance of {@link Context} + * @throws NullPointerException If {@code httpPipeline} is null. + */ + public SyncLocationPollingStrategy(HttpPipeline httpPipeline, ObjectSerializer serializer, Context context) { + this(httpPipeline, null, serializer, context); + } + + /** + * Creates an instance of the location polling strategy. + * + * @param httpPipeline an instance of {@link HttpPipeline} to send requests with + * @param endpoint an endpoint for creating an absolute path when the path itself is relative. + * @param serializer a custom serializer for serializing and deserializing polling responses + * @param context an instance of {@link Context} + * @throws NullPointerException If {@code httpPipeline} is null. + */ + public SyncLocationPollingStrategy(HttpPipeline httpPipeline, String endpoint, ObjectSerializer serializer, + Context context) { + this.httpPipeline = Objects.requireNonNull(httpPipeline, "'httpPipeline' cannot be null"); + this.endpoint = endpoint; + this.serializer = (serializer == null) ? DEFAULT_SERIALIZER : serializer; + this.context = context == null ? Context.NONE : context; + } + + @Override + public boolean canPoll(Response initialResponse) { + HttpHeader locationHeader = initialResponse.getHeaders().get(HttpHeaderName.LOCATION); + if (locationHeader != null) { + try { + new URL(getAbsolutePath(locationHeader.getValue(), endpoint, LOGGER)); + return true; + } catch (MalformedURLException e) { + LOGGER.info("Failed to parse Location header into a URL.", e); + return false; + } + } + return false; + } + + @Override + public PollResponse onInitialResponse(Response response, PollingContext pollingContext, + TypeReference pollResponseType) { + HttpHeader locationHeader = response.getHeaders().get(HttpHeaderName.LOCATION); + if (locationHeader != null) { + pollingContext.setData(PollingConstants.LOCATION, + getAbsolutePath(locationHeader.getValue(), endpoint, LOGGER)); + } + pollingContext.setData(PollingConstants.HTTP_METHOD, response.getRequest().getHttpMethod().name()); + pollingContext.setData(PollingConstants.REQUEST_URL, response.getRequest().getUrl().toString()); + + if (response.getStatusCode() == 200 || response.getStatusCode() == 201 + || response.getStatusCode() == 202 || response.getStatusCode() == 204) { + Duration retryAfter = ImplUtils.getRetryAfterFromHeaders(response.getHeaders(), OffsetDateTime::now); + return new PollResponse<>(LongRunningOperationStatus.IN_PROGRESS, + PollingUtils.convertResponseSync(response.getValue(), serializer, pollResponseType), retryAfter); + } + + throw LOGGER.logExceptionAsError(new AzureException(String.format( + "Operation failed or cancelled with status code %d, 'Location' header: %s, and response body: %s", + response.getStatusCode(), locationHeader, serializeResponseSync(response.getValue(), serializer)))); + } + + @Override + public PollResponse poll(PollingContext pollingContext, TypeReference pollResponseType) { + HttpRequest request = new HttpRequest(HttpMethod.GET, pollingContext.getData(PollingConstants.LOCATION)); + + + try (HttpResponse response = httpPipeline.sendSync(request, context)) { + HttpHeader locationHeader = response.getHeaders().get(HttpHeaderName.LOCATION); + + if (locationHeader != null) { + pollingContext.setData(PollingConstants.LOCATION, locationHeader.getValue()); + } + + LongRunningOperationStatus status; + if (response.getStatusCode() == 202) { + status = LongRunningOperationStatus.IN_PROGRESS; + } else if (response.getStatusCode() >= 200 && response.getStatusCode() <= 204) { + status = LongRunningOperationStatus.SUCCESSFULLY_COMPLETED; + } else { + status = LongRunningOperationStatus.FAILED; + } + + BinaryData responseBody = response.getBodyAsBinaryData(); + pollingContext.setData(PollingConstants.POLL_RESPONSE_BODY, responseBody.toString()); + Duration retryAfter = ImplUtils.getRetryAfterFromHeaders(response.getHeaders(), OffsetDateTime::now); + + return new PollResponse<>(status, + PollingUtils.deserializeResponseSync(responseBody, serializer, pollResponseType), retryAfter); + } + } + + @Override + public U getResult(PollingContext pollingContext, TypeReference resultType) { + if (pollingContext.getLatestResponse().getStatus() == LongRunningOperationStatus.FAILED) { + throw LOGGER.logExceptionAsError(new AzureException("Long-running operation failed.")); + } else if (pollingContext.getLatestResponse().getStatus() == LongRunningOperationStatus.USER_CANCELLED) { + throw LOGGER.logExceptionAsError(new AzureException("Long-running operation cancelled.")); + } + + String finalGetUrl; + String httpMethod = pollingContext.getData(PollingConstants.HTTP_METHOD); + if (HttpMethod.PUT.name().equalsIgnoreCase(httpMethod) + || HttpMethod.PATCH.name().equalsIgnoreCase(httpMethod)) { + finalGetUrl = pollingContext.getData(PollingConstants.REQUEST_URL); + } else if (HttpMethod.POST.name().equalsIgnoreCase(httpMethod) + && pollingContext.getData(PollingConstants.LOCATION) != null) { + finalGetUrl = pollingContext.getData(PollingConstants.LOCATION); + } else { + throw LOGGER.logExceptionAsError(new AzureException("Cannot get final result")); + } + + if (finalGetUrl == null) { + String latestResponseBody = pollingContext.getData(PollingConstants.POLL_RESPONSE_BODY); + return PollingUtils.deserializeResponseSync(BinaryData.fromString(latestResponseBody), serializer, + resultType); + } + + HttpRequest request = new HttpRequest(HttpMethod.GET, finalGetUrl); + try (HttpResponse response = httpPipeline.sendSync(request, context)) { + BinaryData responseBody = response.getBodyAsBinaryData(); + return PollingUtils.deserializeResponseSync(responseBody, serializer, resultType); + } + } +} diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncOperationResourcePollingStrategy.java b/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncOperationResourcePollingStrategy.java new file mode 100644 index 0000000000000..eca9484a64680 --- /dev/null +++ b/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncOperationResourcePollingStrategy.java @@ -0,0 +1,217 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.util.polling; + +import com.azure.core.exception.AzureException; +import com.azure.core.http.HttpHeader; +import com.azure.core.http.HttpHeaderName; +import com.azure.core.http.HttpMethod; +import com.azure.core.http.HttpPipeline; +import com.azure.core.http.HttpRequest; +import com.azure.core.http.HttpResponse; +import com.azure.core.http.rest.Response; +import com.azure.core.implementation.ImplUtils; +import com.azure.core.implementation.serializer.DefaultJsonSerializer; +import com.azure.core.util.BinaryData; +import com.azure.core.util.Context; +import com.azure.core.util.logging.ClientLogger; +import com.azure.core.util.polling.implementation.PollResult; +import com.azure.core.util.polling.implementation.PollingConstants; +import com.azure.core.util.polling.implementation.PollingUtils; +import com.azure.core.util.serializer.ObjectSerializer; +import com.azure.core.util.serializer.TypeReference; + +import java.net.MalformedURLException; +import java.net.URL; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.Objects; + +import static com.azure.core.util.polling.implementation.PollingUtils.getAbsolutePath; + +/** + * Implements a synchronous operation resource polling strategy, typically from Operation-Location. + * + * @param the type of the response type from a polling call, or BinaryData if raw response body should be kept + * @param the type of the final result object to deserialize into, or BinaryData if raw response body should be + * kept + */ +public class SyncOperationResourcePollingStrategy implements SyncPollingStrategy { + private static final ClientLogger LOGGER = new ClientLogger(SyncOperationResourcePollingStrategy.class); + private static final HttpHeaderName DEFAULT_OPERATION_LOCATION_HEADER + = HttpHeaderName.fromString("Operation-Location"); + private static final TypeReference POLL_RESULT_TYPE_REFERENCE + = TypeReference.createInstance(PollResult.class); + + private final HttpPipeline httpPipeline; + private final ObjectSerializer serializer; + private final String endpoint; + private final HttpHeaderName operationLocationHeaderName; + private final Context context; + + /** + * Creates an instance of the operation resource polling strategy using a JSON serializer and "Operation-Location" + * as the header for polling. + * + * @param httpPipeline an instance of {@link HttpPipeline} to send requests with + */ + public SyncOperationResourcePollingStrategy(HttpPipeline httpPipeline) { + this(httpPipeline, null, new DefaultJsonSerializer(), DEFAULT_OPERATION_LOCATION_HEADER, Context.NONE); + } + + /** + * Creates an instance of the operation resource polling strategy. + * + * @param httpPipeline an instance of {@link HttpPipeline} to send requests with + * @param serializer a custom serializer for serializing and deserializing polling responses + * @param operationLocationHeaderName a custom header for polling the long-running operation + */ + public SyncOperationResourcePollingStrategy(HttpPipeline httpPipeline, ObjectSerializer serializer, + String operationLocationHeaderName) { + this(httpPipeline, serializer, operationLocationHeaderName, Context.NONE); + } + + /** + * Creates an instance of the operation resource polling strategy. + * + * @param httpPipeline an instance of {@link HttpPipeline} to send requests with + * @param serializer a custom serializer for serializing and deserializing polling responses + * @param operationLocationHeaderName a custom header for polling the long-running operation + * @param context an instance of {@link com.azure.core.util.Context} + */ + public SyncOperationResourcePollingStrategy(HttpPipeline httpPipeline, ObjectSerializer serializer, + String operationLocationHeaderName, Context context) { + this(httpPipeline, null, serializer, operationLocationHeaderName, context); + } + + /** + * Creates an instance of the operation resource polling strategy. + * + * @param httpPipeline an instance of {@link HttpPipeline} to send requests with. + * @param endpoint an endpoint for creating an absolute path when the path itself is relative. + * @param serializer a custom serializer for serializing and deserializing polling responses. + * @param operationLocationHeaderName a custom header for polling the long-running operation. + * @param context an instance of {@link com.azure.core.util.Context}. + */ + public SyncOperationResourcePollingStrategy(HttpPipeline httpPipeline, String endpoint, ObjectSerializer serializer, + String operationLocationHeaderName, Context context) { + this(httpPipeline, endpoint, serializer, + operationLocationHeaderName == null ? null : HttpHeaderName.fromString(operationLocationHeaderName), + context); + } + + private SyncOperationResourcePollingStrategy(HttpPipeline httpPipeline, String endpoint, + ObjectSerializer serializer, HttpHeaderName operationLocationHeaderName, Context context) { + this.httpPipeline = Objects.requireNonNull(httpPipeline, "'httpPipeline' cannot be null"); + this.endpoint = endpoint; + this.serializer = serializer != null ? serializer : new DefaultJsonSerializer(); + this.operationLocationHeaderName = (operationLocationHeaderName == null) + ? DEFAULT_OPERATION_LOCATION_HEADER : operationLocationHeaderName; + this.context = context == null ? Context.NONE : context; + } + + @Override + public boolean canPoll(Response initialResponse) { + HttpHeader operationLocationHeader = initialResponse.getHeaders().get(operationLocationHeaderName); + if (operationLocationHeader != null) { + try { + new URL(getAbsolutePath(operationLocationHeader.getValue(), endpoint, LOGGER)); + return true; + } catch (MalformedURLException e) { + return false; + } + } + return false; + } + + @Override + public PollResponse onInitialResponse(Response response, PollingContext pollingContext, + TypeReference pollResponseType) { + HttpHeader operationLocationHeader = response.getHeaders().get(operationLocationHeaderName); + HttpHeader locationHeader = response.getHeaders().get(HttpHeaderName.LOCATION); + if (operationLocationHeader != null) { + pollingContext.setData(operationLocationHeaderName.getCaseSensitiveName(), + getAbsolutePath(operationLocationHeader.getValue(), endpoint, LOGGER)); + } + + if (locationHeader != null) { + pollingContext.setData(PollingConstants.LOCATION, + getAbsolutePath(locationHeader.getValue(), endpoint, LOGGER)); + } + + pollingContext.setData(PollingConstants.HTTP_METHOD, response.getRequest().getHttpMethod().name()); + pollingContext.setData(PollingConstants.REQUEST_URL, response.getRequest().getUrl().toString()); + + if (response.getStatusCode() == 200 || response.getStatusCode() == 201 + || response.getStatusCode() == 202 || response.getStatusCode() == 204) { + Duration retryAfter = ImplUtils.getRetryAfterFromHeaders(response.getHeaders(), OffsetDateTime::now); + return new PollResponse<>(LongRunningOperationStatus.IN_PROGRESS, + PollingUtils.convertResponseSync(response.getValue(), serializer, pollResponseType), retryAfter); + } + + throw LOGGER.logExceptionAsError(new AzureException(String.format( + "Operation failed or cancelled with status code %d, '%s' header: %s, and response body: %s", + response.getStatusCode(), operationLocationHeaderName, operationLocationHeader, + PollingUtils.serializeResponseSync(response.getValue(), serializer)))); + + } + + @Override + public PollResponse poll(PollingContext pollingContext, TypeReference pollResponseType) { + HttpRequest request = new HttpRequest(HttpMethod.GET, pollingContext.getData(operationLocationHeaderName + .getCaseSensitiveName())); + + try (HttpResponse response = httpPipeline.sendSync(request, context)) { + BinaryData responseBody = response.getBodyAsBinaryData(); + PollResult pollResult = PollingUtils.deserializeResponseSync(responseBody, serializer, + POLL_RESULT_TYPE_REFERENCE); + + String resourceLocation = pollResult.getResourceLocation(); + if (resourceLocation != null) { + pollingContext.setData(PollingConstants.RESOURCE_LOCATION, + getAbsolutePath(resourceLocation, endpoint, LOGGER)); + } + pollingContext.setData(PollingConstants.POLL_RESPONSE_BODY, responseBody.toString()); + + Duration retryAfter = ImplUtils.getRetryAfterFromHeaders(response.getHeaders(), OffsetDateTime::now); + + return new PollResponse<>(pollResult.getStatus(), + PollingUtils.deserializeResponseSync(responseBody, serializer, pollResponseType), retryAfter); + } + } + + @Override + public U getResult(PollingContext pollingContext, TypeReference resultType) { + if (pollingContext.getLatestResponse().getStatus() == LongRunningOperationStatus.FAILED) { + throw LOGGER.logExceptionAsError(new AzureException("Long running operation failed.")); + } else if (pollingContext.getLatestResponse().getStatus() == LongRunningOperationStatus.USER_CANCELLED) { + throw LOGGER.logExceptionAsError(new AzureException("Long running operation cancelled.")); + } + String finalGetUrl = pollingContext.getData(PollingConstants.RESOURCE_LOCATION); + if (finalGetUrl == null) { + String httpMethod = pollingContext.getData(PollingConstants.HTTP_METHOD); + if (HttpMethod.PUT.name().equalsIgnoreCase(httpMethod) + || HttpMethod.PATCH.name().equalsIgnoreCase(httpMethod)) { + finalGetUrl = pollingContext.getData(PollingConstants.REQUEST_URL); + } else if (HttpMethod.POST.name().equalsIgnoreCase(httpMethod) + && pollingContext.getData(PollingConstants.LOCATION) != null) { + finalGetUrl = pollingContext.getData(PollingConstants.LOCATION); + } else { + throw LOGGER.logExceptionAsError(new AzureException("Cannot get final result")); + } + } + + if (finalGetUrl == null) { + String latestResponseBody = pollingContext.getData(PollingConstants.POLL_RESPONSE_BODY); + return PollingUtils.deserializeResponseSync(BinaryData.fromString(latestResponseBody), serializer, + resultType); + } + + HttpRequest request = new HttpRequest(HttpMethod.GET, finalGetUrl); + try (HttpResponse response = httpPipeline.sendSync(request, context)) { + BinaryData responseBody = response.getBodyAsBinaryData(); + return PollingUtils.deserializeResponseSync(responseBody, serializer, resultType); + } + } +} diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncPoller.java b/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncPoller.java index d9e6f6a1a4908..6bd6145ba5de0 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncPoller.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncPoller.java @@ -3,13 +3,16 @@ package com.azure.core.util.polling; +import com.azure.core.http.rest.Response; +import com.azure.core.util.serializer.TypeReference; + import java.time.Duration; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.function.Supplier; /** - * A type that offers API that simplifies the task of executing long-running operations against - * an Azure service. + * A type that offers API that simplifies the task of executing long-running operations against an Azure service. * *

* It provides the following functionality: @@ -41,8 +44,8 @@ public interface SyncPoller { PollResponse waitForCompletion(); /** - * Wait for polling to complete with a timeout. The polling is considered complete based on - * status defined in {@link LongRunningOperationStatus}. + * Wait for polling to complete with a timeout. The polling is considered complete based on status defined in + * {@link LongRunningOperationStatus}. * * @param timeout the duration to waits for polling completion. * @return the final poll response. @@ -69,7 +72,7 @@ public interface SyncPoller { PollResponse waitUntil(Duration timeout, LongRunningOperationStatus statusToWaitFor); /** - * Retrieve the final result of the long running operation. + * Retrieve the final result of the long-running operation. * * @return the final result of the long-running operation if there is one. */ @@ -99,23 +102,20 @@ default SyncPoller setPollInterval(Duration pollInterval) { * Creates default SyncPoller. * * @param pollInterval the polling interval. - * @param syncActivationOperation the operation to synchronously activate (start) the long running operation, - * this operation will be called with a new {@link PollingContext}. - * @param pollOperation the operation to poll the current state of long running operation, this parameter - * is required and the operation will be called with current {@link PollingContext}. - * @param cancelOperation a {@link Function} that represents the operation to cancel the long running operation - * if service supports cancellation, this parameter is required and if service does not support cancellation - * then the implementer should return Mono.error with an error message indicating absence of cancellation - * support, the operation will be called with current {@link PollingContext}. - * @param fetchResultOperation a {@link Function} that represents the operation to retrieve final result of - * the long running operation if service support it, this parameter is required and operation will be called - * current {@link PollingContext}, if service does not have an api to fetch final result and if final result - * is same as final poll response value then implementer can choose to simply return value from provided - * final poll response. - * + * @param syncActivationOperation the operation to synchronously activate (start) the long-running operation, this + * operation will be called with a new {@link PollingContext}. + * @param pollOperation the operation to poll the current state of long-running operation, this parameter is + * required and the operation will be called with current {@link PollingContext}. + * @param cancelOperation a {@link Function} that represents the operation to cancel the long-running operation if + * service supports cancellation, this parameter is required and if service does not support cancellation then the + * implementer should throw an exception with an error message indicating absence of cancellation support, the + * operation will be called with current {@link PollingContext}. + * @param fetchResultOperation a {@link Function} that represents the operation to retrieve final result of the + * long-running operation if service support it, this parameter is required and operation will be called current + * {@link PollingContext}, if service does not have an api to fetch final result and if final result is same as + * final poll response value then implementer can choose to simply return value from provided final poll response. * @param The type of poll response value. * @param The type of the final result of long-running operation. - * * @return new {@link SyncPoller} instance. */ @SuppressWarnings("unchecked") @@ -127,4 +127,45 @@ static SyncPoller createPoller(Duration pollInterval, return new SimpleSyncPoller<>(pollInterval, syncActivationOperation, pollOperation, cancelOperation, fetchResultOperation); } + + /** + * Creates PollerFlux. + *

+ * This create method uses a {@link SyncPollingStrategy} to poll the status of a long-running operation after the + * activation operation is invoked. See {@link SyncPollingStrategy} for more details of known polling strategies and + * how to create a custom strategy. + * + * @param pollInterval the polling interval + * @param initialOperation the activation operation to activate (start) the long-running operation. This operation + * will be invoked at most once across all subscriptions. This parameter is required. If there is no specific + * activation work to be done then invocation should return null, this operation will be called with a new + * {@link PollingContext}. + * @param strategy a known syncrhonous strategy for polling a long-running operation in Azure + * @param pollResponseType the {@link TypeReference} of the response type from a polling call, or BinaryData if raw + * response body should be kept. This should match the generic parameter {@link U}. + * @param resultType the {@link TypeReference} of the final result object to deserialize into, or BinaryData if raw + * response body should be kept. This should match the generic parameter {@link U}. + * @param The type of poll response value. + * @param The type of the final result of long-running operation. + * @return new {@link SyncPoller} instance. + */ + static SyncPoller createPoller(Duration pollInterval, Supplier> initialOperation, + SyncPollingStrategy strategy, TypeReference pollResponseType, TypeReference resultType) { + Function, PollResponse> syncActivationOperation = pollingContext -> { + Response response = initialOperation.get(); + if (!strategy.canPoll(response)) { + throw new IllegalStateException("Cannot poll with strategy " + strategy.getClass().getSimpleName()); + } + + return strategy.onInitialResponse(response, pollingContext, pollResponseType); + }; + + Function, PollResponse> pollOperation = + context -> strategy.poll(context, pollResponseType); + BiFunction, PollResponse, T> cancelOperation = strategy::cancel; + Function, U> fetchResultOperation = context -> strategy.getResult(context, resultType); + + return createPoller(pollInterval, syncActivationOperation, pollOperation, cancelOperation, + fetchResultOperation); + } } diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncPollingStrategy.java b/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncPollingStrategy.java new file mode 100644 index 0000000000000..1cf66f2679d0f --- /dev/null +++ b/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncPollingStrategy.java @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.util.polling; + +import com.azure.core.http.rest.Response; +import com.azure.core.util.BinaryData; +import com.azure.core.util.serializer.TypeReference; + +/** + * Represents a known strategy for polling a long-running operation in Azure. + *

+ * The methods in the polling strategy will be invoked from the {@link SyncPoller}. The order of the invocations is: + * + *

    + *
  1. {@link #canPoll(Response)} - exits if returns false
  2. + *
  3. {@link #onInitialResponse(Response, PollingContext, TypeReference)} - immediately after + * {@link #canPoll(Response)} returns true
  4. + *
  5. {@link #poll(PollingContext, TypeReference)} - invoked after each polling interval, if the last polling response + * indicates an "In Progress" status. Returns a {@link PollResponse} with the latest status
  6. + *
  7. {@link #getResult(PollingContext, TypeReference)} - invoked when the last polling response indicates a + * "Successfully Completed" status. Returns the final result of the given type
  8. + *
+ * + * If the user decides to cancel the {@link PollingContext} or {@link SyncPoller}, the + * {@link #cancel(PollingContext, PollResponse)} method will be invoked. If the strategy doesn't support cancellation, + * an error will be returned. + *

+ * Users are not expected to provide their own implementation of this interface. Built-in polling strategies in this + * library and other client libraries are often sufficient for handling polling in most long-running operations in + * Azure. When there are special scenarios, built-in polling strategies can be inherited and select methods can be + * overridden to accomplish the polling requirements, without writing an entire polling strategy from scratch. + * + * @param the {@link TypeReference} of the response type from a polling call, or BinaryData if raw response body + * should be kept + * @param the {@link TypeReference} of the final result object to deserialize into, or BinaryData if raw response + * body should be kept + */ +public interface SyncPollingStrategy { + /** + * Checks if this strategy is able to handle polling for this long-running operation based on the information in the + * initial response. + * + * @param initialResponse the response from the initial method call to activate the long-running operation + * @return true if this polling strategy can handle the initial response, false if not + */ + boolean canPoll(Response initialResponse); + + /** + * Parses the initial response into a {@link LongRunningOperationStatus}, and stores information useful for polling + * in the {@link PollingContext}. If the result is anything other than + * {@link LongRunningOperationStatus#IN_PROGRESS}, the long-running operation will be terminated and none of the + * other methods will be invoked. + * + * @param response the response from the initial method call to activate the long-running operation + * @param pollingContext the {@link PollingContext} for the current polling operation + * @param pollResponseType the {@link TypeReference} of the response type from a polling call, or BinaryData if raw + * response body should be kept. This should match the generic parameter {@link U}. + * @return the poll response containing the status and the response content + */ + PollResponse onInitialResponse(Response response, PollingContext pollingContext, + TypeReference pollResponseType); + + /** + * Parses the response from the polling URL into a {@link PollResponse}, and stores information useful for further + * polling and final response in the {@link PollingContext}. The result must have the + * {@link LongRunningOperationStatus} specified, and the entire polling response content as a {@link BinaryData}. + * + * @param pollingContext the {@link PollingContext} for the current polling operation + * @param pollResponseType the {@link TypeReference} of the response type from a polling call, or BinaryData if raw + * response body should be kept. This should match the generic parameter {@link U}. + * @return the poll response containing the status and the response content + */ + PollResponse poll(PollingContext pollingContext, TypeReference pollResponseType); + + /** + * Parses the response from the final GET call into the result type of the long-running operation. + * + * @param pollingContext the {@link PollingContext} for the current polling operation + * @param resultType the {@link TypeReference} of the final result object to deserialize into, or BinaryData if raw + * response body should be kept. + * @return the final result + */ + U getResult(PollingContext pollingContext, TypeReference resultType); + + /** + * Cancels the long-running operation if service supports cancellation. If service does not support cancellation + * then the implementer should throw an {@link IllegalStateException} with an error message indicating absence of + * cancellation. + *

+ * Implementing this method is optional - by default, cancellation will not be supported unless overridden. + * + * @param pollingContext the {@link PollingContext} for the current polling operation, or null if the polling has + * started in a {@link SyncPoller} + * @param initialResponse the response from the initial operation + * @return the cancellation response content + * @throws IllegalStateException If cancellation isn't supported. + */ + default T cancel(PollingContext pollingContext, PollResponse initialResponse) { + throw new IllegalStateException("Cancellation is not supported."); + } +} diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncStatusCheckPollingStrategy.java b/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncStatusCheckPollingStrategy.java new file mode 100644 index 0000000000000..724cfe1a44464 --- /dev/null +++ b/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/SyncStatusCheckPollingStrategy.java @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.util.polling; + +import com.azure.core.exception.AzureException; +import com.azure.core.http.rest.Response; +import com.azure.core.implementation.ImplUtils; +import com.azure.core.implementation.serializer.DefaultJsonSerializer; +import com.azure.core.util.logging.ClientLogger; +import com.azure.core.util.polling.implementation.PollingUtils; +import com.azure.core.util.serializer.ObjectSerializer; +import com.azure.core.util.serializer.TypeReference; + +import java.time.Duration; +import java.time.OffsetDateTime; + +/** + * Fallback polling strategy that doesn't poll but exits successfully if no other polling strategies are detected and + * status code is 2xx. + * + * @param the type of the response type from a polling call, or BinaryData if raw response body should be kept + * @param the type of the final result object to deserialize into, or BinaryData if raw response body should be + * kept + */ +public class SyncStatusCheckPollingStrategy implements SyncPollingStrategy { + private static final ClientLogger LOGGER = new ClientLogger(SyncStatusCheckPollingStrategy.class); + private static final ObjectSerializer DEFAULT_SERIALIZER = new DefaultJsonSerializer(); + + private final ObjectSerializer serializer; + + /** + * Creates a status check polling strategy with a JSON serializer. + */ + public SyncStatusCheckPollingStrategy() { + this(DEFAULT_SERIALIZER); + } + + /** + * Creates a status check polling strategy with a custom object serializer. + * + * @param serializer a custom serializer for serializing and deserializing polling responses + */ + public SyncStatusCheckPollingStrategy(ObjectSerializer serializer) { + this.serializer = (serializer == null) ? DEFAULT_SERIALIZER : serializer; + } + + @Override + public boolean canPoll(Response initialResponse) { + return true; + } + + @Override + public PollResponse onInitialResponse(Response response, PollingContext pollingContext, + TypeReference pollResponseType) { + if (response.getStatusCode() == 200 || response.getStatusCode() == 201 + || response.getStatusCode() == 202 || response.getStatusCode() == 204) { + Duration retryAfter = ImplUtils.getRetryAfterFromHeaders(response.getHeaders(), OffsetDateTime::now); + return new PollResponse<>(LongRunningOperationStatus.SUCCESSFULLY_COMPLETED, + PollingUtils.convertResponseSync(response.getValue(), serializer, pollResponseType), retryAfter); + } else { + throw LOGGER.logExceptionAsError(new AzureException("Operation failed or cancelled: " + + response.getStatusCode())); + } + } + + @Override + public PollResponse poll(PollingContext context, TypeReference pollResponseType) { + throw LOGGER.logExceptionAsError(new IllegalStateException( + "StatusCheckPollingStrategy doesn't support polling")); + } + + @Override + public U getResult(PollingContext pollingContext, TypeReference resultType) { + T activationResponse = pollingContext.getActivationResponse().getValue(); + return PollingUtils.convertResponseSync(activationResponse, serializer, resultType); + } +} diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/implementation/PollResult.java b/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/implementation/PollResult.java new file mode 100644 index 0000000000000..5d8677fd94617 --- /dev/null +++ b/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/implementation/PollResult.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.util.polling.implementation; + +import com.azure.core.util.polling.LongRunningOperationStatus; +import com.fasterxml.jackson.annotation.JsonSetter; + +/** + * A simple structure representing the partial response received from an operation location URL, containing the + * information of the status of the long-running operation. + */ +public final class PollResult { + private LongRunningOperationStatus status; + private String resourceLocation; + + /** + * Gets the status of the long-running operation. + * + * @return the status represented as a {@link LongRunningOperationStatus} + */ + public LongRunningOperationStatus getStatus() { + return status; + } + + /** + * Sets the long-running operation status in the format of a string returned by the service. This is called by + * the deserializer when a response is received. + * + * @param status the status of the long-running operation + * @return the modified PollResult instance + */ + @JsonSetter + public PollResult setStatus(String status) { + if (PollingConstants.STATUS_NOT_STARTED.equalsIgnoreCase(status)) { + this.status = LongRunningOperationStatus.NOT_STARTED; + } else if (PollingConstants.STATUS_IN_PROGRESS.equalsIgnoreCase(status) + || PollingConstants.STATUS_RUNNING.equalsIgnoreCase(status)) { + this.status = LongRunningOperationStatus.IN_PROGRESS; + } else if (PollingConstants.STATUS_SUCCEEDED.equalsIgnoreCase(status)) { + this.status = LongRunningOperationStatus.SUCCESSFULLY_COMPLETED; + } else if (PollingConstants.STATUS_FAILED.equalsIgnoreCase(status)) { + this.status = LongRunningOperationStatus.FAILED; + } else { + this.status = LongRunningOperationStatus.fromString(status, true); + } + return this; + } + + /** + * Sets the long-running operation status in the format of the {@link LongRunningOperationStatus} enum. + * + * @param status the status of the long-running operation + * @return the modified PollResult instance + */ + public PollResult setStatus(LongRunningOperationStatus status) { + this.status = status; + return this; + } + + /** + * Gets the resource location URL to get the final result. This is often available in the response when the + * long-running operation has been successfully completed. + * + * @return the resource location URL to get the final result + */ + public String getResourceLocation() { + return resourceLocation; + } + + /** + * Sets the resource location URL. this should only be called by the deserializer when a response is received. + * + * @param resourceLocation the resource location URL + * @return the modified PollResult instance + */ + public PollResult setResourceLocation(String resourceLocation) { + this.resourceLocation = resourceLocation; + return this; + } +} diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/implementation/PollingUtils.java b/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/implementation/PollingUtils.java index 53ccaa3edc709..0c8a6ed140c0d 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/implementation/PollingUtils.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/util/polling/implementation/PollingUtils.java @@ -34,6 +34,21 @@ public static Mono serializeResponse(Object response, ObjectSerializ } } + /** + * Serialize a response to a {@link BinaryData}. If the response is already a {@link BinaryData}, return as is. + * + * @param response the response from an activation or polling call + * @param serializer the object serializer to use + * @return a {@link BinaryData} response + */ + public static BinaryData serializeResponseSync(Object response, ObjectSerializer serializer) { + if (response instanceof BinaryData) { + return (BinaryData) response; + } else { + return BinaryData.fromObject(response, serializer); + } + } + /** * Deserialize a {@link BinaryData} into a poll response type. If the poll response type is also a * {@link BinaryData}, return as is. @@ -54,13 +69,33 @@ public static Mono deserializeResponse(BinaryData binaryData, ObjectSeria } } + /** + * Deserialize a {@link BinaryData} into a poll response type. If the poll response type is also a + * {@link BinaryData}, return as is. + * + * @param binaryData the binary data to deserialize + * @param serializer the object serializer to use + * @param typeReference the {@link TypeReference} of the poll response type + * @param the generic parameter of the poll response type + * @return the deserialized object + */ + @SuppressWarnings("unchecked") + public static T deserializeResponseSync(BinaryData binaryData, ObjectSerializer serializer, + TypeReference typeReference) { + if (TypeUtil.isTypeOrSubTypeOf(BinaryData.class, typeReference.getJavaType())) { + return (T) binaryData; + } else { + return binaryData.toObject(typeReference, serializer); + } + } + /** * Converts an object received from an activation or a polling call to another type requested by the user. If the * object type is identical to the type requested by the user, it's returned as is. If the response is null, an * empty publisher is returned. - * + *

* This is useful when an activation response needs to be converted to a polling response type, or a final result - * type, if the long running operation completes upon activation. + * type, if the long-running operation completes upon activation. * * @param response the response from an activation or polling call * @param serializer the object serializer to use @@ -81,11 +116,37 @@ public static Mono convertResponse(Object response, ObjectSerializer seri } } + /** + * Converts an object received from an activation or a polling call to another type requested by the user. If the + * object type is identical to the type requested by the user, it's returned as is. If the response is null, null + * is returned. + *

+ * This is useful when an activation response needs to be converted to a polling response type, or a final result + * type, if the long-running operation completes upon activation. + * + * @param response the response from an activation or polling call + * @param serializer the object serializer to use + * @param typeReference the {@link TypeReference} of the user requested type + * @param the generic parameter of the user requested type + * @return the converted object + */ + @SuppressWarnings("unchecked") + public static T convertResponseSync(Object response, ObjectSerializer serializer, + TypeReference typeReference) { + if (response == null) { + return null; + } else if (TypeUtil.isTypeOrSubTypeOf(response.getClass(), typeReference.getJavaType())) { + return (T) response; + } else { + return deserializeResponseSync(serializeResponseSync(response, serializer), serializer, typeReference); + } + } + /** * Create an absolute path from the endpoint if the 'path' is relative. Otherwise, return the 'path' as absolute * path. * - * @param path an relative path or absolute path. + * @param path a relative path or absolute path. * @param endpoint an endpoint to create the absolute path if the path is relative. * @return an absolute path. */ diff --git a/sdk/core/azure-core/src/main/java/module-info.java b/sdk/core/azure-core/src/main/java/module-info.java index 79da1711203ba..0a9669b124f5a 100644 --- a/sdk/core/azure-core/src/main/java/module-info.java +++ b/sdk/core/azure-core/src/main/java/module-info.java @@ -48,6 +48,7 @@ opens com.azure.core.util to com.fasterxml.jackson.databind; opens com.azure.core.util.logging to com.fasterxml.jackson.databind; opens com.azure.core.util.polling to com.fasterxml.jackson.databind; + opens com.azure.core.util.polling.implementation to com.fasterxml.jackson.databind; opens com.azure.core.util.serializer to com.fasterxml.jackson.databind; opens com.azure.core.implementation to com.fasterxml.jackson.databind; opens com.azure.core.implementation.logging to com.fasterxml.jackson.databind;