PhoneNumbersAsyncClient.java

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.communication.phonenumbers;

import com.azure.communication.phonenumbers.implementation.PhoneNumberAdminClientImpl;
import com.azure.communication.phonenumbers.implementation.PhoneNumbersImpl;
import com.azure.communication.phonenumbers.implementation.converters.PhoneNumberErrorConverter;
import com.azure.communication.phonenumbers.implementation.models.CommunicationErrorResponseException;
import com.azure.communication.phonenumbers.implementation.models.PhoneNumbersPurchasePhoneNumbersResponse;
import com.azure.communication.phonenumbers.implementation.models.PhoneNumberPurchaseRequest;
import com.azure.communication.phonenumbers.implementation.models.PhoneNumberRawOperation;
import com.azure.communication.phonenumbers.implementation.models.PhoneNumberSearchRequest;
import com.azure.communication.phonenumbers.implementation.models.PhoneNumbersSearchAvailablePhoneNumbersResponse;
import com.azure.communication.phonenumbers.implementation.models.PhoneNumbersReleasePhoneNumberResponse;
import com.azure.communication.phonenumbers.implementation.models.PhoneNumberCapabilitiesRequest;
import com.azure.communication.phonenumbers.implementation.models.PhoneNumbersUpdateCapabilitiesResponse;
import com.azure.communication.phonenumbers.models.PurchasedPhoneNumber;
import com.azure.communication.phonenumbers.models.ReleasePhoneNumberResult;
import com.azure.communication.phonenumbers.models.PhoneNumberAssignmentType;
import com.azure.communication.phonenumbers.models.PhoneNumberCapabilities;
import com.azure.communication.phonenumbers.models.PhoneNumberError;
import com.azure.communication.phonenumbers.models.PhoneNumberErrorResponseException;
import com.azure.communication.phonenumbers.models.PhoneNumberOperation;
import com.azure.communication.phonenumbers.models.PhoneNumberOperationStatus;
import com.azure.communication.phonenumbers.models.PhoneNumberSearchOptions;
import com.azure.communication.phonenumbers.models.PhoneNumberSearchResult;
import com.azure.communication.phonenumbers.models.PhoneNumberType;
import com.azure.communication.phonenumbers.models.PurchasePhoneNumbersResult;
import com.azure.core.annotation.ReturnType;
import com.azure.core.annotation.ServiceClient;
import com.azure.core.annotation.ServiceMethod;
import com.azure.core.http.rest.PagedFlux;
import com.azure.core.http.rest.Response;
import com.azure.core.util.Context;
import com.azure.core.util.CoreUtils;
import com.azure.core.util.logging.ClientLogger;
import com.azure.core.util.polling.LongRunningOperationStatus;
import com.azure.core.util.polling.PollResponse;
import com.azure.core.util.polling.PollerFlux;
import com.azure.core.util.polling.PollingContext;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;

import static com.azure.core.util.FluxUtil.monoError;
import static com.azure.core.util.FluxUtil.withContext;

/**
 * Asynchronous client for Communication service phone number operations.
 */
@ServiceClient(builder = PhoneNumbersClientBuilder.class, isAsync = true)
public final class PhoneNumbersAsyncClient {
    private final ClientLogger logger = new ClientLogger(PhoneNumbersAsyncClient.class);
    private final PhoneNumbersImpl client;
    private final Duration defaultPollInterval = Duration.ofSeconds(1);

    PhoneNumbersAsyncClient(PhoneNumberAdminClientImpl phoneNumberAdminClient) {
        this.client = phoneNumberAdminClient.getPhoneNumbers();
    }

    /**
     * Gets information about a purchased phone number.
     * @param phoneNumber The phone number id in E.164 format. The leading plus can be either + or encoded
     *                    as %2B.
     * @return {@link PurchasedPhoneNumber} representing the purchased telephone number.
     * @throws NullPointerException if {@code phoneNumber} is null.
     */
    @ServiceMethod(returns = ReturnType.SINGLE)
    public Mono<PurchasedPhoneNumber> getPurchasedPhoneNumber(String phoneNumber) {
        if (Objects.isNull(phoneNumber)) {
            return monoError(logger, new NullPointerException("'phoneNumber' cannot be null."));
        }
        return client.getByNumberAsync(phoneNumber)
            .onErrorMap(CommunicationErrorResponseException.class, e -> translateException(e));
    }

    /**
     * Gets information about a purchased phone number with response.
     * @param phoneNumber The phone number id in E.164 format. The leading plus can be either + or encoded
     *                    as %2B.
     * @return {@link PurchasedPhoneNumber} representing the purchased telephone number.
     * @throws NullPointerException if {@code phoneNumber} is null.
     */
    @ServiceMethod(returns = ReturnType.SINGLE)
    public Mono<Response<PurchasedPhoneNumber>> getPurchasedPhoneNumberWithResponse(String phoneNumber) {
        if (Objects.isNull(phoneNumber)) {
            return monoError(logger, new NullPointerException("'phoneNumber' cannot be null."));
        }
        return client.getByNumberWithResponseAsync(phoneNumber)
            .onErrorMap(CommunicationErrorResponseException.class, e -> translateException(e));
    }

    /**
     * Gets the list of the purchased phone numbers.
     *
     * @return A {@link PagedFlux} of {@link PurchasedPhoneNumber} instances representing a purchased telephone numbers.
     */
    @ServiceMethod(returns = ReturnType.COLLECTION)
    public PagedFlux<PurchasedPhoneNumber> listPurchasedPhoneNumbers() {
        try {
            return client.listPhoneNumbersAsync(null, null);
        } catch (RuntimeException ex) {
            return new PagedFlux<>(() -> monoError(logger, ex));
        }
    }

    /**
     * Starts the search for available phone numbers to purchase.
     *
     * @param countryCode The ISO 3166-2 country code.
     * @param phoneNumberType {@link PhoneNumberType} The phone number type.
     * @param assignmentType {@link PhoneNumberAssignmentType} The phone number assignment type.
     * @param capabilities {@link PhoneNumberCapabilities} The phone number capabilities.
     * @return A {@link PollerFlux} object with the reservation result.
     * @throws NullPointerException if {@code countryCode} or {@code searchRequest} is null.
     */
    @ServiceMethod(returns = ReturnType.LONG_RUNNING_OPERATION)
    public PollerFlux<PhoneNumberOperation, PhoneNumberSearchResult> beginSearchAvailablePhoneNumbers(
        String countryCode, PhoneNumberType phoneNumberType, PhoneNumberAssignmentType assignmentType, PhoneNumberCapabilities capabilities) {
        return beginSearchAvailablePhoneNumbers(countryCode, phoneNumberType, assignmentType, capabilities, null, null);
    }

    /**
     * Starts the search for available phone numbers to purchase.
     *
     * @param countryCode The ISO 3166-2 country code.
     * @param phoneNumberType {@link PhoneNumberType} The phone number type.
     * @param assignmentType {@link PhoneNumberAssignmentType} The phone number assignment type.
     * @param capabilities {@link PhoneNumberCapabilities} The phone number capabilities.
     * @param searchOptions The phone number search options.
     * @return A {@link PollerFlux} object with the reservation result.
     * @throws NullPointerException if {@code countryCode} or {@code searchRequest} is null.
     * @throws RuntimeException if search operation fails.
     */
    @ServiceMethod(returns = ReturnType.LONG_RUNNING_OPERATION)
    public PollerFlux<PhoneNumberOperation, PhoneNumberSearchResult> beginSearchAvailablePhoneNumbers(
        String countryCode, PhoneNumberType phoneNumberType, PhoneNumberAssignmentType assignmentType,
        PhoneNumberCapabilities capabilities, PhoneNumberSearchOptions searchOptions) {
        return beginSearchAvailablePhoneNumbers(countryCode, phoneNumberType, assignmentType, capabilities, searchOptions, null);
    }

    PollerFlux<PhoneNumberOperation, PhoneNumberSearchResult> beginSearchAvailablePhoneNumbers(
        String countryCode,  PhoneNumberType phoneNumberType, PhoneNumberAssignmentType assignmentType,
        PhoneNumberCapabilities capabilities, PhoneNumberSearchOptions searchOptions, Context context) {
        try {
            Objects.requireNonNull(countryCode, "'countryCode' cannot be null.");
            Objects.requireNonNull(phoneNumberType, "'phoneNumberType' cannot be null.");
            Objects.requireNonNull(assignmentType, "'assignmentType' cannot be null.");
            Objects.requireNonNull(capabilities, "'capabilities' cannot be null.");

            String areaCode = null;
            Integer quantity = null;
            if (searchOptions != null) {
                areaCode = searchOptions.getAreaCode();
                quantity = searchOptions.getQuantity();
            }
            PhoneNumberSearchRequest searchRequest = new PhoneNumberSearchRequest();
            searchRequest
                .setPhoneNumberType(phoneNumberType)
                .setAssignmentType(assignmentType)
                .setCapabilities(capabilities)
                .setAreaCode(areaCode)
                .setQuantity(quantity);

            return new PollerFlux<>(defaultPollInterval,
                searchAvailableNumbersInitOperation(countryCode, searchRequest, context),
                pollOperation(),
                cancelOperation(),
                searchAvailableNumbersFetchFinalResultOperation());

        } catch (RuntimeException ex) {
            return PollerFlux.error(ex);
        }
    }

    private Function<PollingContext<PhoneNumberOperation>, Mono<PhoneNumberOperation>>
        searchAvailableNumbersInitOperation(String countryCode, PhoneNumberSearchRequest searchRequest, Context context) {
        return (pollingContext) -> {
            return withContext(contextValue -> {
                if (context != null) {
                    contextValue = context;
                }
                return client.searchAvailablePhoneNumbersWithResponseAsync(countryCode, searchRequest, contextValue)
                    .onErrorMap(CommunicationErrorResponseException.class, e -> translateException(e))
                    .flatMap((PhoneNumbersSearchAvailablePhoneNumbersResponse response) -> {
                        pollingContext.setData("operationId", response.getDeserializedHeaders().getOperationId());
                        pollingContext.setData("searchId", response.getDeserializedHeaders().getSearchId());
                        return getOperation(pollingContext.getData("operationId"));
                    });
            });
        };
    }

    private Function<PollingContext<PhoneNumberOperation>, Mono<PollResponse<PhoneNumberOperation>>>
        pollOperation() {
        return (pollingContext) -> {
            return getOperation(pollingContext.getData("operationId"))
                .onErrorMap(CommunicationErrorResponseException.class, e -> translateException(e))
                .flatMap(operation -> {
                    if (operation.getStatus().toString().equalsIgnoreCase(PhoneNumberOperationStatus.SUCCEEDED.toString())) {
                        return Mono.just(new PollResponse<>(
                            LongRunningOperationStatus.SUCCESSFULLY_COMPLETED, operation));
                    } else if (operation.getStatus().toString().equalsIgnoreCase(PhoneNumberOperationStatus.FAILED.toString())) {
                        return Mono.just(new PollResponse<>(
                            LongRunningOperationStatus.FAILED, operation));
                    } else if (operation.getStatus().toString().equalsIgnoreCase(PhoneNumberOperationStatus.NOT_STARTED.toString())) {
                        return Mono.just(new PollResponse<>(
                            LongRunningOperationStatus.NOT_STARTED, operation));
                    }
                    return Mono.just(new PollResponse<>(LongRunningOperationStatus.IN_PROGRESS, operation));
                });
        };
    }

    private BiFunction<PollingContext<PhoneNumberOperation>,
        PollResponse<PhoneNumberOperation>, Mono<PhoneNumberOperation>>
        cancelOperation() {
        return (pollingContext, firstResponse) -> {
            if (firstResponse == null || firstResponse.getValue() == null) {
                return Mono.error(logger.logExceptionAsError(
                    new IllegalArgumentException("Cannot cancel a poll response that never started.")));
            }
            String operationId = firstResponse.getValue().getId();
            if (!CoreUtils.isNullOrEmpty(operationId)) {
                logger.info("Cancelling search available phone numbers operation for operation id: {}", operationId);
                return client.cancelOperationAsync(operationId).thenReturn(firstResponse.getValue())
                    .onErrorMap(CommunicationErrorResponseException.class, e -> translateException(e));
            }
            return Mono.empty();
        };
    }

    private Function<PollingContext<PhoneNumberOperation>, Mono<PhoneNumberSearchResult>>
        searchAvailableNumbersFetchFinalResultOperation() {
        return (pollingContext) -> {
            return client.getSearchResultAsync(pollingContext.getData("searchId"))
                .onErrorMap(CommunicationErrorResponseException.class, e -> translateException(e));
        };
    }

    /**
     * Starts the purchase of the phone number(s) in the search result associated with a given id.
     *
     * @param searchId ID of the search.
     * @return A {@link PollerFlux} object.
     * @throws NullPointerException if {@code searchId} is null.
     * @throws RuntimeException if purchase operation fails.
     */
    @ServiceMethod(returns = ReturnType.LONG_RUNNING_OPERATION)
    public PollerFlux<PhoneNumberOperation, PurchasePhoneNumbersResult> beginPurchasePhoneNumbers(String searchId) {
        return beginPurchasePhoneNumbers(searchId, null);
    }

    PollerFlux<PhoneNumberOperation, PurchasePhoneNumbersResult> beginPurchasePhoneNumbers(String searchId, Context context) {
        try {
            Objects.requireNonNull(searchId, "'searchId' cannot be null.");
            return new PollerFlux<>(defaultPollInterval,
                purchaseNumbersInitOperation(searchId, context),
                pollOperation(),
                (pollingContext, firstResponse) -> Mono.error(logger.logExceptionAsError(new RuntimeException("Cancellation is not supported"))),
                (pollingContext) -> Mono.just(new PurchasePhoneNumbersResult()));
        } catch (RuntimeException ex) {
            return PollerFlux.error(ex);
        }
    }

    private Function<PollingContext<PhoneNumberOperation>, Mono<PhoneNumberOperation>>
        purchaseNumbersInitOperation(String searchId, Context context) {
        return (pollingContext) -> {
            return withContext(contextValue -> {
                if (context != null) {
                    contextValue = context;
                }
                return client.purchasePhoneNumbersWithResponseAsync(new PhoneNumberPurchaseRequest().setSearchId(searchId), contextValue)
                    .onErrorMap(CommunicationErrorResponseException.class, e -> translateException(e))
                    .flatMap((PhoneNumbersPurchasePhoneNumbersResponse response) -> {
                        pollingContext.setData("operationId", response.getDeserializedHeaders().getOperationId());
                        return getOperation(pollingContext.getData("operationId"));
                    });
            });
        };
    }

    /**
     * Begins release of a purchased phone number.
     *
     * This function returns a Long Running Operation poller that allows you to wait indefinitely until the
     * operation is complete.
     * @param phoneNumber The phone number id in E.164 format. The leading plus can be either + or encoded
     *                    as %2B.
     * @return A {@link PollerFlux} object.
     * @throws NullPointerException if {@code phoneNumber} is null.
     * @throws RuntimeException if release operation fails.
     */
    @ServiceMethod(returns = ReturnType.LONG_RUNNING_OPERATION)
    public PollerFlux<PhoneNumberOperation, ReleasePhoneNumberResult> beginReleasePhoneNumber(String phoneNumber) {
        return beginReleasePhoneNumber(phoneNumber, null);
    }

    PollerFlux<PhoneNumberOperation, ReleasePhoneNumberResult> beginReleasePhoneNumber(String phoneNumber, Context context) {
        try {
            Objects.requireNonNull(phoneNumber, "'phoneNumber' cannot be null.");
            return new PollerFlux<>(defaultPollInterval,
                releaseNumberInitOperation(phoneNumber, context),
                pollOperation(),
                (pollingContext, firstResponse) -> Mono.error(logger.logExceptionAsError(new RuntimeException("Cancellation is not supported"))),
                (pollingContext) -> Mono.just(new ReleasePhoneNumberResult()));
        } catch (RuntimeException ex) {
            return PollerFlux.error(ex);
        }
    }

    private Function<PollingContext<PhoneNumberOperation>, Mono<PhoneNumberOperation>>
        releaseNumberInitOperation(String phoneNumber, Context context) {
        return (pollingContext) -> {
            return withContext(contextValue -> {
                if (context != null) {
                    contextValue = context;
                }
                return client.releasePhoneNumberWithResponseAsync(phoneNumber, contextValue)
                    .onErrorMap(CommunicationErrorResponseException.class, e -> translateException(e))
                    .flatMap((PhoneNumbersReleasePhoneNumberResponse response) -> {
                        pollingContext.setData("operationId", response.getDeserializedHeaders().getOperationId());
                        return getOperation(pollingContext.getData("operationId"));
                    });
            });
        };
    }

    /**
     * Update capabilities of a purchased phone number.
     * @param phoneNumber The phone number id in E.164 format. The leading plus can be either + or encoded
     *                    as %2B.
     * @param capabilities Update capabilities of a purchased phone number.
     * @return A {@link PollerFlux} object.
     * @throws NullPointerException if {@code phoneNumber} or {@code capabilities} is null.
     * @throws RuntimeException if update capabilities operation fails.
     */
    @ServiceMethod(returns = ReturnType.LONG_RUNNING_OPERATION)
    public PollerFlux<PhoneNumberOperation, PurchasedPhoneNumber>
        beginUpdatePhoneNumberCapabilities(String phoneNumber, PhoneNumberCapabilities capabilities) {
        return beginUpdatePhoneNumberCapabilities(phoneNumber, capabilities, null);
    }

    PollerFlux<PhoneNumberOperation, PurchasedPhoneNumber>
        beginUpdatePhoneNumberCapabilities(String phoneNumber, PhoneNumberCapabilities capabilities, Context context) {
        try {
            Objects.requireNonNull(phoneNumber, "'phoneNumber' cannot be null.");
            Objects.requireNonNull(capabilities, "'capabilities' cannot be null.");
            PhoneNumberCapabilitiesRequest capabilitiesRequest = new PhoneNumberCapabilitiesRequest()
                .setCalling(capabilities.getCalling())
                .setSms(capabilities.getSms());
            return new PollerFlux<>(defaultPollInterval,
                updateNumberCapabilitiesInitOperation(phoneNumber, capabilitiesRequest, context),
                pollOperation(),
                (pollingContext, firstResponse) -> Mono.error(logger.logExceptionAsError(new RuntimeException("Cancellation is not supported"))),
                updateNumberCapabilitiesFetchFinalResultOperation(phoneNumber));
        } catch (RuntimeException ex) {
            return PollerFlux.error(ex);
        }
    }

    private Function<PollingContext<PhoneNumberOperation>, Mono<PhoneNumberOperation>>
        updateNumberCapabilitiesInitOperation(String phoneNumber, PhoneNumberCapabilitiesRequest capabilitiesUpdateRequest, Context context) {
        return (pollingContext) -> {
            return withContext(contextValue -> {
                if (context != null) {
                    contextValue = context;
                }
                return client.updateCapabilitiesWithResponseAsync(phoneNumber, capabilitiesUpdateRequest, contextValue)
                    .onErrorMap(CommunicationErrorResponseException.class, e -> translateException(e))
                    .flatMap((PhoneNumbersUpdateCapabilitiesResponse response) -> {
                        pollingContext.setData("operationId", response.getDeserializedHeaders().getOperationId());
                        return getOperation(pollingContext.getData("operationId"));
                    });
            });
        };
    }

    private Function<PollingContext<PhoneNumberOperation>, Mono<PurchasedPhoneNumber>>
        updateNumberCapabilitiesFetchFinalResultOperation(String phoneNumber) {
        return (pollingContext) -> {
            return client.getByNumberAsync(phoneNumber)
                .onErrorMap(CommunicationErrorResponseException.class, e -> translateException(e));
        };
    }

    private Mono<PhoneNumberOperation> getOperation(String operationId) {
        return client.getOperationAsync(operationId)
            .onErrorMap(CommunicationErrorResponseException.class, e -> translateException(e))
            .flatMap((PhoneNumberRawOperation rawOperation) -> {
                if (rawOperation.getError() != null) {
                    return monoError(logger, new RuntimeException(rawOperation.getError().getMessage()));
                }
                PhoneNumberOperation operation = new PhoneNumberOperation(
                    rawOperation.getStatus(),
                    rawOperation.getResourceLocation(),
                    rawOperation.getCreatedDateTime(),
                    rawOperation.getId(),
                    rawOperation.getOperationType(),
                    rawOperation.getLastActionDateTime());
                return Mono.just(operation);
            });
    }

    private PhoneNumberErrorResponseException translateException(CommunicationErrorResponseException exception) {
        PhoneNumberError error = null;
        if (exception.getValue() != null) {
            error = PhoneNumberErrorConverter.convert(exception.getValue().getError());
        }
        return new PhoneNumberErrorResponseException(exception.getMessage(), exception.getResponse(), error);
    }
}