From efcc15b4708ce73794c644d598e79d4c416bc10b Mon Sep 17 00:00:00 2001 From: ffalqui Date: Fri, 16 Jan 2026 17:59:41 +0100 Subject: [PATCH 1/5] fix LangManager bean injection in Content Scheduler plugin --- Dockerfile.tomcat | 2 +- admin-console/pom.xml | 2 +- cds-plugin/pom.xml | 2 +- cms-plugin/pom.xml | 2 +- contentscheduler-plugin/pom.xml | 2 +- .../aps/system/services/content/ContentJobs.java | 11 ++++++----- .../jpcontentscheduler/aps/contentThreadConfig.xml | 1 + engine/pom.xml | 2 +- keycloak-plugin/pom.xml | 2 +- mail-plugin/pom.xml | 2 +- pom.xml | 2 +- portal-ui/pom.xml | 2 +- redis-plugin/pom.xml | 2 +- seo-plugin/pom.xml | 2 +- solr-plugin/pom.xml | 2 +- versioning-plugin/pom.xml | 2 +- webapp/pom.xml | 2 +- 17 files changed, 22 insertions(+), 20 deletions(-) diff --git a/Dockerfile.tomcat b/Dockerfile.tomcat index 2a1629224f..fc3a0bb4f2 100644 --- a/Dockerfile.tomcat +++ b/Dockerfile.tomcat @@ -6,7 +6,7 @@ LABEL name="Entando App" \ maintainer="dev@entando.com" \ vendor="Entando Inc." \ version="${VERSION}" \ - release="7.3.0-fix.2" \ + release="7.3.0-fix.4" \ summary="Entando Application" \ description="This Entando app engine application provides APIs and composition for Entando applications" diff --git a/admin-console/pom.xml b/admin-console/pom.xml index 7100e9d169..8fcb5d453c 100644 --- a/admin-console/pom.xml +++ b/admin-console/pom.xml @@ -4,7 +4,7 @@ org.entando app-engine - 7.3.0-fix.2 + 7.3.0-fix.4 org.entando.entando entando-admin-console diff --git a/cds-plugin/pom.xml b/cds-plugin/pom.xml index 958673c107..9f726cca18 100644 --- a/cds-plugin/pom.xml +++ b/cds-plugin/pom.xml @@ -5,7 +5,7 @@ org.entando app-engine - 7.3.0-fix.2 + 7.3.0-fix.4 entando-plugin-jpcds org.entando.entando.plugins diff --git a/cms-plugin/pom.xml b/cms-plugin/pom.xml index 44af87182b..8dab3d708a 100644 --- a/cms-plugin/pom.xml +++ b/cms-plugin/pom.xml @@ -5,7 +5,7 @@ org.entando app-engine - 7.3.0-fix.2 + 7.3.0-fix.4 org.entando.entando.plugins entando-plugin-jacms diff --git a/contentscheduler-plugin/pom.xml b/contentscheduler-plugin/pom.xml index cc53170d13..c6c65a4615 100644 --- a/contentscheduler-plugin/pom.xml +++ b/contentscheduler-plugin/pom.xml @@ -4,7 +4,7 @@ org.entando app-engine - 7.3.0-fix.2 + 7.3.0-fix.4 entando-plugin-jpcontentscheduler org.entando.entando.plugins diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/ContentJobs.java b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/ContentJobs.java index 6bdf5080bf..017e90659c 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/ContentJobs.java +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/ContentJobs.java @@ -38,7 +38,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.scheduling.quartz.QuartzJobBean; @@ -70,12 +69,10 @@ public class ContentJobs extends QuartzJobBean implements ApplicationContextAwar private ICategoryManager _categoryManager; private IPageManager _pageManager; private IContentModelManager _contentModelManager; + private ILangManager _langManager; private ApplicationContext _ctx; - @Autowired - private ILangManager langManager; - @Override public void setApplicationContext(ApplicationContext ac) throws BeansException { this._ctx = ac; @@ -97,6 +94,7 @@ private void initBeans(ApplicationContext appCtx) { this.setContentModelManager((IContentModelManager) appCtx.getBean("jacmsContentModelManager")); this.setCategoryManager((ICategoryManager) appCtx.getBean("CategoryManager")); this.setPageManager((IPageManager) appCtx.getBean("PageManager")); + this.setLangManager((ILangManager) appCtx.getBean("LangManager")); } @Override @@ -217,7 +215,7 @@ public boolean scanEntity(Content currentContent) { for (int i = 0; i < attributes.size(); i++) { AttributeInterface entityAttribute = attributes.get(i); if (entityAttribute.isActive()) { - List errors = entityAttribute.validate(new AttributeTracer(), langManager); + List errors = entityAttribute.validate(new AttributeTracer(), this.getLangManager()); if (null != errors && errors.size() > 0) { return false; } @@ -767,4 +765,7 @@ public void setContentModelManager(IContentModelManager contentModelManager) { this._contentModelManager = contentModelManager; } + public ILangManager getLangManager() { return _langManager; } + + public void setLangManager(ILangManager langManager) { this._langManager = langManager; } } diff --git a/contentscheduler-plugin/src/main/resources/spring/plugins/jpcontentscheduler/aps/contentThreadConfig.xml b/contentscheduler-plugin/src/main/resources/spring/plugins/jpcontentscheduler/aps/contentThreadConfig.xml index 606a30be5b..a32ddc94bb 100644 --- a/contentscheduler-plugin/src/main/resources/spring/plugins/jpcontentscheduler/aps/contentThreadConfig.xml +++ b/contentscheduler-plugin/src/main/resources/spring/plugins/jpcontentscheduler/aps/contentThreadConfig.xml @@ -29,6 +29,7 @@ + diff --git a/engine/pom.xml b/engine/pom.xml index f8fffd4179..e132740725 100644 --- a/engine/pom.xml +++ b/engine/pom.xml @@ -4,7 +4,7 @@ org.entando app-engine - 7.3.0-fix.2 + 7.3.0-fix.4 org.entando.entando entando-engine diff --git a/keycloak-plugin/pom.xml b/keycloak-plugin/pom.xml index 53a50910c8..6372edfdd1 100644 --- a/keycloak-plugin/pom.xml +++ b/keycloak-plugin/pom.xml @@ -5,7 +5,7 @@ app-engine org.entando - 7.3.0-fix.2 + 7.3.0-fix.4 org.entando.entando entando-keycloak-auth diff --git a/mail-plugin/pom.xml b/mail-plugin/pom.xml index ffe5ff71d3..602995a48b 100644 --- a/mail-plugin/pom.xml +++ b/mail-plugin/pom.xml @@ -4,7 +4,7 @@ org.entando app-engine - 7.3.0-fix.2 + 7.3.0-fix.4 entando-plugin-jpmail org.entando.entando.plugins diff --git a/pom.xml b/pom.xml index 00264590f3..f7fdeb2dd9 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ org.entando app-engine - 7.3.0-fix.2 + 7.3.0-fix.4 pom diff --git a/portal-ui/pom.xml b/portal-ui/pom.xml index 0ccc9e937c..1e61498ffb 100644 --- a/portal-ui/pom.xml +++ b/portal-ui/pom.xml @@ -4,7 +4,7 @@ org.entando app-engine - 7.3.0-fix.2 + 7.3.0-fix.4 org.entando.entando entando-portal-ui diff --git a/redis-plugin/pom.xml b/redis-plugin/pom.xml index 361b7afd40..64a9a66f76 100644 --- a/redis-plugin/pom.xml +++ b/redis-plugin/pom.xml @@ -5,7 +5,7 @@ org.entando app-engine - 7.3.0-fix.2 + 7.3.0-fix.4 entando-plugin-jpredis org.entando.entando.plugins diff --git a/seo-plugin/pom.xml b/seo-plugin/pom.xml index 5d5bf6029d..ca2fe65474 100644 --- a/seo-plugin/pom.xml +++ b/seo-plugin/pom.xml @@ -4,7 +4,7 @@ org.entando app-engine - 7.3.0-fix.2 + 7.3.0-fix.4 entando-plugin-jpseo org.entando.entando.plugins diff --git a/solr-plugin/pom.xml b/solr-plugin/pom.xml index 601ad5cf27..401cca4fd3 100644 --- a/solr-plugin/pom.xml +++ b/solr-plugin/pom.xml @@ -5,7 +5,7 @@ org.entando app-engine - 7.3.0-fix.2 + 7.3.0-fix.4 entando-plugin-jpsolr org.entando.entando.plugins diff --git a/versioning-plugin/pom.xml b/versioning-plugin/pom.xml index d8f1ba15d6..68188282c3 100644 --- a/versioning-plugin/pom.xml +++ b/versioning-plugin/pom.xml @@ -4,7 +4,7 @@ org.entando app-engine - 7.3.0-fix.2 + 7.3.0-fix.4 entando-plugin-jpversioning org.entando.entando.plugins diff --git a/webapp/pom.xml b/webapp/pom.xml index 1c53727757..a80615bd2a 100644 --- a/webapp/pom.xml +++ b/webapp/pom.xml @@ -5,7 +5,7 @@ org.entando app-engine - 7.3.0-fix.2 + 7.3.0-fix.4 org.entando.entando webapp From f6bebee27e2ff68089b80ee104a5b4954951756e Mon Sep 17 00:00:00 2001 From: ffalqui Date: Mon, 19 Jan 2026 16:34:08 +0100 Subject: [PATCH 2/5] fix Content Scheduler menu position --- .../jacms/apsadmin/jsp/common/template/subMenu.jsp | 11 +++++++++++ .../apsadmin/global-messages_en.properties | 2 +- .../apsadmin/global-messages_it.properties | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cms-plugin/src/main/webapp/WEB-INF/plugins/jacms/apsadmin/jsp/common/template/subMenu.jsp b/cms-plugin/src/main/webapp/WEB-INF/plugins/jacms/apsadmin/jsp/common/template/subMenu.jsp index 424d599f72..687cf8557b 100644 --- a/cms-plugin/src/main/webapp/WEB-INF/plugins/jacms/apsadmin/jsp/common/template/subMenu.jsp +++ b/cms-plugin/src/main/webapp/WEB-INF/plugins/jacms/apsadmin/jsp/common/template/subMenu.jsp @@ -46,6 +46,17 @@ + + + + + + + + + + +
  • jacmsContentManager"> diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/global-messages_en.properties b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/global-messages_en.properties index c4f7adc063..564049d0e2 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/global-messages_en.properties +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/global-messages_en.properties @@ -1,5 +1,5 @@ jpcontentscheduler.code=jpcontentscheduler -jpcontentscheduler.name=Content Scheduler +jpcontentscheduler.name=Scheduler jpcontentscheduler.title.management=Content Scheduler diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/global-messages_it.properties b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/global-messages_it.properties index f53f9dc28f..54b04b86c4 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/global-messages_it.properties +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/global-messages_it.properties @@ -1,5 +1,5 @@ jpcontentscheduler.code=jpcontentscheduler -jpcontentscheduler.name=Content Scheduler +jpcontentscheduler.name=Scheduler jpcontentscheduler.title.management=Content Scheduler Management From 4e3ef00ee95cc4192fec60551913b961c513becb Mon Sep 17 00:00:00 2001 From: ffalqui Date: Tue, 20 Jan 2026 15:31:31 +0100 Subject: [PATCH 3/5] Suppress Struts 2 warnings for missing i18n keys and OGNL security --- engine/src/main/resources/base.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/engine/src/main/resources/base.xml b/engine/src/main/resources/base.xml index 4ba04cb7b8..6c73b4a444 100644 --- a/engine/src/main/resources/base.xml +++ b/engine/src/main/resources/base.xml @@ -87,4 +87,9 @@ + + + + + \ No newline at end of file From 1b2d1dac47592253027fd873d95d1e91050cdb32 Mon Sep 17 00:00:00 2001 From: ffalqui Date: Wed, 21 Jan 2026 10:33:47 +0100 Subject: [PATCH 4/5] fix content-scheduler user settings bugs --- .../ContentThreadConfigUsersAction.java | 21 ++++++++++++++----- .../apsadmin/config/package_en.properties | 2 +- .../apsadmin/config/package_it.properties | 2 +- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/ContentThreadConfigUsersAction.java b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/ContentThreadConfigUsersAction.java index 5be35e80e1..0362d446ec 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/ContentThreadConfigUsersAction.java +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/ContentThreadConfigUsersAction.java @@ -59,8 +59,10 @@ public class ContentThreadConfigUsersAction extends BaseAction { */ public String viewUsers() { try { - this.setConfigItemOnSession(); - + // Only initialize session if not already set, to preserve any pending changes + if (this.getRequest().getSession().getAttribute(THREAD_CONFIG_SESSION_PARAM_USERS_CONTENT_TYPE) == null) { + this.setConfigItemOnSession(); + } } catch (Throwable t) { _logger.error("Error in viewUsers", t); return FAILURE; @@ -255,8 +257,13 @@ public String saveUsersItem() { private Map> setConfigItemOnSession() { Map> usersContentType = this.getContentSchedulerManager().getConfig().getUsersContentType(); - this.getRequest().getSession().setAttribute(THREAD_CONFIG_SESSION_PARAM_USERS_CONTENT_TYPE, usersContentType); - return usersContentType; + // Create mutable copies to allow add/remove operations + Map> mutableCopy = new java.util.HashMap<>(); + if (usersContentType != null) { + usersContentType.forEach((key, value) -> mutableCopy.put(key, new ArrayList<>(value))); + } + this.getRequest().getSession().setAttribute(THREAD_CONFIG_SESSION_PARAM_USERS_CONTENT_TYPE, mutableCopy); + return mutableCopy; } private void setConfigItemOnSession(Map> config) { @@ -264,7 +271,11 @@ private void setConfigItemOnSession(Map> config) { } public Map> getUsersContentType() { - return (Map>) this.getRequest().getSession().getAttribute(THREAD_CONFIG_SESSION_PARAM_USERS_CONTENT_TYPE); + Map> config = (Map>) this.getRequest().getSession().getAttribute(THREAD_CONFIG_SESSION_PARAM_USERS_CONTENT_TYPE); + if (config == null) { + config = this.setConfigItemOnSession(); + } + return config; } public List getContentTypes() { diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/package_en.properties b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/package_en.properties index 3c916a2c12..e8af10556d 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/package_en.properties +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/package_en.properties @@ -53,7 +53,7 @@ requiredstringByArg=the file {0} is required legend.User=Users jpcontentscheduler.label.username=Username jpcontentscheduler.label.contentTypes=Content Types - +jpcontentscheduler.label.contentTypes.all=All Content Types jpcontentscheduler.label.contentTypes.contentType=Content type jpcontentscheduler.label.contentTypes.startDate=Start date diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/package_it.properties b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/package_it.properties index e19f15397f..8924375e75 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/package_it.properties +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/package_it.properties @@ -55,7 +55,7 @@ requiredstringByArg=Il file {0} é richiesto legend.User=Utenti jpcontentscheduler.label.username=Username jpcontentscheduler.label.contentTypes=Tipi di contenuto - +jpcontentscheduler.label.contentTypes.all=Tutti i Tipi di Contenuto jpcontentscheduler.label.contentTypes.contentType=Tipo di contenuto jpcontentscheduler.label.contentTypes.startDate=Data pubblicazione From 5f519db3a0130e1cf6dd7a2d60f85ca757e4b6dc Mon Sep 17 00:00:00 2001 From: ffalqui Date: Wed, 21 Jan 2026 18:19:56 +0100 Subject: [PATCH 5/5] update to EntLogger, EntException, improve coverage and handles duplicate key exception in versioning plugin --- .../system/services/content/ContentJobs.java | 70 +++--- .../content/ContentSchedulerManager.java | 32 ++- .../content/IContentSchedulerManager.java | 3 +- .../content/parse/ContentThreadConfigDOM.java | 18 +- .../system/services/content/util/Utils.java | 6 +- ...ContentThreadConfigContentTypesAction.java | 1 - .../ContentThreadConfigUsersAction.java | 5 +- .../versioning/VersioningManager.java | 40 +++- .../versioning/DuplicateKeyExceptionTest.java | 182 +++++++++++++++ .../versioning/TestVersioningManager.java | 65 ++++-- .../versioning/VersioningManagerUnitTest.java | 216 ++++++++++++++++++ 11 files changed, 537 insertions(+), 101 deletions(-) create mode 100644 versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/DuplicateKeyExceptionTest.java create mode 100644 versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/VersioningManagerUnitTest.java diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/ContentJobs.java b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/ContentJobs.java index 017e90659c..de843e7a8a 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/ContentJobs.java +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/ContentJobs.java @@ -21,32 +21,11 @@ */ package org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - import com.agiletec.aps.system.services.lang.ILangManager; -import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.ContentThreadConstants; -import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.model.ContentState; -import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.model.ContentSuspendMove; -import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.util.Utils; -import org.quartz.JobExecutionContext; -import org.quartz.JobExecutionException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.BeansException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.scheduling.quartz.QuartzJobBean; - import com.agiletec.aps.system.ApsSystemUtils; import com.agiletec.aps.system.common.entity.model.AttributeFieldError; import com.agiletec.aps.system.common.entity.model.AttributeTracer; import com.agiletec.aps.system.common.entity.model.attribute.AttributeInterface; -import com.agiletec.aps.system.exception.ApsSystemException; import com.agiletec.aps.system.services.category.Category; import com.agiletec.aps.system.services.category.ICategoryManager; import com.agiletec.aps.system.services.page.IPage; @@ -57,10 +36,31 @@ import com.agiletec.plugins.jacms.aps.system.services.content.model.Content; import com.agiletec.plugins.jacms.aps.system.services.contentmodel.ContentModel; import com.agiletec.plugins.jacms.aps.system.services.contentmodel.IContentModelManager; +import org.entando.entando.ent.exception.EntException; +import org.entando.entando.ent.util.EntLogging.EntLogger; +import org.entando.entando.ent.util.EntLogging.EntLogFactory; +import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.ContentThreadConstants; +import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.model.ContentState; +import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.model.ContentSuspendMove; +import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.util.Utils; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.scheduling.quartz.QuartzJobBean; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + + public class ContentJobs extends QuartzJobBean implements ApplicationContextAware { - private static final Logger _logger = LoggerFactory.getLogger(ContentJobs.class); + private static final EntLogger _logger = EntLogFactory.getSanitizedLogger(ContentJobs.class); private static final String APPLICATION_CONTEXT_KEY = "applicationContext"; @@ -114,7 +114,7 @@ public void executeJob(ApplicationContext appCtx) throws JobExecutionException { if (this.getContentSchedulerManager().getConfig() .isActive()/* && isCurrentSiteAllowed() */) { Date startJobDate = new Date(); - _logger.info(ContentThreadConstants.START_TIME_LOG + Utils.printTimeStamp(startJobDate)); + _logger.info(ContentThreadConstants.START_TIME_LOG + "{}", Utils.printTimeStamp(startJobDate)); List removedContents = new ArrayList(); List publishedContents = new ArrayList(); List moveContents = new ArrayList(); @@ -128,11 +128,11 @@ public void executeJob(ApplicationContext appCtx) throws JobExecutionException { Collections.sort(removedContents); Collections.sort(moveContents); Date endJobDate = new Date(); - _logger.info(ContentThreadConstants.END_TIME_LOG + Utils.printTimeStamp(endJobDate)); + _logger.info(ContentThreadConstants.END_TIME_LOG + "{}", Utils.printTimeStamp(endJobDate)); this.getContentSchedulerManager().sendMailWithResults(publishedContents, removedContents, moveContents, startJobDate, endJobDate); } catch (Throwable t) { - throw new ApsSystemException(ContentThreadConstants.ERROR_ON_MAIL, t); + throw new EntException(ContentThreadConstants.ERROR_ON_MAIL, t); } } catch (Throwable t) { ApsSystemUtils.logThrowable(t, this, t.getMessage()); @@ -145,7 +145,7 @@ public void executeJob(ApplicationContext appCtx) throws JobExecutionException { } } - private void publishContentsJob(List publishedContents) throws ApsSystemException { + private void publishContentsJob(List publishedContents) throws EntException { try { // Restituisce gli id dei contenuti che hanno un attributo con nome // key Data_inizio e valore la data corrente @@ -162,24 +162,21 @@ private void publishContentsJob(List publishedContents) throws Aps if (null == contentToPublish) { publishedContents.add(new ContentState(contentId, "null", "null", ContentThreadConstants.PUBLISH_ACTION, ContentThreadConstants.NULL_CONTENT)); - _logger.info("Pubblicazione automatica non riuscita: " + contentId + " - " - + ContentThreadConstants.NULL_CONTENT); + _logger.info("Pubblicazione automatica non riuscita: {} - " + ContentThreadConstants.NULL_CONTENT, contentId); continue; } if (contentToPublish.isOnLine()) { publishedContents.add(new ContentState(contentToPublish.getId(), contentToPublish.getTypeCode(), contentToPublish.getDescription(), ContentThreadConstants.PUBLISH_ACTION, ContentThreadConstants.ISALREADYONLINE)); - _logger.info("Pubblicazione automatica non riuscita: " + contentToPublish.getId() + " - " - + ContentThreadConstants.ISALREADYONLINE); + _logger.info("Pubblicazione automatica non riuscita: {} - " + ContentThreadConstants.ISALREADYONLINE, contentToPublish.getId()); continue; } if (!Content.STATUS_READY.equals(contentToPublish.getStatus())) { publishedContents.add(new ContentState(contentToPublish.getId(), contentToPublish.getTypeCode(), contentToPublish.getDescription(), ContentThreadConstants.PUBLISH_ACTION, ContentThreadConstants.NOTREADYSTATUS)); - _logger.info("Pubblicazione automatica non riuscita: " + contentToPublish.getId() + " - " - + ContentThreadConstants.NOTREADYSTATUS); + _logger.info("Pubblicazione automatica non riuscita: {} - " + ContentThreadConstants.NOTREADYSTATUS, contentToPublish.getId()); continue; } boolean validation = this.scanEntity(contentToPublish); @@ -187,14 +184,13 @@ private void publishContentsJob(List publishedContents) throws Aps publishedContents.add(new ContentState(contentToPublish.getId(), contentToPublish.getTypeCode(), contentToPublish.getDescription(), ContentThreadConstants.PUBLISH_ACTION, ContentThreadConstants.CONTENTWITHERRORS)); - _logger.info("Pubblicazione automatica non riuscita: " + contentToPublish.getId() + " - " - + ContentThreadConstants.CONTENTWITHERRORS); + _logger.info("Pubblicazione automatica non riuscita: {} - " + ContentThreadConstants.CONTENTWITHERRORS, contentToPublish.getId()); continue; } // pubblicazione on line del contenuto e modifica data di // ultima modifica this.getContentManager().insertOnLineContent(contentToPublish); - _logger.info("Pubblicato automaticamente contenuto " + contentToPublish.getId()); + _logger.info("Pubblicato automaticamente contenuto {}", contentToPublish.getId()); publishedContents.add(new ContentState(contentToPublish.getId(), contentToPublish.getTypeCode(), contentToPublish.getDescription(), ContentThreadConstants.PUBLISH_ACTION, ContentThreadConstants.ACTION_SUCCESS)); @@ -205,7 +201,7 @@ private void publishContentsJob(List publishedContents) throws Aps } } } catch (Throwable t) { - throw new ApsSystemException(ContentThreadConstants.ERROR_ON_PUBLISH, t); + throw new EntException(ContentThreadConstants.ERROR_ON_PUBLISH, t); } } @@ -230,7 +226,7 @@ public boolean scanEntity(Content currentContent) { @SuppressWarnings("unchecked") private void suspendOrMoveContentsJob(List removedContents, List moveContents, - ApplicationContext appCtx) throws ApsSystemException { + ApplicationContext appCtx) throws EntException { try { // Restituisce gli id dei contenuti che hanno un attributo con nome // key Data_fine e valore la data corrente diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/ContentSchedulerManager.java b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/ContentSchedulerManager.java index 59bf16505e..78146a1c43 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/ContentSchedulerManager.java +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/ContentSchedulerManager.java @@ -26,7 +26,6 @@ import com.agiletec.aps.system.common.AbstractService; import com.agiletec.aps.system.common.entity.model.EntitySearchFilter; import com.agiletec.aps.system.common.entity.model.attribute.ITextAttribute; -import com.agiletec.aps.system.exception.ApsSystemException; import com.agiletec.aps.system.services.authorization.IApsAuthority; import com.agiletec.aps.system.services.authorization.IAuthorizationManager; import com.agiletec.aps.system.services.baseconfig.ConfigInterface; @@ -213,7 +212,7 @@ public List getContentAttrDataFine() throws EntException { */ @Override public void sendMailWithResults(List publishedContents, List suspendedContents, List movedContents, Date startJobDate, - Date endJobDate) throws EntException, ApsSystemException { + Date endJobDate) throws EntException { // TODO send to groups // sendToGroups(publishedContents, suspendedContents); sendToUsers(publishedContents, suspendedContents, movedContents, startJobDate, endJobDate); @@ -229,11 +228,10 @@ public void sendMailWithResults(List publishedContents, List publishedContents, List suspendedContents, List moveContents, Date startJobDate, Date endJobDate) - throws EntException, ApsSystemException { + throws EntException { Map> mapUsers = this.getConfig().getUsersContentType(); Set keys = mapUsers.keySet(); - for (Iterator i = keys.iterator(); i.hasNext();) { - String key = i.next(); + for (String key : keys) { List typesList = mapUsers.get(key); List contentPList = contentOfTypes(publishedContents, typesList); List contentSList = contentOfTypes(suspendedContents, typesList); @@ -241,7 +239,7 @@ private void sendToUsers(List publishedContents, List 0) || (contentSList != null && contentSList.size() > 0) || (contentMList != null && contentMList.size() > 0)) { UserDetails user = this.getUserManager().getUser(key); if (user == null) { - ApsSystemUtils.getLogger().error(ContentThreadConstants.USER_IS_NULL + key); + _logger.error(ContentThreadConstants.USER_IS_NULL + "{}", key); continue; } else { UserProfile profile = (UserProfile) user.getProfile(); @@ -251,28 +249,24 @@ private void sendToUsers(List publishedContents, List 0) { email[0] = mailAttribute.getText(); String simpleText = Utils.prepareMailText(contentPList, contentSList, contentMList, this.getConfig(), startJobDate, endJobDate); + boolean issent = false; if (this.getConfig().isAlsoHtml()) { String applBaseUrl = this.getConfigManager().getParam(SystemConstants.PAR_APPL_BASE_URL); String htmlText = Utils.prepareMailHtml(contentPList, contentSList, contentMList, this.getConfig(), startJobDate, endJobDate, applBaseUrl); - boolean issent = this.getMailManager().sendMixedMail(simpleText, htmlText, config.getSubject(), null, email, null, null, config.getSenderCode()); + issent = this.getMailManager().sendMixedMail(simpleText, htmlText, config.getSubject(), null, email, null, null, config.getSenderCode()); // System.out.println("***MAIL html"); - if (issent) { - ApsSystemUtils.getLogger().info(ContentThreadConstants.MAIL_SENT + key); - } else { - ApsSystemUtils.getLogger().error(ContentThreadConstants.SEND_ERROR + key); - } } else { // System.out.println("***MAIL simple"); - boolean issent = this.getMailManager().sendMail(simpleText, config.getSubject(), email, null, null, config.getSenderCode()); - if (issent) { - ApsSystemUtils.getLogger().info(ContentThreadConstants.MAIL_SENT + key); - } else { - ApsSystemUtils.getLogger().error(ContentThreadConstants.SEND_ERROR + key); - } + issent = this.getMailManager().sendMail(simpleText, config.getSubject(), email, null, null, config.getSenderCode()); + } + if (issent) { + _logger.info(ContentThreadConstants.MAIL_SENT + "{}", key); + } else { + _logger.error(ContentThreadConstants.SEND_ERROR + "{}", key); } } } else { - ApsSystemUtils.getLogger().error(ContentThreadConstants.PROFILE_IS_NULL + key); + _logger.error(ContentThreadConstants.PROFILE_IS_NULL + "{}", key); } } } diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/IContentSchedulerManager.java b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/IContentSchedulerManager.java index 4a8dadf043..dd19f1db28 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/IContentSchedulerManager.java +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/IContentSchedulerManager.java @@ -21,7 +21,6 @@ */ package org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content; -import com.agiletec.aps.system.exception.ApsSystemException; import java.util.Date; import java.util.List; @@ -65,7 +64,7 @@ public interface IContentSchedulerManager { public void updateConfig(ContentThreadConfig config) throws EntException; public void sendMailWithResults(List publishedContents, List suspendedContents, List moveContents, Date startJobDate, Date endJobDate) - throws EntException, ApsSystemException; + throws EntException; /** * Return the desired system parameter diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/parse/ContentThreadConfigDOM.java b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/parse/ContentThreadConfigDOM.java index 7871ab9109..6beb092f74 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/parse/ContentThreadConfigDOM.java +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/parse/ContentThreadConfigDOM.java @@ -32,6 +32,7 @@ import java.util.Set; import org.apache.commons.lang3.StringUtils; +import org.entando.entando.ent.exception.EntException; import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.model.ContentThreadConfig; import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.model.ContentTypeElem; import org.jdom.CDATA; @@ -42,7 +43,6 @@ import org.jdom.output.XMLOutputter; import com.agiletec.aps.system.ApsSystemUtils; -import com.agiletec.aps.system.exception.ApsSystemException; /** * Classe DOM delegata alle operazioni di lettura/scrittura della configurazione @@ -56,10 +56,10 @@ public class ContentThreadConfigDOM { * @param xml * The xml containing the configuration. * @return The contentthread configuration. - * @throws ApsSystemException + * @throws EntException * In case of parsing errors. */ - public ContentThreadConfig extractConfig(String xml) throws ApsSystemException { + public ContentThreadConfig extractConfig(String xml) throws EntException { ContentThreadConfig config = new ContentThreadConfig(); config.setGroupsContentType(new HashMap<>()); config.setUsersContentType(new HashMap<>()); @@ -80,10 +80,10 @@ public ContentThreadConfig extractConfig(String xml) throws ApsSystemException { * @param config * The contentThread configuration. * @return The xml containing the configuration. - * @throws ApsSystemException + * @throws EntException * In case of errors. */ - public String createConfigXml(ContentThreadConfig config) throws ApsSystemException { + public String createConfigXml(ContentThreadConfig config) throws EntException { Element root = this.createConfigElement(config); Document doc = new Document(root); String xml = new XMLOutputter().outputString(doc); @@ -440,10 +440,10 @@ private Element createMailElement(ContentThreadConfig config) { * @param xmlText * The text containing an Xml. * @return The Xml element from a given text. - * @throws ApsSystemException + * @throws EntException * In case of parsing exceptions. */ - private Element getRootElement(String xmlText) throws ApsSystemException { + private Element getRootElement(String xmlText) throws EntException { SAXBuilder builder = new SAXBuilder(); builder.setValidation(false); StringReader reader = new StringReader(xmlText); @@ -453,10 +453,10 @@ private Element getRootElement(String xmlText) throws ApsSystemException { root = doc.getRootElement(); } catch (JDOMException t) { ApsSystemUtils.getLogger().error("Error parsing xml: " + t.getMessage()); - throw new ApsSystemException("Error parsing xml", t); + throw new EntException("Error parsing xml", t); } catch (IOException t) { ApsSystemUtils.getLogger().error("Error parsing xml: " + t.getMessage()); - throw new ApsSystemException("Error parsing xml", t); + throw new EntException("Error parsing xml", t); } return root; } diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/util/Utils.java b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/util/Utils.java index 62c2f2ab30..3e7a075929 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/util/Utils.java +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/util/Utils.java @@ -29,13 +29,13 @@ import java.util.List; import java.util.Map; +import org.entando.entando.ent.exception.EntException; import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.ContentThreadConstants; import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.model.ContentState; import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.model.ContentThreadConfig; import org.springframework.context.ApplicationContext; import com.agiletec.aps.system.ApsSystemUtils; -import com.agiletec.aps.system.exception.ApsSystemException; import com.agiletec.plugins.jacms.aps.system.services.content.ContentUtilizer; import com.agiletec.plugins.jacms.aps.system.services.content.model.Content; @@ -59,7 +59,7 @@ public static String printTimeStamp(Date date) { @SuppressWarnings("unchecked") public static Map getReferencingObjects(Content content, ApplicationContext appCtx) - throws ApsSystemException { + throws EntException { Map references = new HashMap(); try { // String[] defNames = @@ -82,7 +82,7 @@ public static Map getReferencingObjects(Content content, Applicati } } } catch (Throwable t) { - throw new ApsSystemException("Errore in hasReferencingObject", t); + throw new EntException("Errore in hasReferencingObject", t); } return references; } diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/ContentThreadConfigContentTypesAction.java b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/ContentThreadConfigContentTypesAction.java index 38bfde3569..48b4ac5335 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/ContentThreadConfigContentTypesAction.java +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/ContentThreadConfigContentTypesAction.java @@ -25,7 +25,6 @@ import java.util.List; import com.agiletec.aps.system.common.tree.ITreeNode; -import com.agiletec.aps.system.exception.ApsSystemException; import com.agiletec.apsadmin.system.ITreeNodeBaseActionHelper; import com.agiletec.apsadmin.system.TreeNodeWrapper; import org.apache.commons.lang3.StringUtils; diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/ContentThreadConfigUsersAction.java b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/ContentThreadConfigUsersAction.java index 0362d446ec..0aba788b04 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/ContentThreadConfigUsersAction.java +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/ContentThreadConfigUsersAction.java @@ -33,7 +33,6 @@ import org.slf4j.LoggerFactory; import com.agiletec.aps.system.common.entity.model.SmallEntityType; -import com.agiletec.aps.system.exception.ApsSystemException; import com.agiletec.aps.system.services.baseconfig.ConfigInterface; import com.agiletec.aps.system.services.user.IUserManager; import com.agiletec.apsadmin.system.BaseAction; @@ -218,7 +217,7 @@ private boolean validateAdd() throws EntException { return true; } - private boolean validateRemoveContentType() throws ApsSystemException { + private boolean validateRemoveContentType() throws EntException { if (StringUtils.isBlank(this.getUsername())) { this.addFieldError("username", this.getText("requiredstringByArg", this.getText("username"))); return false; @@ -231,7 +230,7 @@ private boolean validateRemoveContentType() throws ApsSystemException { return true; } - private boolean validateRemoveUser() throws ApsSystemException { + private boolean validateRemoveUser() throws EntException { if (StringUtils.isBlank(this.getUsername())) { this.addFieldError("username", this.getText("requiredstringByArg", this.getText("username"))); return false; diff --git a/versioning-plugin/src/main/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/VersioningManager.java b/versioning-plugin/src/main/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/VersioningManager.java index 232e5783d2..2c4723ccd3 100644 --- a/versioning-plugin/src/main/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/VersioningManager.java +++ b/versioning-plugin/src/main/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/VersioningManager.java @@ -190,18 +190,54 @@ public void saveContentVersion(String contentId) throws EntException { this.deleteWorkVersions(versionRecord.getContentId(), onlineVersionsToDelete); } if (null == this.getVersioningDAO().getVersion(contentId, versionRecord.getVersion())) { - this.getVersioningDAO().addContentVersion(versionRecord); + try { + this.getVersioningDAO().addContentVersion(versionRecord); + } catch (RuntimeException e) { + if (isDuplicateKeyException(e)) { + _logger.warn("ContentId '{}' - version '{}' already exists (concurrent insert)", contentId, versionRecord.getVersion()); + } else { + throw e; + } + } } else { - logger.warn("ContentId '{}' - version '{}' already exists", contentId, versionRecord.getVersion()); + _logger.warn("ContentId '{}' - version '{}' already exists", contentId, versionRecord.getVersion()); } } } + } catch (EntException e) { + throw e; } catch (Exception e) { _logger.error("error in Error saving version for content {}", contentId, e); throw new EntException("Error saving version for content" + contentId); } } + private boolean isDuplicateKeyException(Throwable e) { + while (e != null) { + if (e instanceof java.sql.SQLException) { + java.sql.SQLException sqlEx = (java.sql.SQLException) e; + String sqlState = sqlEx.getSQLState(); + if (sqlState != null) { + // 23505: unique_violation (PostgreSQL, Derby) + // 23000: integrity constraint violation (MySQL, Oracle - need to check error code) + if ("23505".equals(sqlState)) { + return true; + } + if ("23000".equals(sqlState)) { + int errorCode = sqlEx.getErrorCode(); + // MySQL: 1062 (ER_DUP_ENTRY), 1586 (ER_DUP_ENTRY_WITH_KEY_NAME) + // Oracle: 1 (ORA-00001: unique constraint violated) + if (errorCode == 1062 || errorCode == 1586 || errorCode == 1) { + return true; + } + } + } + } + e = e.getCause(); + } + return false; + } + @Override public void deleteWorkVersions(String contentId, int onlineVersion) throws EntException { try { diff --git a/versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/DuplicateKeyExceptionTest.java b/versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/DuplicateKeyExceptionTest.java new file mode 100644 index 0000000000..fce8684762 --- /dev/null +++ b/versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/DuplicateKeyExceptionTest.java @@ -0,0 +1,182 @@ +/* + * Copyright 2015-Present Entando Inc. (http://www.entando.com) All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.agiletec.plugins.jpversioning.aps.system.services.versioning; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.lang.reflect.Method; +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for duplicate key exception detection across different JDBC databases. + */ +@ExtendWith(MockitoExtension.class) +class DuplicateKeyExceptionTest { + + private VersioningManager versioningManager; + private Method isDuplicateKeyExceptionMethod; + + @BeforeEach + void setUp() throws Exception { + versioningManager = new VersioningManager(); + // Access the private method via reflection + isDuplicateKeyExceptionMethod = VersioningManager.class.getDeclaredMethod("isDuplicateKeyException", Throwable.class); + isDuplicateKeyExceptionMethod.setAccessible(true); + } + + private boolean invokeIsDuplicateKeyException(Throwable e) throws Exception { + return (Boolean) isDuplicateKeyExceptionMethod.invoke(versioningManager, e); + } + + // ==================== PostgreSQL Tests ==================== + + @Test + void testPostgreSQLDuplicateKey() throws Exception { + // PostgreSQL uses SQLState 23505 for unique_violation + SQLException sqlEx = new SQLException("duplicate key value violates unique constraint", "23505"); + assertTrue(invokeIsDuplicateKeyException(sqlEx), "PostgreSQL duplicate key should be detected"); + } + + @Test + void testPostgreSQLDuplicateKeyWrappedInRuntimeException() throws Exception { + SQLException sqlEx = new SQLException("duplicate key value violates unique constraint", "23505"); + RuntimeException wrapped = new RuntimeException("Error adding version record", sqlEx); + assertTrue(invokeIsDuplicateKeyException(wrapped), "Wrapped PostgreSQL duplicate key should be detected"); + } + + // ==================== Derby Tests ==================== + + @Test + void testDerbyDuplicateKey() throws Exception { + // Derby also uses SQLState 23505 for duplicate key + SQLException sqlEx = new SQLException("The statement was aborted because it would have caused a duplicate key value", "23505"); + assertTrue(invokeIsDuplicateKeyException(sqlEx), "Derby duplicate key should be detected"); + } + + @Test + void testDerbyDuplicateKeyWrappedInRuntimeException() throws Exception { + SQLException sqlEx = new SQLException("The statement was aborted because it would have caused a duplicate key value", "23505"); + RuntimeException wrapped = new RuntimeException("Error adding version record", sqlEx); + assertTrue(invokeIsDuplicateKeyException(wrapped), "Wrapped Derby duplicate key should be detected"); + } + + // ==================== MySQL Tests ==================== + + @Test + void testMySQLDuplicateKey_ErrorCode1062() throws Exception { + // MySQL uses SQLState 23000 with error code 1062 (ER_DUP_ENTRY) + SQLException sqlEx = new SQLException("Duplicate entry 'value' for key 'PRIMARY'", "23000", 1062); + assertTrue(invokeIsDuplicateKeyException(sqlEx), "MySQL duplicate key (1062) should be detected"); + } + + @Test + void testMySQLDuplicateKey_ErrorCode1586() throws Exception { + // MySQL uses SQLState 23000 with error code 1586 (ER_DUP_ENTRY_WITH_KEY_NAME) + SQLException sqlEx = new SQLException("Duplicate entry 'value' for key 'key_name'", "23000", 1586); + assertTrue(invokeIsDuplicateKeyException(sqlEx), "MySQL duplicate key (1586) should be detected"); + } + + @Test + void testMySQLDuplicateKeyWrappedInRuntimeException() throws Exception { + SQLException sqlEx = new SQLException("Duplicate entry 'value' for key 'PRIMARY'", "23000", 1062); + RuntimeException wrapped = new RuntimeException("Error adding version record", sqlEx); + assertTrue(invokeIsDuplicateKeyException(wrapped), "Wrapped MySQL duplicate key should be detected"); + } + + // ==================== Oracle Tests ==================== + + @Test + void testOracleDuplicateKey() throws Exception { + // Oracle uses SQLState 23000 with error code 1 (ORA-00001: unique constraint violated) + SQLException sqlEx = new SQLException("ORA-00001: unique constraint (SCHEMA.CONSTRAINT_NAME) violated", "23000", 1); + assertTrue(invokeIsDuplicateKeyException(sqlEx), "Oracle duplicate key should be detected"); + } + + @Test + void testOracleDuplicateKeyWrappedInRuntimeException() throws Exception { + SQLException sqlEx = new SQLException("ORA-00001: unique constraint (SCHEMA.CONSTRAINT_NAME) violated", "23000", 1); + RuntimeException wrapped = new RuntimeException("Error adding version record", sqlEx); + assertTrue(invokeIsDuplicateKeyException(wrapped), "Wrapped Oracle duplicate key should be detected"); + } + + // ==================== Negative Tests ==================== + + @Test + void testNonDuplicateKeyException() throws Exception { + // A general SQL exception that is not a duplicate key + SQLException sqlEx = new SQLException("Connection refused", "08001"); + assertFalse(invokeIsDuplicateKeyException(sqlEx), "Non-duplicate key exception should not be detected"); + } + + @Test + void testForeignKeyViolation_MySQL() throws Exception { + // MySQL foreign key violation: SQLState 23000 with error code 1452 + SQLException sqlEx = new SQLException("Cannot add or update a child row: a foreign key constraint fails", "23000", 1452); + assertFalse(invokeIsDuplicateKeyException(sqlEx), "MySQL foreign key violation should not be detected as duplicate key"); + } + + @Test + void testForeignKeyViolation_PostgreSQL() throws Exception { + // PostgreSQL foreign key violation: SQLState 23503 + SQLException sqlEx = new SQLException("insert or update on table violates foreign key constraint", "23503"); + assertFalse(invokeIsDuplicateKeyException(sqlEx), "PostgreSQL foreign key violation should not be detected as duplicate key"); + } + + @Test + void testNullException() throws Exception { + assertFalse(invokeIsDuplicateKeyException(null), "Null exception should return false"); + } + + @Test + void testNullSQLState() throws Exception { + SQLException sqlEx = new SQLException("Some error", (String) null); + assertFalse(invokeIsDuplicateKeyException(sqlEx), "Null SQL state should return false"); + } + + @Test + void testNonSQLException() throws Exception { + RuntimeException ex = new RuntimeException("Some error"); + assertFalse(invokeIsDuplicateKeyException(ex), "Non-SQLException without cause should return false"); + } + + @Test + void testDeeplyNestedSQLException() throws Exception { + SQLException sqlEx = new SQLException("duplicate key", "23505"); + RuntimeException level1 = new RuntimeException("Level 1", sqlEx); + RuntimeException level2 = new RuntimeException("Level 2", level1); + RuntimeException level3 = new RuntimeException("Level 3", level2); + assertTrue(invokeIsDuplicateKeyException(level3), "Deeply nested SQLException should be detected"); + } + + @Test + void testIntegrityConstraint23000WithoutMatchingErrorCode() throws Exception { + // SQLState 23000 but with an error code that doesn't match duplicate key + SQLException sqlEx = new SQLException("Some other constraint violation", "23000", 9999); + assertFalse(invokeIsDuplicateKeyException(sqlEx), "SQLState 23000 with non-matching error code should return false"); + } +} \ No newline at end of file diff --git a/versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/TestVersioningManager.java b/versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/TestVersioningManager.java index e443263c05..5f5ecb5026 100644 --- a/versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/TestVersioningManager.java +++ b/versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/TestVersioningManager.java @@ -21,12 +21,6 @@ */ package com.agiletec.plugins.jpversioning.aps.system.services.versioning; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - import com.agiletec.aps.BaseTestCase; import com.agiletec.aps.system.SystemConstants; import com.agiletec.aps.system.services.baseconfig.ConfigInterface; @@ -47,6 +41,9 @@ import java.util.Map; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; /** * @author G.Cocco @@ -58,6 +55,22 @@ public class TestVersioningManager extends BaseTestCase { private IVersioningManager versioningManager; private JpversioningTestHelper helper; + @BeforeEach + public void init() throws Exception { + this.versioningManager = (IVersioningManager) this.getService(JpversioningSystemConstants.VERSIONING_MANAGER); + this.configManager = (ConfigInterface) this.getService(SystemConstants.BASE_CONFIG_MANAGER); + this.contentManager = (IContentManager) this.getService(JacmsSystemConstants.CONTENT_MANAGER); + DataSource dataSource = (DataSource) this.getApplicationContext().getBean("portDataSource"); + this.helper = new JpversioningTestHelper(dataSource, this.getApplicationContext()); + this.helper.initContentVersions(); + } + + @AfterEach + public void dispose() throws Exception { + this.helper.cleanContentVersions(); + } + + @Test void testGetVersions() throws Throwable { List versions = this.versioningManager.getVersions("CNG12"); assertNull(versions); @@ -66,6 +79,7 @@ void testGetVersions() throws Throwable { this.checkVersionIds(new long[]{1, 2, 3}, versions); } + @Test void testGetLastVersions() throws Throwable { List versions = this.versioningManager.getLastVersions("CNG", null); assertTrue(versions.isEmpty()); @@ -74,6 +88,7 @@ void testGetLastVersions() throws Throwable { this.checkVersionIds(new long[]{3}, versions); } + @Test void testGetVersion() throws Throwable { ContentVersion contentVersion = this.versioningManager.getVersion(10000); assertNull(contentVersion); @@ -92,6 +107,7 @@ void testGetVersion() throws Throwable { assertEquals("admin", contentVersion.getUsername()); } + @Test void testGetLastVersion() throws Throwable { ContentVersion contentVersion = this.versioningManager.getLastVersion("CNG12"); assertNull(contentVersion); @@ -109,6 +125,7 @@ void testGetLastVersion() throws Throwable { assertEquals("mainEditor", contentVersion.getUsername()); } + @Test void testSaveGetDeleteVersion() throws Throwable { ((VersioningManager) this.versioningManager).saveContentVersion("ART102"); ContentVersion contentVersion = this.versioningManager.getLastVersion("ART102"); @@ -128,14 +145,23 @@ void testSaveGetDeleteVersion() throws Throwable { assertNull(this.versioningManager.getLastVersion("ART102")); } + @Test public void deleteWorkVersions() throws Throwable { - List versions = this.versioningManager.getVersions("ART1"); - this.checkVersionIds(new long[]{1, 2, 3}, versions); - this.versioningManager.deleteWorkVersions("ART1", 0); - versions = this.versioningManager.getVersions("ART1"); - this.checkVersionIds(new long[]{1, 3}, versions); + try { + this.updateConfigItem(JpversioningSystemConstants.CONFIG_PARAM_DELETE_MID_VERSIONS, "true"); + ((VersioningManager) this.versioningManager).initTenantAware(); + List versions = this.versioningManager.getVersions("ART1"); + this.checkVersionIds(new long[]{1, 2, 3}, versions); + this.versioningManager.deleteWorkVersions("ART1", 0); + versions = this.versioningManager.getVersions("ART1"); + this.checkVersionIds(new long[]{1, 3}, versions); + } finally { + this.updateConfigItem(JpversioningSystemConstants.CONFIG_PARAM_DELETE_MID_VERSIONS, ""); + ((VersioningManager) this.versioningManager).initTenantAware(); + } } + private void checkVersionIds(long[] expected, List received) { assertEquals(expected.length, received.size()); for (long current : expected) { @@ -145,16 +171,19 @@ private void checkVersionIds(long[] expected, List received) { } } + @Test void testContentVersionToIgnore_1() throws Exception { this.testContentVersionToIgnore(false, true); this.testContentVersionToIgnore(true, true); } + @Test void testContentVersionToIgnore_2() throws Exception { this.testContentVersionToIgnore(false, false); this.testContentVersionToIgnore(true, false); } + protected void testContentVersionToIgnore(boolean includeVersion, boolean isType) throws Exception { String newContentId = null; List versions = null; @@ -214,19 +243,5 @@ private void updateConfigItem(String paramKey, String paramValue) throws Excepti this.configManager.updateConfigItem(SystemConstants.CONFIG_ITEM_PARAMS, newXmlParams); } - @BeforeEach - private void init() throws Exception { - this.versioningManager = (IVersioningManager) this.getService(JpversioningSystemConstants.VERSIONING_MANAGER); - this.configManager = (ConfigInterface) this.getService(SystemConstants.BASE_CONFIG_MANAGER); - this.contentManager = (IContentManager) this.getService(JacmsSystemConstants.CONTENT_MANAGER); - DataSource dataSource = (DataSource) this.getApplicationContext().getBean("portDataSource"); - this.helper = new JpversioningTestHelper(dataSource, this.getApplicationContext()); - this.helper.initContentVersions(); - } - - @AfterEach - private void dispose() throws Exception { - this.helper.cleanContentVersions(); - } } diff --git a/versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/VersioningManagerUnitTest.java b/versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/VersioningManagerUnitTest.java new file mode 100644 index 0000000000..76c902f375 --- /dev/null +++ b/versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/VersioningManagerUnitTest.java @@ -0,0 +1,216 @@ +/* + * Copyright 2015-Present Entando Inc. (http://www.entando.com) All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.agiletec.plugins.jpversioning.aps.system.services.versioning; + +import com.agiletec.aps.system.services.baseconfig.ConfigInterface; +import com.agiletec.plugins.jacms.aps.system.services.content.IContentManager; +import com.agiletec.plugins.jacms.aps.system.services.content.model.Content; +import com.agiletec.plugins.jacms.aps.system.services.content.model.ContentRecordVO; +import org.entando.entando.ent.exception.EntException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.sql.SQLException; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class VersioningManagerUnitTest { + + @Mock + private IVersioningDAO versioningDAO; + + @Mock + private IContentManager contentManager; + + @Mock + private ConfigInterface configManager; + + @Spy + @InjectMocks + private VersioningManager versioningManager; + + private ContentRecordVO mockContentRecord; + + @BeforeEach + void setUp() { + mockContentRecord = new ContentRecordVO(); + mockContentRecord.setId("ART123"); + mockContentRecord.setTypeCode("ART"); + mockContentRecord.setDescription("Test Content"); + mockContentRecord.setStatus(Content.STATUS_DRAFT); + mockContentRecord.setXmlWork("test"); + mockContentRecord.setModify(new Date()); + mockContentRecord.setVersion("1.0"); + mockContentRecord.setLastEditor("admin"); + } + + @Test + void testSaveContentVersion_ThrowsExceptionForNonDuplicateKeyError() throws Exception { + // Setup + when(contentManager.loadContentVO("ART123")).thenReturn(mockContentRecord); + when(versioningDAO.getVersion(anyString(), anyString())).thenReturn(null); + + // Simulate a generic RuntimeException (not duplicate key) + RuntimeException genericException = new RuntimeException("Database connection failed"); + doThrow(genericException).when(versioningDAO).addContentVersion(any(ContentVersion.class)); + + // Execute and verify - should throw EntException wrapping the original exception + EntException thrown = assertThrows(EntException.class, + () -> versioningManager.saveContentVersion("ART123")); + + assertEquals("Error saving version for contentART123", thrown.getMessage()); + } + + @Test + void testSaveContentVersion_HandlesUniqueConstraintViolationGracefully() throws Exception { + // Setup + when(contentManager.loadContentVO("ART123")).thenReturn(mockContentRecord); + when(versioningDAO.getVersion(anyString(), anyString())).thenReturn(null); + + // Simulate a duplicate key exception (PostgreSQL/Derby SQLState 23505) + SQLException sqlException = new SQLException("unique constraint violation", "23505"); + RuntimeException duplicateKeyException = new RuntimeException("Error adding version record", sqlException); + doThrow(duplicateKeyException).when(versioningDAO).addContentVersion(any(ContentVersion.class)); + + // Execute - should NOT throw exception for duplicate key + assertDoesNotThrow(() -> versioningManager.saveContentVersion("ART123")); + + // Verify addContentVersion was called + verify(versioningDAO).addContentVersion(any(ContentVersion.class)); + } + + @Test + void testSaveContentVersion_HandlesMySQLDuplicateKeyGracefully() throws Exception { + // Setup + when(contentManager.loadContentVO("ART123")).thenReturn(mockContentRecord); + when(versioningDAO.getVersion(anyString(), anyString())).thenReturn(null); + + // Simulate a MySQL duplicate key exception (SQLState 23000, error code 1062) + SQLException sqlException = new SQLException("Duplicate entry for key 'PRIMARY'", "23000", 1062); + RuntimeException duplicateKeyException = new RuntimeException("Error adding version record", sqlException); + doThrow(duplicateKeyException).when(versioningDAO).addContentVersion(any(ContentVersion.class)); + + // Execute - should NOT throw exception + assertDoesNotThrow(() -> versioningManager.saveContentVersion("ART123")); + } + + @Test + void testSaveContentVersion_HandlesOracleDuplicateKeyGracefully() throws Exception { + // Setup + when(contentManager.loadContentVO("ART123")).thenReturn(mockContentRecord); + when(versioningDAO.getVersion(anyString(), anyString())).thenReturn(null); + + // Simulate an Oracle duplicate key exception (SQLState 23000, error code 1) + SQLException sqlException = new SQLException("ORA-00001: unique constraint violated", "23000", 1); + RuntimeException primaryKeyException = new RuntimeException("Error adding version record", sqlException); + doThrow(primaryKeyException).when(versioningDAO).addContentVersion(any(ContentVersion.class)); + + // Execute - should NOT throw exception + assertDoesNotThrow(() -> versioningManager.saveContentVersion("ART123")); + } + + @Test + void testSaveContentVersion_ThrowsForNullPointerException() throws Exception { + // Setup + when(contentManager.loadContentVO("ART123")).thenReturn(mockContentRecord); + when(versioningDAO.getVersion(anyString(), anyString())).thenReturn(null); + + // Simulate a NullPointerException (should NOT be treated as duplicate key) + NullPointerException npe = new NullPointerException("Some null value"); + doThrow(npe).when(versioningDAO).addContentVersion(any(ContentVersion.class)); + + // Execute and verify - should throw EntException + assertThrows(EntException.class, () -> versioningManager.saveContentVersion("ART123")); + } + + @Test + void testSaveContentVersion_ThrowsForIllegalArgumentException() throws Exception { + // Setup + when(contentManager.loadContentVO("ART123")).thenReturn(mockContentRecord); + when(versioningDAO.getVersion(anyString(), anyString())).thenReturn(null); + + // Simulate an IllegalArgumentException (should NOT be treated as duplicate key) + IllegalArgumentException iae = new IllegalArgumentException("Invalid argument"); + doThrow(iae).when(versioningDAO).addContentVersion(any(ContentVersion.class)); + + // Execute and verify - should throw EntException + assertThrows(EntException.class, () -> versioningManager.saveContentVersion("ART123")); + } + + @Test + void testSaveContentVersion_SkipsInsertWhenVersionAlreadyExists() throws Exception { + // Setup + when(contentManager.loadContentVO("ART123")).thenReturn(mockContentRecord); + + // Return existing version (not null) - simulates version already exists + ContentVersion existingVersion = new ContentVersion(); + existingVersion.setId(1L); + when(versioningDAO.getVersion("ART123", "1.0")).thenReturn(existingVersion); + + // Execute + assertDoesNotThrow(() -> versioningManager.saveContentVersion("ART123")); + + // Verify addContentVersion was NEVER called since version already exists + verify(versioningDAO, never()).addContentVersion(any(ContentVersion.class)); + } + + @Test + void testSaveContentVersion_HandlesNestedDuplicateKeyException() throws Exception { + // Setup + when(contentManager.loadContentVO("ART123")).thenReturn(mockContentRecord); + when(versioningDAO.getVersion(anyString(), anyString())).thenReturn(null); + + // Simulate a deeply nested exception where the root cause is a SQLException + SQLException sqlException = new SQLException("duplicate key value violates unique constraint", "23505"); + RuntimeException cause = new RuntimeException("Database error", sqlException); + RuntimeException wrapper = new RuntimeException("Insert failed", cause); + doThrow(wrapper).when(versioningDAO).addContentVersion(any(ContentVersion.class)); + + // Execute - should NOT throw exception because nested cause has duplicate key + assertDoesNotThrow(() -> versioningManager.saveContentVersion("ART123")); + } + + @Test + void testSaveContentVersion_ThrowsForNestedNonDuplicateException() throws Exception { + // Setup + when(contentManager.loadContentVO("ART123")).thenReturn(mockContentRecord); + when(versioningDAO.getVersion(anyString(), anyString())).thenReturn(null); + + // Simulate a nested exception without duplicate key indicators + RuntimeException cause = new RuntimeException("Connection timeout"); + RuntimeException wrapper = new RuntimeException("Database error", cause); + doThrow(wrapper).when(versioningDAO).addContentVersion(any(ContentVersion.class)); + + // Execute and verify - should throw EntException + assertThrows(EntException.class, () -> versioningManager.saveContentVersion("ART123")); + } +} \ No newline at end of file