diff --git a/pom.xml b/pom.xml
index a3e0fc8f..25f4ab5f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,7 +10,7 @@
org.entando.entando.plugins
entando-plugin-jacms
war
- 6.5.3
+ 6.5.4
Entando Plugin: CMS
Allows registered users to manage dynamic contents and digital assets
http://www.entando.com/
@@ -86,6 +86,28 @@
org.apache.maven.plugins
maven-surefire-plugin
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.3.0
+
+
+ test-jar
+ package
+
+ test-jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 8
+ 8
+
+
@@ -180,6 +202,11 @@
+
+ com.github.ben-manes.caffeine
+ caffeine
+ 3.1.8
+
org.apache.commons
commons-collections4
@@ -267,6 +294,17 @@
org.mockito
mockito-junit-jupiter
+ test
+
+
+ org.mockito
+ mockito-core
+
+
+
+
+ org.mockito
+ mockito-inline
org.apache.struts
diff --git a/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/ContentManager.java b/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/ContentManager.java
index 6ffa10ef..8bd155d2 100644
--- a/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/ContentManager.java
+++ b/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/ContentManager.java
@@ -14,6 +14,7 @@
package com.agiletec.plugins.jacms.aps.system.services.content;
import com.agiletec.aps.system.ApsSystemUtils;
+import com.agiletec.aps.system.ApsSystemUtils.ApsDeepDebug;
import com.agiletec.aps.system.SystemConstants;
import com.agiletec.aps.system.common.entity.ApsEntityManager;
import com.agiletec.aps.system.common.entity.IEntityDAO;
@@ -33,6 +34,7 @@
import com.agiletec.plugins.jacms.aps.system.services.content.model.ContentRecordVO;
import com.agiletec.plugins.jacms.aps.system.services.content.model.SmallContentType;
import com.agiletec.plugins.jacms.aps.system.services.resource.ResourceUtilizer;
+import com.github.benmanes.caffeine.cache.Cache;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -54,7 +56,7 @@
* the contents.
*/
public class ContentManager extends ApsEntityManager
- implements IContentManager, GroupUtilizer, PageUtilizer, ContentUtilizer, ResourceUtilizer, CategoryUtilizer {
+ implements IFContentLocalCache, IContentManager, GroupUtilizer, PageUtilizer, ContentUtilizer, ResourceUtilizer, CategoryUtilizer {
private static final EntLogger logger = EntLogFactory.getSanitizedLogger(ContentManager.class);
@@ -72,6 +74,8 @@ public class ContentManager extends ApsEntityManager
private ICacheInfoManager cacheInfoManager;
+ private transient com.github.benmanes.caffeine.cache.Cache localCache;
+
@Override
protected String getConfigItemName() {
return JacmsSystemConstants.CONFIG_ITEM_CONTENT_TYPES;
@@ -91,8 +95,8 @@ public Content createContentType(String typeCode) {
}
/**
- * Return a list of the of the content types in a 'small form'. 'Small form'
- * mans that the contents returned are purged from all unnecessary
+ * Return a list of the content types in a 'small form'. 'Small form'
+ * means that the contents returned are purged from all unnecessary
* information (eg. attributes).
*
* @return The list of the types in a (small form).
@@ -187,6 +191,17 @@ public Content loadContent(String id, boolean onLine) throws EntException {
}
}
+ @Override
+ public Content loadAndCacheContent(String id, boolean onLine) throws EntException {
+ try {
+ ContentRecordVO contentVo = this.loadAndCacheContentVO(id);
+ return this.createContent(contentVo, onLine);
+ } catch (EntException e) {
+ logger.error("Error while loading content : id {}", id, e);
+ throw new EntException("Error while loading content : id " + id, e);
+ }
+ }
+
protected Content createContent(ContentRecordVO contentVo, boolean onLine) throws EntException {
Content content = null;
try {
@@ -238,7 +253,7 @@ protected Content createContent(ContentRecordVO contentVo, boolean onLine) throw
/**
* Return a {@link ContentRecordVO} (shortly: VO) containing the all content
- * informations stored in the DB.
+ * information stored in the DB.
*
* @param id The id of the requested content.
* @return The VO object corresponding to the wanted content.
@@ -247,6 +262,7 @@ protected Content createContent(ContentRecordVO contentVo, boolean onLine) throw
@Override
public ContentRecordVO loadContentVO(String id) throws EntException {
try {
+ ApsDeepDebug.print("cms-local-cache", "cache IGNORE " + id);
return (ContentRecordVO) this.getContentDAO().loadEntityRecord(id);
} catch (Throwable t) {
logger.error("Error while loading content vo : id {}", id, t);
@@ -254,6 +270,27 @@ public ContentRecordVO loadContentVO(String id) throws EntException {
}
}
+ @Override
+ public ContentRecordVO loadAndCacheContentVO(String id) throws EntException {
+ try {
+ return IFContentLocalCache.loadAndCacheContentVO(id, localCache,
+ () -> (ContentRecordVO) this.getContentDAO().loadEntityRecord(id));
+ } catch (Exception t) {
+ logger.error("Error while loading content vo : id {}", id, t);
+ throw new EntException("Error while loading content vo : id " + id, t);
+ }
+ }
+
+ @Override
+ public void evict(String key) {
+ IFContentLocalCache.evict(key, localCache);
+ }
+
+ @Override
+ public void evict(List keys) {
+ IFContentLocalCache.evict(keys, localCache);
+ }
+
/**
* Save a content in the DB.
*
@@ -271,7 +308,7 @@ public void saveContentAndContinue(Content content) throws EntException {
}
/**
- * Save a content in the DB. Hopefully this method has no annotation
+ * Save a content in the DB. Hopefully, this method has no annotation
* attached
*/
@Override
@@ -280,6 +317,7 @@ public void addContent(Content content) throws EntException {
}
private void addUpdateContent(Content content, boolean updateDate) throws EntException {
+ IFContentLocalCache.evict(content, localCache);
try {
content.setLastModified(new Date());
if (updateDate) {
@@ -751,4 +789,11 @@ public void setCacheInfoManager(ICacheInfoManager cacheInfoManager) {
this.cacheInfoManager = cacheInfoManager;
}
+ public Cache getLocalCache() {
+ return localCache;
+ }
+
+ public void setLocalCache(Cache localCache) {
+ this.localCache = localCache;
+ }
}
diff --git a/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/IContentManager.java b/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/IContentManager.java
index 36ea6646..d45e82f2 100644
--- a/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/IContentManager.java
+++ b/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/IContentManager.java
@@ -101,6 +101,8 @@ public interface IContentManager extends IEntityManager {
*/
public Content loadContent(String id, boolean onLine) throws EntException;
+ Content loadAndCacheContent(String id, boolean onLine) throws EntException;
+
/**
* Restituisce un VO contenente le informazioni del record su db
* corrispondente al contenuto di cui all'id inserito.
@@ -111,6 +113,12 @@ public interface IContentManager extends IEntityManager {
*/
public ContentRecordVO loadContentVO(String id) throws EntException;
+ ContentRecordVO loadAndCacheContentVO(String id) throws EntException;
+
+ void evict(String key);
+
+ void evict(List keys);
+
/**
* Salva un contenuto sul DB. Il metodo viene utilizzato sia nel caso di
* salvataggio di un nuovo contenuto (in tal caso l'id del contenuto nuovo
diff --git a/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/IFContentLocalCache.java b/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/IFContentLocalCache.java
new file mode 100644
index 00000000..b4bc7ff9
--- /dev/null
+++ b/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/IFContentLocalCache.java
@@ -0,0 +1,194 @@
+package com.agiletec.plugins.jacms.aps.system.services.content;
+
+import com.agiletec.aps.system.ApsSystemUtils.ApsDeepDebug;
+import com.agiletec.aps.system.common.entity.model.attribute.AbstractListAttribute;
+import com.agiletec.aps.system.common.entity.model.attribute.CompositeAttribute;
+import com.agiletec.aps.system.common.entity.model.attribute.ListAttribute;
+import com.agiletec.aps.system.common.entity.model.attribute.MonoListAttribute;
+import com.agiletec.plugins.jacms.aps.system.services.content.model.Content;
+import com.agiletec.plugins.jacms.aps.system.services.content.model.ContentRecordVO;
+import com.agiletec.plugins.jacms.aps.system.services.content.model.SymbolicLink;
+import com.agiletec.plugins.jacms.aps.system.services.content.model.attribute.LinkAttribute;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+import org.apache.commons.lang.StringUtils;
+import org.entando.entando.aps.system.services.IFeatureFlag;
+
+public interface IFContentLocalCache extends IFeatureFlag {
+ boolean LOCAL_CMS_CACHE_ENABLED = checkEnabled(); // useful for test
+
+ String CMS_LOCAL_CACHE = "cms-local-cache";
+
+ default boolean isEnabled() {
+ return LOCAL_CMS_CACHE_ENABLED;
+ }
+
+ static boolean checkEnabled() {
+ return IFeatureFlag.readEnablementStatus("LOCAL_CMS_CACHE");
+ }
+
+ /**
+ *
+ * @param id the content id used as a key in the cache
+ * @param localCache the current Cache object
+ * @param action the action to perform in case of cache miss
+ * @return the contentRecordVO found in cache or loaded from the database
+ */
+ static ContentRecordVO loadAndCacheContentVO(final String id,
+ final Cache localCache,
+ final Supplier action) {
+
+ if (action == null)
+ return null;
+ if (checkEnabled()
+ && localCache != null) {
+ if (ApsDeepDebug.isTagEnabled(CMS_LOCAL_CACHE)) {
+ if (localCache.asMap().containsKey(id)) {
+ ApsDeepDebug.print(CMS_LOCAL_CACHE,"cache HIT " + id);
+ } else {
+ ApsDeepDebug.print(CMS_LOCAL_CACHE,"cache miss " + id);
+ }
+ }
+ return (ContentRecordVO) localCache.get(id, key -> action.get());
+ } else {
+ return action.get();
+ }
+ }
+
+ /**
+ * Evict a single key from the cache given the content object
+ * @param content the content object associated with the key to remove
+ * @param localCache the cache instance
+ */
+ static void evict(final Content content, final Cache localCache) {
+ if (content != null) {
+ evict(content.getId(), localCache);
+ }
+ }
+
+ /**
+ * Evict a single key from the cache
+ * @param key the key associated with the object to remove
+ * @param localCache the cache instance
+ */
+ static void evict(final String key, final Cache localCache) {
+ if (checkEnabled()
+ && localCache != null
+ && StringUtils.isNotBlank(key)) {
+ ApsDeepDebug.print(CMS_LOCAL_CACHE, "Evicting key from cache: " + key);
+ localCache.invalidate(key);
+ }
+ }
+
+ /**
+ * Evict multiple keys from the cache
+ * @param keys the list of keys to remove
+ * @param localCache the cache instance
+ */
+ static void evict(final List keys, final Cache localCache) {
+ if (checkEnabled()
+ && localCache != null
+ && keys != null) {
+ ApsDeepDebug.print(CMS_LOCAL_CACHE, "Evicting keys from cache: " + keys);
+ localCache.invalidateAll(keys);
+ }
+ }
+
+ /**
+ * Flush the cache references for a given content. This gets called typically from content actions
+ * @param content the content in session
+ * @param cm the content manager instance
+ */
+ static void flushReferences(final Content content,IContentManager cm) {
+ try {
+ if (checkEnabled()
+ && cm != null) {
+ List refs = getContentReferences(content);
+ cm.evict(refs);
+ }
+ } catch (Exception e) {
+ ApsDeepDebug.print(CMS_LOCAL_CACHE, "Error cleaning cache when flushing references from action");
+ }
+ }
+
+ /**
+ * Get the contents referenced by analyzing the attributes list
+ * @param content the content in session
+ * @return the list of the references
+ */
+ static List getContentReferences(final Content content) {
+ final List references = new ArrayList<>();
+
+ if (content != null) {
+ content.getAttributeList()
+ .forEach(a -> {
+ if (a instanceof LinkAttribute) {
+ processLinkAttribute((LinkAttribute) a, references);
+ }
+ if (a instanceof CompositeAttribute) {
+ processCompositeAttribute((CompositeAttribute) a, references);
+ }
+ if (a instanceof ListAttribute) {
+ processListAttribute((ListAttribute) a, references);
+ }
+ if (a instanceof MonoListAttribute) {
+ processListAttribute((MonoListAttribute) a, references);
+ }
+ });
+ }
+ return references;
+ }
+
+ static void processListAttribute(final AbstractListAttribute attr, final List references) {
+ if (attr != null) {
+ attr.getAttributes().forEach(ca -> {
+ if (ca instanceof LinkAttribute) {
+ processLinkAttribute((LinkAttribute) ca, references);
+ }
+ if (ca instanceof CompositeAttribute) {
+ processCompositeAttribute((CompositeAttribute) ca, references);
+ }
+ });
+ }
+ }
+
+ static void processCompositeAttribute(final CompositeAttribute attr, final List references) {
+ if (attr != null) {
+ attr.getAttributes().forEach(ca -> {
+ if (ca instanceof LinkAttribute) {
+ processLinkAttribute((LinkAttribute) ca, references);
+ }
+ });
+ }
+ }
+
+ static void processLinkAttribute(final LinkAttribute attr, final List reference) {
+ if (attr != null && attr.getValue() instanceof SymbolicLink) {
+ final SymbolicLink l = (SymbolicLink) attr.getValue();
+
+ if (l.getDestType() == SymbolicLink.CONTENT_TYPE) {
+ reference.add(l.getContentDest());
+ }
+ }
+ }
+
+ static Cache instantiateLocalCache(long maxSize, long expireMinutes, boolean stats) {
+ if (checkEnabled()) {
+ Caffeine