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
/*******************************************************************************
 *   © 2019-2024 SAP SE or an SAP affiliate company. All rights reserved.
 ******************************************************************************/

package com.sap.cloud.mt.subscription;

import com.fasterxml.jackson.databind.ObjectMapper;
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;
	private static final ObjectMapper mapper = new ObjectMapper();

	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 (StringUtils.isBlank((String) result.get(STATUS))) {
					var resultStr = mapper.writeValueAsString(result);
					logger.error("Mtx returned no status for job {}. Mtx returned {}", jobId, resultStr);
					return new AsyncCallResult(new InternalError("Mtx returned no status for job %s. Mtx returned %s".formatted(jobId, resultStr)));
				}
				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\". Mtx returned %s".formatted(mapper.writeValueAsString(result))));
					default:
						return new AsyncCallResult(new InternalError(
								"Unexpected status %s. Mtx returned %s".formatted(result.get(STATUS), mapper.writeValueAsString(result))));
				}
			} 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