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

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(""));
		}

	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy