All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cloud.mt.subscription.MtxTools Maven / Gradle / Ivy

There is a newer version: 3.3.1
Show newest version
/*
 * ----------------------------------------------------------------
 * © 2022 SAP SE or an SAP affiliate company. All rights reserved.
 * ----------------------------------------------------------------
 *
 */

package com.sap.cloud.mt.subscription;

import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.sap.cloud.mt.subscription.HanaEncryptionTool.DbEncryptionMode;
import com.sap.cloud.mt.subscription.exceptions.AuthorityError;
import com.sap.cloud.mt.subscription.exceptions.InternalError;
import com.sap.cloud.mt.subscription.exceptions.NotFound;
import com.sap.cloud.mt.subscription.exceptions.ParameterError;
import com.sap.cloud.mt.subscription.exits.Exits;
import com.sap.cloud.mt.subscription.json.DeletePayload;
import com.sap.cloud.mt.subscription.json.SubscriptionPayload;
import com.sap.cloud.mt.tools.api.AsyncCallResult;
import com.sap.cloud.mt.tools.api.ServiceResponse;
import com.sap.cloud.mt.tools.exception.InternalException;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.NameValuePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import static com.sap.cloud.mt.subscription.Tools.checkExternalTenantId;
import static com.sap.cloud.mt.subscription.Tools.getApplicationUrl;
import static com.sap.cloud.mt.subscription.Tools.waitSomeTime;

public class MtxTools {

    //Wait a little, to avoid that this threads report to the saas registry before the initial update was finished.
    //CIS runs into a problem when the callback comes before the initial CIS request is executed
    public static final Duration SAAS_REGISTRY_WAIT_TIME = Duration.ofMillis(20);
    private static final String X_JOB_ID = "x-job-id";
    private static final String LOCATION = "Location";
    private static Logger logger = LoggerFactory.getLogger(MtxTools.class);
    private static final String STATUS = "status";
    private static final String JOB_ID = "jobID";
    private final SecurityChecker securityChecker;
    //Domain from which the URL that is passed to CIC is calculated. It should be the URL of the application UI.
    private final String baseUiUrl;
    //Separator between subdomain and baseUIUrl: subdomain+urlSeparator+baseUiUrl
    private final String urlSeparator;
    private final PollingParameters pollingParameter;
    private final DbEncryptionMode hanaEncryptionMode;

    public MtxTools(SecurityChecker securityChecker, String baseUiUrl, String urlSeparator,
                    PollingParameters pollingParameter, DbEncryptionMode hanaEncryptionMode) {
        this.securityChecker = securityChecker;
        this.baseUiUrl = baseUiUrl;
        this.urlSeparator = urlSeparator;
        this.pollingParameter = pollingParameter;
        this.hanaEncryptionMode = hanaEncryptionMode;
    }

    public static AsyncCallResult waitForCompletion(String jobId, StatusProvider statusProvider, PollingParameters pollingParameter) {
        Instant start = Instant.now();
        while (true) {
            logger.debug("Wait for completion of job {}", jobId);
            try {
                Map result = statusProvider.getStatus(jobId);
                if (!result.containsKey(STATUS)) {
                    logger.debug("Mtx returned no status for job {}", jobId);
                    return new AsyncCallResult(new InternalError("Mtx returned no status for job " + jobId));
                }
                logger.debug("Mtx returned status {} for job {}", result.get(STATUS), jobId);
                switch (((String) result.get(STATUS)).toUpperCase(Locale.ENGLISH)) {
                    case "FINISHED":
                        return AsyncCallResult.createOk();
                    case "INITIAL", "QUEUED", "RUNNING", "PROCESSING":
                        break;
                    case "FAILED":
                        return new AsyncCallResult(new InternalError("Provisioning service returned with status \"failed\""));
                    default:
                        return new AsyncCallResult(new InternalError("Unexpected status" + ((String) result.get(STATUS))));
                }
            } catch (Exception e) {
                return new AsyncCallResult(e);
            }
            if (Duration.between(start, Instant.now()).compareTo(pollingParameter.getRequestTimeout()) >= 0) {
                logger.error("Maximum waiting time for job {} exceeded", jobId);
                return new AsyncCallResult(new InternalError("Maximum waiting time on called service exceeded"));
            }
            waitSomeTime(pollingParameter.getInterval());
        }
    }

    public static String extractJobId(ServiceResponse response) throws InternalException {
        String jobId = null;
        if (response.getHeaders() != null) {
            jobId = getFromHeader(response, X_JOB_ID);
            if (StringUtils.isNotBlank(jobId)) {
                return jobId;
            }
            //toDo Delete this after old sidecar deprecation and raise an error if nothing is set in the header
            //field
            // This code is only relevant for old sidecar
            String location = getFromHeader(response, LOCATION);
            String[] parts = location.split("/jobs/");
            if (parts.length == 2) {
                jobId = parts[1];
                if (StringUtils.isNotBlank(jobId)) {
                    if (jobId.startsWith("pollJob(")) {
                        //This is the format of mtxs sidecar which must set the x-job-id
                        throw new InternalException("The header x-job-id wasn't set");
                    }
                    return jobId;
                }
            }
        }
        jobId = (String) getResponseAsMap(response).get(JOB_ID);
        if (StringUtils.isBlank(jobId)) {
            throw new InternalException("No job id returned");
        }
        logger.debug("Returned jobId is {}", jobId);
        return jobId;
    }

    private static String getFromHeader(ServiceResponse response, String fieldName) {
        return Arrays.stream(response.getHeaders())
                .filter(h -> h.getName().equalsIgnoreCase(fieldName))
                .map(NameValuePair::getValue).findFirst().orElse("");
    }

    @SuppressWarnings("unchecked")
    private static Map getResponseAsMap(ServiceResponse response) throws InternalException {
        if (response.getPayload().isPresent()) {
            try {
                return new Gson().fromJson(response.getPayload().get(), Map.class);//NOSONAR
            } catch (JsonSyntaxException e) {
                throw new InternalException("No map returned from mtx service", e);
            }
        } else {
            return new HashMap<>();
        }
    }

    public void unsubscribe(String tenantId,
                            UnSubscribeExecutor unsubscribeExecutor,
                            StatusProvider statusProvider,
                            DeletePayload deletePayload,
                            boolean withoutAuthorityCheck,
                            Exits exits) throws InternalError, ParameterError, AuthorityError {
        if (!withoutAuthorityCheck) {
            securityChecker.checkSubscriptionAuthority();
        }
        checkExternalTenantId(tenantId);
        //Only if this exit returns true, unsubscribe is performed
        boolean isUnsubscribePossible = Boolean.TRUE.equals(exits.getBeforeUnSubscribeMethod().call(tenantId, deletePayload));
        if (isUnsubscribePossible) {
            String jobId = unsubscribeExecutor.execute();
            AsyncCallResult asyncCallResult = waitForCompletion(jobId, statusProvider, pollingParameter);
            if (asyncCallResult.isOk()) {
                exits.getAfterUnSubscribeMethod().call(tenantId, deletePayload);
            } else {
                throw new InternalError(asyncCallResult.getException());
            }
        } else {
            logger.debug("Unsubscribe exit returned false => skipped unsubscribe for tenant {}", tenantId);
        }
    }

    public String subscribe(String tenantId,
                            SubscribeExecutor subscribeExecutor,
                            StatusProvider statusProvider,
                            SubscriptionPayload subscriptionPayload,
                            boolean withoutAuthorityCheck,
                            Exits exits) throws InternalError, ParameterError, AuthorityError {
        if (!withoutAuthorityCheck) {
            securityChecker.checkSubscriptionAuthority();
        }
        checkExternalTenantId(tenantId);
        var serviceCreateOptions = new ServiceCreateOptions(exits.getBeforeSubscribeMethod().call(tenantId, subscriptionPayload));
        var payloadAccess = SubscriptionPayloadAccess.create(subscriptionPayload.getMap());
        HanaEncryptionTool.addEncryptionParameter(serviceCreateOptions, hanaEncryptionMode, payloadAccess);
        String applicationUrl = getApplicationUrl(subscriptionPayload, exits.getSubscribeExit()::uiURL, exits.getSubscribeExit()::uiURL, baseUiUrl, urlSeparator);
        String jobId;
        try {
            jobId = subscribeExecutor.execute(serviceCreateOptions);
        } catch (InternalError e) {
            exits.getAfterSubscribeMethod().call(tenantId, subscriptionPayload, false);
            throw e;
        }
        AsyncCallResult asyncCallResult = waitForCompletion(jobId, statusProvider, pollingParameter);
        exits.getAfterSubscribeMethod().call(tenantId, subscriptionPayload, asyncCallResult.isOk());
        if (asyncCallResult.isNotOk()) {
            throw new InternalError(asyncCallResult.getException());
        }
        return applicationUrl;
    }

    @FunctionalInterface
    public interface UnSubscribeExecutor {
        String execute() throws InternalError;
    }

    @FunctionalInterface
    public interface SubscribeExecutor {
        String execute(ServiceCreateOptions serviceCreateOptions) throws InternalError;
    }

    @FunctionalInterface
    public interface StatusProvider {
        Map getStatus(String jobId) throws InternalError, NotFound;
    }

    @FunctionalInterface
    public interface SaasRegistryCaller {
        void callSaasRegistry(boolean isOk, String message, String applicationUrl) throws InternalError;
    }

    @FunctionalInterface
    public interface SupplierWithInternalError {
        T get() throws InternalError;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy