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

com.sap.cds.services.impl.persistence.JdbcPersistenceService Maven / Gradle / Ivy

There is a newer version: 3.2.0
Show newest version
/**************************************************************************
 * (C) 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
 **************************************************************************/
package com.sap.cds.services.impl.persistence;

import java.sql.Connection;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.VisibleForTesting;
import com.sap.cds.CdsDataStore;
import com.sap.cds.CdsDataStoreConnector;
import com.sap.cds.CdsException;
import com.sap.cds.CdsLockTimeoutException;
import com.sap.cds.NotNullConstraintException;
import com.sap.cds.Result;
import com.sap.cds.UniqueConstraintException;
import com.sap.cds.ql.CdsDataException;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnStatement;
import com.sap.cds.ql.cqn.CqnValidationException;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.services.EventContext;
import com.sap.cds.services.cds.CdsCreateEventContext;
import com.sap.cds.services.cds.CdsDeleteEventContext;
import com.sap.cds.services.cds.CdsReadEventContext;
import com.sap.cds.services.cds.CdsUpdateEventContext;
import com.sap.cds.services.cds.CdsUpsertEventContext;
import com.sap.cds.services.cds.CqnService;
import com.sap.cds.services.changeset.ChangeSetContext;
import com.sap.cds.services.changeset.ChangeSetContextSPI;
import com.sap.cds.services.changeset.ChangeSetListener;
import com.sap.cds.services.handler.annotations.Before;
import com.sap.cds.services.handler.annotations.HandlerOrder;
import com.sap.cds.services.handler.annotations.On;
import com.sap.cds.services.persistence.PersistenceService;
import com.sap.cds.services.request.RequestContext;
import com.sap.cds.services.runtime.CdsRuntime;
import com.sap.cds.services.transaction.ChangeSetMemberDelegate;
import com.sap.cds.services.transaction.TransactionManager;
import com.sap.cds.services.utils.CdsErrorStatuses;
import com.sap.cds.services.utils.ErrorStatusException;
import com.sap.cds.services.utils.OpenTelemetryUtils;
import com.sap.cds.services.utils.OrderConstants;
import com.sap.cds.services.utils.SessionContextUtils;
import com.sap.cds.services.utils.StringUtils;
import com.sap.cds.services.utils.TenantAwareCache;
import com.sap.cds.services.utils.model.CdsModelUtils;
import com.sap.cds.services.utils.model.CqnUtils;
import com.sap.cds.services.utils.services.AbstractCqnService;

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Scope;

public class JdbcPersistenceService extends AbstractCqnService implements PersistenceService {

	private static final Logger logger = LoggerFactory.getLogger(JdbcPersistenceService.class);

	private final TenantAwareCache cachedConnectors;
	private final Map cachedDataStores = new ConcurrentHashMap<>();
	private final TransactionManager txMgr;

	public JdbcPersistenceService(String name, Supplier connectionSupplier, TransactionManager txMgr, CdsRuntime runtime) {
		super(name, runtime);
		this.txMgr = txMgr;
		com.sap.cds.transaction.TransactionManager cds4jTxMgr = new com.sap.cds.transaction.TransactionManager() {
			@Override
			public boolean isActive() {
				ChangeSetContextSPI changeSetContext = (ChangeSetContextSPI) ChangeSetContext.getCurrent();
				boolean activeChangeSet = changeSetContext != null
						&& changeSetContext.hasChangeSetMember(txMgr.getName());
				boolean activeTxMgr = txMgr.isActive();
				return activeChangeSet || activeTxMgr;
			}

			@Override
			public void setRollbackOnly() {
				ChangeSetContext changeSetContext = ChangeSetContext.getCurrent();
				if (changeSetContext != null) {
					// this directly goes to the ChangeSet, instead of using the transaction manager
					changeSetContext.markForCancel();
				} else {
					txMgr.setRollbackOnly();
				}
			}
		};
		this.cachedConnectors = TenantAwareCache.create(() -> {
			return CdsDataStoreConnector.createJdbcConnector(RequestContext.getCurrent(runtime).getModel(), cds4jTxMgr)
					.connection(connectionSupplier)
					.config(new JdbcDataStoreConfiguration(runtime.getEnvironment().getCdsProperties())).build();
		}, runtime);
	}

	@Before
	@HandlerOrder(OrderConstants.Before.TRANSACTION_BEGIN)
	protected void ensureTransaction(EventContext context) {
		// register and begin transaction
		ChangeSetContextSPI changeSetContext = (ChangeSetContextSPI) context.getChangeSetContext();
		if (!changeSetContext.isMarkedTransactional() && requiresTransaction(context)) {
			changeSetContext.markTransactional();
		}

		if (changeSetContext.isMarkedTransactional() && !changeSetContext.hasChangeSetMember(txMgr.getName())) {
			changeSetContext.register(new ChangeSetMemberDelegate(txMgr));
			try {
				txMgr.begin();
			} catch (Exception e) { // NOSONAR
				throw new ErrorStatusException(CdsErrorStatuses.TRANSACTION_INITIALIZATION_FAILED, e);
			}
		}

		try {
			updateSessionContext(context);
		} catch (CdsException e) {
			logger.warn("Not supported to set the locale on the connection session", e);
		}
	}

	private boolean requiresTransaction(EventContext context) {
		// TODO media streams are currently not yet detected, but also require transactions
		if (context.getEvent().equals(CqnService.EVENT_READ)) {
			CqnSelect cqn = context.as(CdsReadEventContext.class).getCqn();
			if (cqn != null && !cqn.getLock().isPresent()) {
				// only READ events without locks don't require transactions
				return false;
			}
		}
		return true;
	}

	@On
	@HandlerOrder(OrderConstants.On.DEFAULT_ON)
	protected Result defaultRead(CdsReadEventContext context) {
		return checkExceptionAndOtelSpan(() -> getCdsDataStore().execute(context.getCqn(), context.getCqnNamedValues()), "SELECT", context);
	}

	@On
	@HandlerOrder(OrderConstants.On.DEFAULT_ON)
	protected Result defaultCreate(CdsCreateEventContext context) {
		return checkExceptionAndOtelSpan(() -> getCdsDataStore().execute(context.getCqn()), "INSERT", context);
	}

	@On
	@HandlerOrder(OrderConstants.On.DEFAULT_ON)
	protected Result defaultUpsert(CdsUpsertEventContext context) {
		return checkExceptionAndOtelSpan(() -> getCdsDataStore().execute(context.getCqn()), "UPSERT", context);
	}

	@On
	@HandlerOrder(OrderConstants.On.DEFAULT_ON)
	protected Result defaultUpdate(CdsUpdateEventContext context) {
		return checkExceptionAndOtelSpan(() -> getCdsDataStore().execute(context.getCqn(), context.getCqnValueSets()), "UPDATE", context);
	}

	@On
	@HandlerOrder(OrderConstants.On.DEFAULT_ON)
	protected Result defaultDelete(CdsDeleteEventContext context) {
		return checkExceptionAndOtelSpan(() -> getCdsDataStore().execute(context.getCqn(), context.getCqnValueSets()), "DELETE", context);
	}

	public CdsDataStore getCdsDataStore() {
		if(!ChangeSetContext.isActive()) {
			return cachedConnectors.findOrCreate().connect();
		}

		ChangeSetContext context = ChangeSetContext.getCurrent();
		String currentTenant = RequestContext.getCurrent(runtime).getUserInfo().getTenant();

		CachedCdsDataStore cachedDataStore = cachedDataStores.get(context);
		if(cachedDataStore == null) {
			cachedDataStore = new CachedCdsDataStore(cachedConnectors.findOrCreate().connect(), currentTenant);
			context.register(new ChangeSetListener(){
				@Override
				public void afterClose(boolean completed) {
					cachedDataStores.remove(context);
				}
			});
			cachedDataStores.put(context, cachedDataStore);
		} else if(!Objects.equals(cachedDataStore.getTenant(), currentTenant)) {
			// cached data stores and their cached (by means of transactions) JDBC connections are tenant-dependant
			// we implement this check to ensure that we don't switch tenants on an existing transaction
			// otherwise this could lead to data leaking from one tenant to another
			throw new ErrorStatusException(CdsErrorStatuses.TRANSACTION_TENANT_MISMATCH, cachedDataStore.getTenant(), currentTenant);
		}

		return cachedDataStore.getCdsDataStore();
	}

	@VisibleForTesting
	Result checkExceptionAndOtelSpan(Supplier supplier, String cqnOperation, EventContext context) {
		CqnStatement cqnStatement = (CqnStatement) context.get("cqn");
		updateSessionContext(context);
		Optional span = OpenTelemetryUtils.createSpan(OpenTelemetryUtils.CdsSpanType.CQN);
		try (Scope scope = span.map(Span::makeCurrent).orElse(null)) {
			OpenTelemetryUtils.updateSpan(span, runtime, cqnOperation, context.getTarget(), cqnStatement, "sql");

			return supplier.get();
		} catch (UniqueConstraintException e) {
			throw new ErrorStatusException(CdsErrorStatuses.UNIQUE_CONSTRAINT_VIOLATED, e.getEntityName(), e);
		} catch (NotNullConstraintException e) {
			throw new ErrorStatusException(CdsErrorStatuses.VALUE_REQUIRED,
					StringUtils.stringifyList(e.getElementNames()), e.getEntityName(), e);
		} catch (CdsLockTimeoutException e) {
			throw new ErrorStatusException(CdsErrorStatuses.LOCK_TIMEOUT, e.getDefinition().getQualifiedName(), e);
		} catch (CqnValidationException | CdsDataException e) {
			throw new ErrorStatusException(CdsErrorStatuses.INVALID_CQN, e.getMessage(), e); // TODO CDS4J support required
		}
		finally {
			OpenTelemetryUtils.endSpan(span);
		}
	}

	private void updateSessionContext(EventContext context) {
		getCdsDataStore().setSessionContext(SessionContextUtils.toSessionContext(context));
	}

	@Override
	protected String getTargetEntity(CqnStatement statement) {
		CdsModel model = RequestContext.getCurrent(runtime).getModel();
		return CdsModelUtils.getEntityPath(
				CqnUtils.getTargetRef(statement, true), model).target().type().getQualifiedName();
	}

	private static class CachedCdsDataStore {

		private final CdsDataStore cdsDataStore;
		private final String tenant;

		public CachedCdsDataStore(CdsDataStore cdsDataStore, String tenant) {
			this.cdsDataStore = cdsDataStore;
			this.tenant = tenant;
		}

		public CdsDataStore getCdsDataStore() {
			return cdsDataStore;
		}

		public String getTenant() {
			return tenant;
		}

	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy