Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.sap.cds.mtx.impl.MetaDataAccessorImpl Maven / Gradle / Ivy
/*
* ----------------------------------------------------------------
* © 2019-2021 SAP SE or an SAP affiliate company. All rights reserved.
* ----------------------------------------------------------------
*
*/
package com.sap.cds.mtx.impl;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import org.checkerframework.checker.index.qual.NonNegative;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.Ticker;
import com.github.benmanes.caffeine.cache.Weigher;
import com.sap.cds.CdsException;
import com.sap.cds.mtx.MetaDataAccessor;
import com.sap.cds.mtx.ModelId;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.reflect.impl.CdsModelReader;
/**
* Class that provides access to CDS and Edmx models and caches them
*
* @param Type used for the Edmx model
*/
public class MetaDataAccessorImpl implements MetaDataAccessor {
private static final String BASEMODEL_ETAG = "\"basemodel\"";
private static final long NANOS_TO_SECONDS = 1000 * 1000 * 1000L;
private static final Logger logger = LoggerFactory.getLogger(MetaDataAccessorImpl.class);
private final Cache modelIdToCdsModel;
private final Cache modelIdToEdmxModel;
private final Cache modelIdToI18n;
@FunctionalInterface
public interface EdmxModelCreator {
M parse(String edmx, String serviceName);
default M getBaseModel(String serviceName) { return null; }
}
@FunctionalInterface
public interface CdsModelCreator {
CdsModel parse(String csn);
default CdsModel getBaseModel() { return null; }
}
@FunctionalInterface
public interface I18nResourceCreator {
I18n parse(String json, String language);
default I18n getBaseModel(String language) { return null; }
}
public static class MetaDataAccessorConfig {
private SidecarAccess sidecarAccess;
private CacheParams cacheParams;
private EdmxModelCreator> strToEdmx;
private CdsModelCreator strToModel;
private I18nResourceCreator strToI18n;
private MetaDataAccessorConfig() {
}
public SidecarAccess getSidecarAccess() {
return sidecarAccess;
}
public CacheParams getCacheParams() {
return cacheParams;
}
public EdmxModelCreator> getStrToEdmx() {
return strToEdmx;
}
public CdsModelCreator getStrToModel() {
return strToModel;
}
public I18nResourceCreator getStrToI18n() {
return strToI18n;
}
public static class Builder {
private SidecarAccess sidecarAccess;
private CacheParams cacheParams;
private EdmxModelCreator> strToEdmx;
private CdsModelCreator strToModel;
private I18nResourceCreator strToI18n;
public Builder sidecarAccess(SidecarAccess sidecarAccess) {
this.sidecarAccess = sidecarAccess;
return this;
}
public Builder cacheParams(CacheParams cacheParams) {
this.cacheParams = cacheParams;
return this;
}
public Builder strToEdmx(EdmxModelCreator> strToEdmx) {
this.strToEdmx = strToEdmx;
return this;
}
public Builder strToModel(CdsModelCreator strToModel) {
this.strToModel = strToModel;
return this;
}
public Builder strToI18n(I18nResourceCreator strToI18n) {
this.strToI18n = strToI18n;
return this;
}
public MetaDataAccessorConfig build() {
MetaDataAccessorConfig config = new MetaDataAccessorConfig();
config.sidecarAccess = sidecarAccess;
config.cacheParams = cacheParams;
config.strToEdmx = strToEdmx;
config.strToModel = strToModel;
config.strToI18n = strToI18n;
return config;
}
}
}
/**
* @param config MetaDataAccessor configurations
* @param cacheTicker Optional ticker used by guava cache for testing
* purposes, use null for productive use
*/
@SuppressWarnings("unchecked")
public MetaDataAccessorImpl(MetaDataAccessorConfig config, Ticker cacheTicker) {
if (cacheTicker == null) {
cacheTicker = Ticker.systemTicker();
}
ExecutorService executorService = Executors.newSingleThreadExecutor();
SidecarAccess sidecarAccess = config.getSidecarAccess();
CacheParams cacheParams = config.getCacheParams();
EdmxModelCreator strToEdmx = (EdmxModelCreator) config.getStrToEdmx();
CdsModelCreator strToModel = config.getStrToModel();
I18nResourceCreator strToI18n = config.getStrToI18n();
if (strToModel == null) {
modelIdToCdsModel = null;
} else {
modelIdToCdsModel = new CdsCache(sidecarAccess, strToModel, cacheParams, cacheTicker, executorService);
}
if (strToEdmx == null) {
modelIdToEdmxModel = null;
} else {
modelIdToEdmxModel = new EdmxCache<>(sidecarAccess, strToEdmx, cacheParams, cacheTicker, executorService);
}
if (strToI18n == null) {
modelIdToI18n = null;
} else {
modelIdToI18n = new I18nCache(sidecarAccess, strToI18n, cacheParams, cacheTicker, executorService);
}
}
/**
* @param sidecarAccess Object of type {@link SidecarAccess}that provides access
* to the node.js application sidecar/mtx via a rest API
* @param cacheParams Parameters that control size and lifecycle of cache for
* cds and edmx models
* @param strToEdmx Function that converts an edmx model description given
* as string into an edmx model
* @param cacheTicker Optional ticker used by guava cache for testing
* purposes, use null for productive use
*/
public MetaDataAccessorImpl(SidecarAccess sidecarAccess, CacheParams cacheParams, EdmxModelCreator strToEdmx,
Ticker cacheTicker) {
this(new MetaDataAccessorConfig.Builder().sidecarAccess(sidecarAccess).cacheParams(cacheParams)
.strToEdmx(strToEdmx).strToModel((csn) -> {
return CdsModelReader.read(new CdsModelReader.Config.Builder().setIncludeUIAnnotations(true).build(), csn, true);
}).build(), cacheTicker);
}
@Override
public CdsModel getCdsModel(ModelId key, int maxAgeSeconds) {
if (modelIdToCdsModel == null) {
throw new CdsException("Cache not configured");
}
return modelIdToCdsModel.getOrLoadIfStale(key, maxAgeSeconds);
}
@Override
public M getEdmx(ModelId key, int maxAgeSeconds) throws CdsException {
if (modelIdToEdmxModel == null) {
throw new CdsException("Cache not configured");
}
return modelIdToEdmxModel.getOrLoadIfStale(key, maxAgeSeconds);
}
@Override
public I18n getI18n(ModelId modelId, int maxAgeSeconds) {
if (modelIdToI18n == null) {
throw new CdsException("Cache not configured");
}
return modelIdToI18n.getOrLoadIfStale(modelId, maxAgeSeconds);
}
@Override
public void evict(String tenantId) {
if (modelIdToCdsModel != null) {
modelIdToCdsModel.evict(tenantId);
}
if (modelIdToEdmxModel != null) {
modelIdToEdmxModel.evict(tenantId);
}
if (modelIdToI18n != null) {
modelIdToI18n.evict(tenantId);
}
// inform listeners
}
@Override
public void refresh(String tenantId, int maxAgeSeconds) {
if (modelIdToCdsModel != null) {
modelIdToCdsModel.refresh(tenantId, maxAgeSeconds);
}
if (modelIdToEdmxModel != null) {
modelIdToEdmxModel.refresh(tenantId, maxAgeSeconds);
}
if (modelIdToI18n != null) {
modelIdToI18n.refresh(tenantId, maxAgeSeconds);
}
// inform listeners
}
private static abstract class Cache {
private final Ticker ticker;
private final LoadingCache> cache;
private final String cacheName = getClass().getSimpleName();
private final boolean isWithBaseModelEtag;
protected Cache(CacheParams params, Ticker ticker, ExecutorService executorService) {
this.ticker = ticker;
this.isWithBaseModelEtag = params.isWithBaseModelETag();
this.cache = Caffeine.newBuilder().maximumWeight(params.getMaximumSize())
.weigher(new Weigher>() {
@Override
public @NonNegative int weigh(ModelId key, Entry value) {
if (isWithBaseModelEtag && BASEMODEL_ETAG.equals(value.getETag())) {
return 0;
}
return 1;
}
})
.expireAfterAccess(params.getExpirationDuration(), params.getExpirationDurationUnit())
.refreshAfterWrite(params.getRefreshDuration(), params.getRefreshDurationUnit())
.executor(executorService)
.ticker(ticker)
.evictionListener((k, v, c) -> {
if (c.wasEvicted()) {
logger.debug("Evicted '{}' in cache '{}' with cause '{}'", k, cacheName, c);
}
})
.build(new CacheLoader>() {
@Override
public Entry load(ModelId key) {
return Cache.this.load(key, null);
}
@Override
public Entry reload(ModelId key, Entry oldValue) {
logger.debug("Reloading '{}' in cache '{}'", key, cacheName);
try {
return Cache.this.load(key, oldValue);
} catch (Exception e) {// NOSONAR
logger.error("Reloading '{}' failed", key, e);
return oldValue;
}
}
});
}
public void evict(String tenantId) {
logger.debug("Evicting tenant '{}' from cache '{}'", tenantId, cacheName);
forTenant(tenantId, cache::invalidate);
}
public void refresh(String tenantId, int maxAgeSeconds) {
logger.debug("Refreshing tenant '{}' in cache '{}'", tenantId, cacheName);
forTenant(tenantId, k -> getOrLoadIfStale(k, maxAgeSeconds));
}
private void forTenant(String tenantId, Consumer action) {
cache.asMap().keySet().stream().filter(k -> Objects.equals(tenantId, k.getTenantId())).forEach(action);
}
public V getOrLoadIfStale(ModelId key, int maxAgeSeconds) {
long maxAgeNanos = maxAgeSeconds * NANOS_TO_SECONDS;
Entry entry;
try {
entry = cache.get(key);
} catch (RuntimeException e) {
throw new CdsException(e);
}
if ((ticker.read() - entry.refreshed()) > maxAgeNanos) {
// sync load
Entry loaded = load(key, entry);
if (loaded != entry) {
cache.put(key, loaded);
entry = loaded;
}
} else {
logger.debug("'{}' in cache '{}' is not older than '{}'", key, cacheName, maxAgeSeconds);
}
return entry.getEntry();
}
private Entry load(ModelId key, Entry oldEntry) {
logger.debug("Loading '{}' in cache '{}'", key, cacheName);
String eTag = oldEntry != null ? oldEntry.getETag() : isWithBaseModelEtag ? BASEMODEL_ETAG : null;
long beforeAccess = ticker.read();
SidecarResponse model = access(key, eTag);
if (oldEntry != null && model.isNotModified()) {
oldEntry.refresh(beforeAccess);
logger.debug("Refreshed unchanged '{}' in cache '{}'", key, cacheName);
return oldEntry;
}
// model has changed -> notify
V value = BASEMODEL_ETAG.equals(eTag) && model.isNotModified() ? getBaseModel(key) : parse(key, model.getPayload());
return new Entry<>(value, model.getETag(), beforeAccess);
}
abstract SidecarResponse access(ModelId key, String eTag);
abstract V parse(ModelId key, String model);
abstract V getBaseModel(ModelId key);
}
private static class Entry {
private final V entry;
private final String eTag;
private final AtomicLong refreshed;
public Entry(V entry, String eTag, long refreshed) {
this.entry = entry;
this.eTag = eTag != null ? eTag.trim() : null;
this.refreshed = new AtomicLong(refreshed);
}
public V getEntry() {
return entry;
}
public String getETag() {
return eTag;
}
public void refresh(long refreshed) {
this.refreshed.set(refreshed);
}
public long refreshed() {
return refreshed.get();
}
}
private static class EdmxCache extends Cache {
private final SidecarAccess sidecarAccess;
private final EdmxModelCreator strToEdmx;
public EdmxCache(SidecarAccess sidecarAccess, EdmxModelCreator strToEdmx, CacheParams params, Ticker ticker, ExecutorService executorService) {
super(params, ticker, executorService);
this.sidecarAccess = sidecarAccess;
this.strToEdmx = strToEdmx;
}
@Override
SidecarResponse access(ModelId key, String eTag) {
return sidecarAccess.getEdmx(key, eTag);
}
@Override
M parse(ModelId key, String model) {
return strToEdmx.parse(model, key.getServiceName().orElse(null));
}
@Override
M getBaseModel(ModelId key) {
return strToEdmx.getBaseModel(key.getServiceName().orElse(null));
}
}
private static class CdsCache extends Cache {
private final SidecarAccess sidecarAccess;
private final CdsModelCreator strToModel;
public CdsCache(SidecarAccess sidecarAccess, CdsModelCreator strToModel, CacheParams params,
Ticker ticker, ExecutorService executorService) {
super(params, ticker, executorService);
this.sidecarAccess = sidecarAccess;
this.strToModel = strToModel;
}
@Override
SidecarResponse access(ModelId key, String eTag) {
return sidecarAccess.getCsn(key, eTag);
}
@Override
CdsModel parse(ModelId key, String csn) {
return strToModel.parse(csn);
}
@Override
CdsModel getBaseModel(ModelId key) {
return strToModel.getBaseModel();
}
}
private static class I18nCache extends Cache {
private final SidecarAccess sidecarAccess;
private final I18nResourceCreator strToI18n;
public I18nCache(SidecarAccess sidecarAccess, I18nResourceCreator strToI18n, CacheParams params,
Ticker ticker, ExecutorService executorService) {
super(params, ticker, executorService);
this.sidecarAccess = sidecarAccess;
this.strToI18n = strToI18n;
}
@Override
SidecarResponse access(ModelId key, String eTag) {
return sidecarAccess.getI18n(key, eTag);
}
@Override
I18n parse(ModelId key, String json) {
return strToI18n.parse(json, key.getLanguage().orElse(""));
}
@Override
I18n getBaseModel(ModelId key) {
return strToI18n.getBaseModel(key.getLanguage().orElse(""));
}
}
}