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

com.sap.cloud.spring.boot.mt.cds.MultiTenantSingleModelConfig Maven / Gradle / Ivy

The newest version!
/******************************************************************************
 * © 2020 SAP SE or an SAP affiliate company. All rights reserved.            *
 ******************************************************************************/

package com.sap.cloud.spring.boot.mt.cds;

import com.sap.cds.CdsDataStoreConnector;
import com.sap.cds.mt.CdsDataStoreLookup;
import com.sap.cds.mt.TenantAwareCdsDataStoreConnector;
import com.sap.cds.mtx.CdsDataStoreConnectorCreator;
import com.sap.cds.mtx.MetaDataAccessor;
import com.sap.cds.mtx.impl.CacheParams;
import com.sap.cds.mtx.impl.CdsDataStoreConnectorCreatorImpl;
import com.sap.cds.mtx.impl.MetaDataAccessorSingleModelImpl;
import com.sap.cds.mtx.impl.ModelOutDatedChecker;
import com.sap.cloud.mt.runtime.TenantAwareDataSource;
import com.sap.cloud.mt.runtime.TenantProvider;
import com.sap.cloud.spring.boot.mt.lib.InvocationHandlerForTenant;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.Assert;

import javax.sql.DataSource;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

@Configuration("comSapMtMultiTenantSingleModelConfig")
@Conditional(MultiTenantSingleModelCondition.class)
@ConfigurationProperties(Const.CSN_MODEL_PREFIX)
public class MultiTenantSingleModelConfig {
    private static final String CSN_JSON = "csn.json";
    private String path = CSN_JSON;

    @Bean("comSapMtMetaDataAccessor")
    MetaDataAccessor metaDataAccessor(EdmxConfig edmxConfig,
                                              EdmxModelCreator edmxModelCreator) throws IOException {
        Assert.notNull(path, "No path to CSN model specified");
        Assert.hasLength(path, "No path to CSN model specified");
        return new MetaDataAccessorSingleModelImpl(path, edmxConfig.getFolder(),
                edmxModelCreator.stringToModelCreator());
    }

    @Bean("comSapMtCdsDataStoreConnector")
    public CdsDataStoreConnector cdsDataStoreConnector(MetaDataAccessor metaDataAccessor,
                                                       TenantAwareDataSource tenantAwareDataSource,
                                                       CdsTransactionManager cdsTransactionManager,
                                                       TenantProvider tenantProvider,
                                                       CacheConfig cacheConfig) {
        CdsDataStoreConnectorCreator cdsDataStoreConnectorCreator = new CdsDataStoreConnectorCreatorImpl(
                metaDataAccessor,
                // a function that returns for a tenant the corresponding data source.
                tenantId -> {
                    return getDataSourceProxy(tenantAwareDataSource, tenantId);
                },
                cdsTransactionManager);
        ModelOutDatedChecker outDatedChecker = new ModelOutDatedChecker(metaDataAccessor);
        CacheParams cacheParams = new CacheParams(cacheConfig.getMaximumSize(),
                cacheConfig.getExpirationDuration(), cacheConfig.getExpirationDurationUnit(),
                //never refresh, the model never changes for a running program
                10000000, TimeUnit.DAYS);
        CdsDataStoreLookup cdsDataStoreLookup = new CdsDataStoreLookup(cdsDataStoreConnectorCreator,
                outDatedChecker,
                cacheParams,
                null);
        return new TenantAwareCdsDataStoreConnector(cdsDataStoreLookup, tenantProvider);
    }

    private DataSource getDataSourceProxy(TenantAwareDataSource tenantAwareDataSource, String tenantId) {
        // The connection invocation handler works on the primary tenant aware data source and not on any proxy. Spring creates a connection
        // on the primary data source when a transaction is started with @Transactional. The connection invocation handler accesses this connection
        // by using Spring's DataSourceUtils class. This is the reason a connection proxy is needed. It must be avoided that a connection is closed
        // physically when connection.close() is called, because this is done by Spring when the transaction is ended. If the connection is then already
        // closed, Spring Boot throws an exception. Class DataSourceUtils avoids this by considering Spring's transaction management.
        // The connection invocation handler calls a method of DataSourceUtils to release a connection when method close is called.
        // It is important that Spring Boot and the connection handler work on the same data source instance, as it is the key for the transaction handling.
        // The connection invocation handler is wrapped into a InvocationHandlerForTenant that delegates each call wrapped in a tenant overwrite to the
        // connection invocation handler. In this way it is assured that each access to the tenant aware data source is done with the tenant "tenantId"
        // which is passed to the function that is specified here.
        // Otherwise the tenant aware data source would consult the tenant provider to get the tenant id. In most cases
        // this would yield the same id as provided via function parameter "tenantId", as it is determined normally via the same tenant provider.
        // But if a buffer refresh is added to the TenantAwareCdsDataStoreConnector in the future, it will lead to a call of this
        // function outside of a rest request scope.
        // The data source used by the CdsdataStoreConnectorCreator must be wrapped into a proxy, to achieve that the connection proxy described above
        // is returned, when method getConnection() is called. It is wrapped in an InvocationHandlerForTenant for the same reasons as explained above.
        // Note: This works only with the original TenantProvider with TenantOverwrite. If this is replaced by a provider
        // that doesn't integrate the TenantOverwrite, wrong tenants will be accessed.
        Supplier connectionInvocationHandlerSupplier = () -> new InvocationHandlerForTenant(tenantId, new ConnectionInvocationHandler(tenantAwareDataSource));
        InvocationHandler dataSourceInvocationHandler =
                new InvocationHandlerForTenant(tenantId, new DataSourceInvocationHandler(tenantAwareDataSource, connectionInvocationHandlerSupplier));
        return (DataSource) Proxy.newProxyInstance(MultiTenantSingleModelConfig.class.getClassLoader(), new Class[]{DataSource.class}, dataSourceInvocationHandler);
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }
}