diff --git a/Dockerfile.tomcat b/Dockerfile.tomcat
index 27221b39dc..de861b4f9b 100644
--- a/Dockerfile.tomcat
+++ b/Dockerfile.tomcat
@@ -10,8 +10,8 @@ LABEL name="Entando App" \
summary="Entando Application" \
description="This Entando app engine application provides APIs and composition for Entando applications"
-#COPY target/generated-resources/licenses /licenses
-#COPY target/generated-resources/licenses.xml /
+COPY target/generated-resources/licenses /licenses
+COPY target/generated-resources/licenses.xml /
COPY --chown=185:0 webapp/target/*.war /usr/local/tomcat/webapps/
# Copy CookieProcessor JAR if it exists
diff --git a/README.md b/README.md
index cfb8547e45..fc67935f80 100644
--- a/README.md
+++ b/README.md
@@ -66,4 +66,6 @@ The general log level is controlled by the variable `ROOT_LOG_LEVEL`, that in te
|| ENTANDO_BUNDLE_CLI_ETC | ${ENTANDO_BUNDLE_CLI_ETC}/hub/credentials | Credentials/parameters saved within JSON files under this path for ent bundle add hub command
|| ENTANDO_APP_ENGINE_HEALTH_CHECK_TYPE | db.migration.strategy | [auto], skip, disabled, generate_sql | Liquibase strategy
|| LOG_CONFIG_FILE_PATH | | to use the logback composable feature |
-|| ENTANDO_DOCKER_REGISTRY_OVERRIDE | | Deprecated-for v1 bundles, to propagate to CM for plugins |
+|| ENTANDO_DOCKER_REGISTRY_OVERRIDE | | Deprecated-for v1 bundles, to propagate to CM for plugins |
+| Feature Flags | ENTANDO_FEATURE_FLAGS | comma-separated list of tags | Enable experimental features. Example: `CACHE_PIPELINE` |
+|| ENTANDO_FF_DEEP_DEBUG | comma-separated list of tags | Enable deep debug logging for specific components. Example: `service-reload` |
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/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..79b3020a26 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,47 +21,47 @@
*/
package org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content;
+
+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.services.category.Category;
+import com.agiletec.aps.system.services.category.ICategoryManager;
+import com.agiletec.aps.system.services.lang.ILangManager;
+import com.agiletec.aps.system.services.page.IPage;
+import com.agiletec.aps.system.services.page.IPageManager;
+import com.agiletec.aps.system.services.page.Widget;
+import com.agiletec.aps.util.ApsProperties;
+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.contentmodel.ContentModel;
+import com.agiletec.plugins.jacms.aps.system.services.contentmodel.IContentModelManager;
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.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.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;
-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;
-import com.agiletec.aps.system.services.page.IPageManager;
-import com.agiletec.aps.system.services.page.Widget;
-import com.agiletec.aps.util.ApsProperties;
-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.contentmodel.ContentModel;
-import com.agiletec.plugins.jacms.aps.system.services.contentmodel.IContentModelManager;
+
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";
@@ -70,12 +70,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 +95,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
@@ -116,7 +115,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();
@@ -130,11 +129,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());
@@ -147,7 +146,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
@@ -164,24 +163,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);
@@ -189,14 +185,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));
@@ -207,7 +202,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);
}
}
@@ -217,7 +212,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;
}
@@ -232,7 +227,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
@@ -767,4 +762,9 @@ 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/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..22cb694b1c 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
@@ -51,6 +51,8 @@
import org.entando.entando.aps.system.services.tenants.RefreshableBeanTenantAware;
import org.entando.entando.aps.system.services.userprofile.model.UserProfile;
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;
@@ -58,8 +60,6 @@
import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.model.ContentTypeElem;
import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.parse.ContentThreadConfigDOM;
import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.util.Utils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
@@ -70,7 +70,7 @@
public class ContentSchedulerManager extends AbstractService implements IContentSchedulerManager,
RefreshableBeanTenantAware {
- private static final Logger _logger = LoggerFactory.getLogger(ContentSchedulerManager.class);
+ private static final EntLogger _logger = EntLogFactory.getSanitizedLogger(ContentSchedulerManager.class);
private static final long serialVersionUID = 6880576602469119814L;
private IContentSearcherDAO _workContentSearcherDAO;
@@ -213,7 +213,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 +229,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 +240,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 +250,22 @@ 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());
- // System.out.println("***MAIL html");
- if (issent) {
- ApsSystemUtils.getLogger().info(ContentThreadConstants.MAIL_SENT + key);
- } else {
- ApsSystemUtils.getLogger().error(ContentThreadConstants.SEND_ERROR + key);
- }
+ issent = this.getMailManager().sendMixedMail(simpleText, htmlText, config.getSubject(), null, email, null, null, config.getSenderCode());
} 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);
}
}
}
@@ -358,8 +351,6 @@ public String getSystemParam(String paramName) {
Map systemParams = SystemParamsUtils.getParams(xmlParams);
param = systemParams.get(paramName);
} catch (Throwable t) {
- // _logger.error("error getting the system parameter " + paramName,
- // t);
ApsSystemUtils.logThrowable(t, this, "error getting the system parameter " + paramName);
}
return param;
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 ab9f480dfd..01b09b2f8e 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.jdom2.CDATA;
@@ -42,7 +43,6 @@
import org.jdom2.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 b7f14424ed..4e4959b9d9 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;
@@ -37,11 +36,9 @@
import com.agiletec.aps.system.common.entity.model.SmallEntityType;
import com.agiletec.aps.system.services.baseconfig.ConfigInterface;
-import com.agiletec.aps.system.services.category.Category;
import com.agiletec.aps.system.services.category.ICategoryManager;
import com.agiletec.apsadmin.system.AbstractTreeAction;
-import static com.agiletec.apsadmin.system.BaseAction.FAILURE;
import com.agiletec.plugins.jacms.aps.system.services.content.IContentManager;
import org.apache.struts2.action.Action;
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 adaf2dbb59..8a54e82ebf 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;
@@ -59,8 +58,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;
@@ -216,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;
@@ -229,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;
@@ -255,8 +256,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,8 +270,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() {
List smallContentTypes = this.getContentManager().getSmallEntityTypes();
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..182b183669 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,6 +53,8 @@ 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
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
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/src/main/java/com/agiletec/aps/system/common/AbstractService.java b/engine/src/main/java/com/agiletec/aps/system/common/AbstractService.java
index 0c9cd2dad8..0d34504880 100644
--- a/engine/src/main/java/com/agiletec/aps/system/common/AbstractService.java
+++ b/engine/src/main/java/com/agiletec/aps/system/common/AbstractService.java
@@ -13,14 +13,14 @@
*/
package com.agiletec.aps.system.common;
+import com.agiletec.aps.system.common.notify.ApsEvent;
+import com.agiletec.aps.system.common.notify.INotifyManager;
+import com.agiletec.aps.util.ApsTenantApplicationUtils;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
-
-import com.agiletec.aps.system.common.notify.ApsEvent;
-import com.agiletec.aps.system.common.notify.INotifyManager;
import org.springframework.context.ApplicationEvent;
/**
@@ -82,6 +82,12 @@ public void setBeanName(String name) {
public String getName() {
return _name;
}
+
+
+ protected String getTenantCode() {
+ return ApsTenantApplicationUtils.getTenant()
+ .orElse("");
+ }
protected INotifyManager getNotifyManager() {
return _notifyManager;
diff --git a/engine/src/main/java/org/entando/entando/web/swagger/OpenApiConfig.java b/engine/src/main/java/org/entando/entando/web/swagger/OpenApiConfig.java
index f03062b9b6..8416116dc8 100644
--- a/engine/src/main/java/org/entando/entando/web/swagger/OpenApiConfig.java
+++ b/engine/src/main/java/org/entando/entando/web/swagger/OpenApiConfig.java
@@ -11,6 +11,7 @@
import io.swagger.v3.oas.models.servers.Server;
import jakarta.servlet.ServletContext;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
@@ -18,7 +19,8 @@
@Configuration
public class OpenApiConfig {
- private static final String SECURITY_SCHEME_NAME = "entando";
+ private static final String OAUTH2_SECURITY_SCHEME_NAME = "oauth2";
+ private static final String BASIC_AUTH_SECURITY_SCHEME_NAME = "basicAuth";
@Autowired
ServletContext servletContext; // package-private for testing
@@ -26,6 +28,9 @@ public class OpenApiConfig {
@Autowired
Environment environment; // package-private for testing
+ @Value("${keycloak.enabled:false}")
+ boolean keycloakEnabled; // package-private for testing
+
private String getAuthServer() {
String baseAuthServer = environment.getProperty(SystemConstants.SYSTEM_PROP_KEYCLOAK_AUTH_URL);
String kcRealm = environment.getProperty(SystemConstants.SYSTEM_PROP_KEYCLOAK_REALM);
@@ -50,7 +55,18 @@ public OpenAPI customOpenAPI() {
openAPI.addServersItem(new Server().url(contextPath));
}
- // Add OAuth2 security scheme if auth server is configured
+ if (keycloakEnabled) {
+ // Add OAuth2 security scheme when Keycloak is enabled
+ configureOAuth2Security(openAPI);
+ } else {
+ // Add Basic Auth security scheme when Keycloak is disabled
+ configureBasicAuthSecurity(openAPI);
+ }
+
+ return openAPI;
+ }
+
+ private void configureOAuth2Security(OpenAPI openAPI) {
String authServer = getAuthServer();
if (authServer != null && !authServer.isEmpty()) {
OAuthFlow authorizationCodeFlow = new OAuthFlow()
@@ -62,15 +78,24 @@ public OpenAPI customOpenAPI() {
authorizationCodeFlow.addExtension("x-logout-url", authServer + "/logout");
openAPI.components(new Components()
- .addSecuritySchemes(SECURITY_SCHEME_NAME, new SecurityScheme()
+ .addSecuritySchemes(OAUTH2_SECURITY_SCHEME_NAME, new SecurityScheme()
.type(SecurityScheme.Type.OAUTH2)
.flows(new OAuthFlows()
.authorizationCode(authorizationCodeFlow))));
// Apply security globally
- openAPI.addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME));
+ openAPI.addSecurityItem(new SecurityRequirement().addList(OAUTH2_SECURITY_SCHEME_NAME));
}
+ }
- return openAPI;
+ private void configureBasicAuthSecurity(OpenAPI openAPI) {
+ openAPI.components(new Components()
+ .addSecuritySchemes(BASIC_AUTH_SECURITY_SCHEME_NAME, new SecurityScheme()
+ .type(SecurityScheme.Type.HTTP)
+ .scheme("basic")
+ .description("Basic Authentication - Use your Entando username and password")));
+
+ // Apply security globally
+ openAPI.addSecurityItem(new SecurityRequirement().addList(BASIC_AUTH_SECURITY_SCHEME_NAME));
}
}
\ No newline at end of file
diff --git a/engine/src/main/resources/base.xml b/engine/src/main/resources/base.xml
index 4ba04cb7b8..cd1f42ecb0 100644
--- a/engine/src/main/resources/base.xml
+++ b/engine/src/main/resources/base.xml
@@ -87,4 +87,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/engine/src/test/java/org/entando/entando/web/swagger/OpenApiConfigTest.java b/engine/src/test/java/org/entando/entando/web/swagger/OpenApiConfigTest.java
index 260fccaf10..3c73d2fff5 100644
--- a/engine/src/test/java/org/entando/entando/web/swagger/OpenApiConfigTest.java
+++ b/engine/src/test/java/org/entando/entando/web/swagger/OpenApiConfigTest.java
@@ -4,7 +4,6 @@
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.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.agiletec.aps.system.SystemConstants;
@@ -35,26 +34,39 @@ class OpenApiConfigTest {
@BeforeEach
public void setup() {
- when(environment.getProperty(SystemConstants.SYSTEM_PROP_KEYCLOAK_AUTH_URL)).thenReturn(authUrl);
- when(environment.getProperty(SystemConstants.SYSTEM_PROP_KEYCLOAK_REALM)).thenReturn(realm);
+ Mockito.lenient().when(environment.getProperty(SystemConstants.SYSTEM_PROP_KEYCLOAK_AUTH_URL)).thenReturn(authUrl);
+ Mockito.lenient().when(environment.getProperty(SystemConstants.SYSTEM_PROP_KEYCLOAK_REALM)).thenReturn(realm);
Mockito.lenient().when(servletContext.getContextPath()).thenReturn("/entando-de-app");
}
@Test
- void createOpenApiConfig() {
+ void createOpenApiConfig_keycloakEnabled() {
OpenApiConfig config = new OpenApiConfig();
config.environment = environment;
config.servletContext = servletContext;
+ config.keycloakEnabled = true;
OpenAPI openAPI = config.customOpenAPI();
assertNotNull(openAPI);
}
@Test
- void customOpenAPITest() {
+ void createOpenApiConfig_keycloakDisabled() {
OpenApiConfig config = new OpenApiConfig();
config.environment = environment;
config.servletContext = servletContext;
+ config.keycloakEnabled = false;
+
+ OpenAPI openAPI = config.customOpenAPI();
+ assertNotNull(openAPI);
+ }
+
+ @Test
+ void customOpenAPI_keycloakEnabled_shouldConfigureOAuth2() {
+ OpenApiConfig config = new OpenApiConfig();
+ config.environment = environment;
+ config.servletContext = servletContext;
+ config.keycloakEnabled = true;
OpenAPI openAPI = config.customOpenAPI();
@@ -70,11 +82,11 @@ void customOpenAPITest() {
assertEquals(1, openAPI.getServers().size());
assertEquals("/entando-de-app", openAPI.getServers().get(0).getUrl());
- // Check security configuration with Keycloak enabled
+ // Check OAuth2 security configuration when Keycloak is enabled
assertNotNull(openAPI.getComponents());
assertNotNull(openAPI.getComponents().getSecuritySchemes());
- SecurityScheme securityScheme = openAPI.getComponents().getSecuritySchemes().get("entando");
- assertNotNull(securityScheme);
+ SecurityScheme securityScheme = openAPI.getComponents().getSecuritySchemes().get("oauth2");
+ assertNotNull(securityScheme, "OAuth2 security scheme should be configured when Keycloak is enabled");
assertEquals(SecurityScheme.Type.OAUTH2, securityScheme.getType());
assertNotNull(securityScheme.getFlows());
assertNotNull(securityScheme.getFlows().getAuthorizationCode());
@@ -87,23 +99,59 @@ void customOpenAPITest() {
assertNotNull(openAPI.getSecurity());
assertEquals(1, openAPI.getSecurity().size());
SecurityRequirement securityRequirement = openAPI.getSecurity().get(0);
- assertTrue(securityRequirement.containsKey("entando"));
+ assertTrue(securityRequirement.containsKey("oauth2"));
+ }
+
+ @Test
+ void customOpenAPI_keycloakDisabled_shouldConfigureBasicAuth() {
+ OpenApiConfig config = new OpenApiConfig();
+ config.environment = environment;
+ config.servletContext = servletContext;
+ config.keycloakEnabled = false;
+
+ OpenAPI openAPI = config.customOpenAPI();
+
+ assertNotNull(openAPI);
+ Info info = openAPI.getInfo();
+ assertNotNull(info);
+ assertEquals("Entando API", info.getTitle());
+
+ // Check server configuration
+ assertNotNull(openAPI.getServers());
+ assertEquals(1, openAPI.getServers().size());
+ assertEquals("/entando-de-app", openAPI.getServers().get(0).getUrl());
+
+ // Check Basic Auth security configuration when Keycloak is disabled
+ assertNotNull(openAPI.getComponents());
+ assertNotNull(openAPI.getComponents().getSecuritySchemes());
+ SecurityScheme securityScheme = openAPI.getComponents().getSecuritySchemes().get("basicAuth");
+ assertNotNull(securityScheme, "Basic Auth security scheme should be configured when Keycloak is disabled");
+ assertEquals(SecurityScheme.Type.HTTP, securityScheme.getType());
+ assertEquals("basic", securityScheme.getScheme());
+ assertNotNull(securityScheme.getDescription());
+
+ // Check security requirement
+ assertNotNull(openAPI.getSecurity());
+ assertEquals(1, openAPI.getSecurity().size());
+ SecurityRequirement securityRequirement = openAPI.getSecurity().get(0);
+ assertTrue(securityRequirement.containsKey("basicAuth"));
}
@Test
- void customOpenAPIWithoutAuthServer() {
+ void customOpenAPI_keycloakEnabled_withoutAuthServer_shouldNotConfigureSecurity() {
when(environment.getProperty(SystemConstants.SYSTEM_PROP_KEYCLOAK_AUTH_URL)).thenReturn(null);
when(servletContext.getContextPath()).thenReturn("/entando-de-app");
OpenApiConfig config = new OpenApiConfig();
config.environment = environment;
config.servletContext = servletContext;
+ config.keycloakEnabled = true;
OpenAPI openAPI = config.customOpenAPI();
assertNotNull(openAPI);
- // Security should not be configured when auth server is null
+ // Security should not be configured when auth server is null (even if Keycloak is enabled)
if (openAPI.getComponents() != null) {
assertNull(openAPI.getComponents().getSecuritySchemes());
}
@@ -117,6 +165,7 @@ void customOpenAPIWithEmptyContextPath() {
OpenApiConfig config = new OpenApiConfig();
config.environment = environment;
config.servletContext = servletContext;
+ config.keycloakEnabled = true;
OpenAPI openAPI = config.customOpenAPI();
diff --git a/jakarta-ee10-compatibility-matrix.md b/jakarta-ee10-compatibility-matrix.md
deleted file mode 100644
index fe28bdc02a..0000000000
--- a/jakarta-ee10-compatibility-matrix.md
+++ /dev/null
@@ -1,207 +0,0 @@
-# Jakarta EE 10 Compatibility Matrix for Entando App Engine
-
-**Generated**: September 22, 2025
-**Project**: Entando App Engine v7.5.0
-**Current State**: Java EE 8 (javax.* namespace)
-**Target**: Jakarta EE 10 (jakarta.* namespace)
-
-## Executive Summary
-
-- **Total Modules**: 14 (1 parent + 13 sub-modules)
-- **Test Files**: 578 test files with 3,346 test annotations
-- **Critical Dependencies**: 23 major libraries requiring updates
-- **Risk Level**: HIGH (Major framework upgrade required)
-
-## Core Framework Dependencies
-
-### Spring Ecosystem (CRITICAL - Major Version Jump Required)
-
-| Component | Current Version | Target Version | Compatibility | Risk Level | Notes |
-|-----------|----------------|----------------|---------------|------------|-------|
-| Spring Framework | 5.3.39 | 6.2.1 | ❌ Breaking | CRITICAL | Major version jump, requires Jakarta EE namespace |
-| Spring Security | 5.8.16 | 6.3.1 | ❌ Breaking | CRITICAL | Depends on Spring 6.x |
-| Spring Security OAuth2 | 2.5.2.RELEASE | 6.3.1 | ❌ Breaking | CRITICAL | Major architectural changes |
-| Spring Data Redis | 2.5.12 | 3.2.1 | ❌ Breaking | HIGH | Requires Spring 6.x |
-| Spring Data Commons | 2.5.3 | 3.2.1 | ❌ Breaking | HIGH | Requires Spring 6.x |
-| Spring Data REST | 3.5.6 | 4.2.1 | ❌ Breaking | HIGH | Requires Spring 6.x |
-| Spring Session Data Redis | 2.7.4 | 3.2.1 | ❌ Breaking | HIGH | Requires Spring 6.x |
-| Spring HATEOAS | 1.5.6 | 2.2.1 | ❌ Breaking | MEDIUM | API changes expected |
-
-### Web & Servlet Stack
-
-| Component | Current Version | Target Version | Compatibility | Risk Level | Notes |
-|-----------|----------------|----------------|---------------|------------|-------|
-| Servlet API | javax.servlet-api 4.0.1 | jakarta.servlet-api 6.0.0 | ❌ Breaking | CRITICAL | Namespace change required |
-| JSP API | jsp-api 2.2 | jakarta.servlet.jsp-api 3.1.1 | ❌ Breaking | HIGH | Namespace + version jump |
-| EL API | javax.el-api 3.0.0 | jakarta.el-api 5.0.1 | ❌ Breaking | HIGH | Namespace + version jump |
-| Annotation API | javax.annotation-api 1.3.2 | jakarta.annotation-api 2.1.1 | ❌ Breaking | HIGH | Namespace change |
-| Tomcat Servlet API | 10.0.8 | 10.1.33 | ⚠️ Partial | MEDIUM | Already Jakarta EE 9, needs 10 |
-
-### Struts2 & MVC
-
-| Component | Current Version | Target Version | Compatibility | Risk Level | Notes |
-|-----------|----------------|----------------|---------------|------------|-------|
-| Struts2 Core | 6.7.4 | 6.7.4+ | ✅ Compatible | LOW | Should work with Jakarta EE 10 |
-| Struts2 Spring Plugin | 6.7.4 | 6.7.4+ | ⚠️ Depends | MEDIUM | Requires Spring 6 compatibility testing |
-| Struts2 Tiles Plugin | 6.7.4 | 6.7.4+ | ✅ Compatible | LOW | No direct Jakarta EE dependency |
-| Struts2 JSON Plugin | 6.7.4 | 6.7.4+ | ✅ Compatible | LOW | No direct Jakarta EE dependency |
-
-### Validation & Data Binding
-
-| Component | Current Version | Target Version | Compatibility | Risk Level | Notes |
-|-----------|----------------|----------------|---------------|------------|-------|
-| Hibernate Validator | 6.0.20.Final | 8.0.1.Final | ❌ Breaking | HIGH | Major version jump for Jakarta EE 10 |
-| Validation API | javax.validation 2.0.1.Final | jakarta.validation-api 3.0.2 | ❌ Breaking | HIGH | Namespace change |
-| Jackson Core | 2.16.2 | 2.18.1 | ✅ Compatible | LOW | Should work with Jakarta EE 10 |
-| Jackson Databind | 2.16.2 | 2.18.1 | ✅ Compatible | LOW | Should work with Jakarta EE 10 |
-
-### API & Documentation
-
-| Component | Current Version | Target Version | Compatibility | Risk Level | Notes |
-|-----------|----------------|----------------|---------------|------------|-------|
-| JAX-RS API | jakarta.ws.rs-api 2.1.6 | jakarta.ws.rs-api 3.1.0 | ⚠️ Minor | MEDIUM | Already Jakarta, needs version update |
-| JAXB API | javax.xml.bind 2.3.1 | jakarta.xml.bind-api 4.0.0 | ❌ Breaking | HIGH | Namespace + major version |
-| SpringFox Swagger2 | 2.10.5 | SpringDoc 2.x | ❌ Breaking | HIGH | SpringFox deprecated, migrate to SpringDoc |
-
-## Module-Specific Analysis
-
-### Core Modules
-
-| Module | Java EE Dependencies | Jakarta Ready | Risk Level | Migration Effort |
-|--------|---------------------|---------------|------------|------------------|
-| **engine** | Heavy (servlet, annotation, validation) | ❌ No | CRITICAL | HIGH - Core module with extensive Spring usage |
-| **webapp** | Heavy (servlet, JSP, EL) | ❌ No | HIGH | HIGH - Main assembly with all integrations |
-| **admin-console** | Medium (servlet, JSP) | ❌ No | HIGH | MEDIUM - Admin interface dependencies |
-| **portal-ui** | Medium (servlet, filters) | ❌ No | HIGH | MEDIUM - UI components and filters |
-
-### Plugin Modules
-
-| Plugin | Primary Dependencies | Jakarta Ready | Risk Level | Migration Effort |
-|--------|---------------------|---------------|------------|------------------|
-| **keycloak-plugin** | Spring Security, Servlet | ❌ No | CRITICAL | HIGH - OAuth/Security integration |
-| **cms-plugin** | JAX-RS, Validation, Servlet | ❌ No | HIGH | HIGH - Content management APIs |
-| **mail-plugin** | Spring, Validation | ❌ No | MEDIUM | MEDIUM - Email functionality |
-| **redis-plugin** | Spring Data Redis, Session | ❌ No | HIGH | MEDIUM - Session management |
-| **seo-plugin** | Servlet, Validation | ❌ No | MEDIUM | MEDIUM - SEO features |
-| **versioning-plugin** | Spring, Servlet | ❌ No | MEDIUM | MEDIUM - Content versioning |
-| **cds-plugin** | Servlet, JAX-RS | ❌ No | MEDIUM | MEDIUM - Content delivery |
-| **solr-plugin** | Spring, Servlet | ❌ No | MEDIUM | MEDIUM - Search functionality |
-| **contentscheduler-plugin** | Spring, Quartz | ❌ No | MEDIUM | MEDIUM - Job scheduling |
-
-## Third-Party Library Impact
-
-### High Risk Libraries (Require Major Updates)
-- **Spring Framework**: Complete ecosystem migration needed
-- **Hibernate Validator**: Major version jump (6.x → 8.x)
-- **SpringFox**: Migration to SpringDoc required
-- **Spring Security OAuth2**: Architecture changes
-
-### Medium Risk Libraries (Version Updates)
-- **JAX-RS**: Already Jakarta, needs version update
-- **JAXB**: Namespace change required
-- **Tomcat**: Minor version update for Jakarta EE 10
-
-### Low Risk Libraries (Should be Compatible)
-- **Jackson**: Modern versions support Jakarta EE
-- **Struts2**: Framework is Jakarta EE 10 compatible
-- **Apache Commons**: No direct Jakarta EE dependencies
-- **Logging (Logback/SLF4J)**: No direct Jakarta EE dependencies
-
-## Testing Impact Analysis
-
-### Current Test Coverage
-- **Total Test Files**: 578
-- **Test Annotations**: 3,346 (@Test, @IntegrationTest, @SpringBootTest)
-- **Test Structure**: Standard Maven test structure (src/test/java)
-
-### Testing Migration Requirements
-
-| Test Category | Count (Estimated) | Migration Effort | Risk Level |
-|---------------|------------------|------------------|------------|
-| Unit Tests | ~2,500 | LOW | Tests should mostly work after namespace migration |
-| Integration Tests | ~600 | HIGH | Spring context configuration changes required |
-| Web Tests | ~200 | HIGH | Servlet/MockMvc configuration updates needed |
-| Security Tests | ~46 | CRITICAL | Spring Security 6 has major changes |
-
-### Test Dependencies Requiring Updates
-- **Spring Test**: 5.3.39 → 6.2.1
-- **Spring Boot Test**: If used, requires 3.x
-- **MockMvc**: Spring 6 changes
-- **Security Test**: Spring Security 6 changes
-
-## Migration Timeline Estimates
-
-### Phase 1: Core Framework (4 weeks)
-- Spring Framework 6.2 migration
-- Basic compilation fixes
-- Core module updates
-
-### Phase 2: Namespace Migration (4 weeks)
-- Automated javax → jakarta conversion
-- Manual verification and fixes
-- Compilation error resolution
-
-### Phase 3: Plugin Updates (4 weeks)
-- Individual plugin migrations
-- Plugin integration testing
-- Cross-plugin compatibility verification
-
-### Phase 4: Testing & Validation (4 weeks)
-- Test framework updates
-- Integration test fixes
-- End-to-end validation
-
-## Risk Mitigation Strategies
-
-### Critical Risks
-1. **Spring Framework Breaking Changes**
- - Create compatibility layer where possible
- - Extensive integration testing
- - Phased rollout approach
-
-2. **Test Suite Failures**
- - Update test frameworks in parallel
- - Maintain test coverage metrics
- - Create new tests for Jakarta EE 10 features
-
-3. **Plugin Incompatibilities**
- - Test plugins independently
- - Maintain plugin isolation
- - Create fallback mechanisms
-
-### Recommended Approach
-1. **Parallel Development**: Maintain current branch while developing migration
-2. **Incremental Testing**: Test each module independently
-3. **Rollback Plan**: Maintain ability to revert at any phase
-4. **Documentation**: Document all breaking changes and workarounds
-
-## Success Metrics
-
-### Technical Metrics
-- ✅ All 578 test files pass
-- ✅ All 14 modules compile successfully
-- ✅ All 9 plugins function correctly
-- ✅ Performance regression < 5%
-- ✅ Memory usage regression < 10%
-
-### Functional Metrics
-- ✅ All REST APIs maintain compatibility
-- ✅ Web interface functions correctly
-- ✅ Authentication/authorization works
-- ✅ Plugin activation/deactivation works
-- ✅ Database operations function correctly
-
----
-
-**Legend**:
-- ✅ Compatible: No migration required
-- ⚠️ Partial: Minor updates required
-- ❌ Breaking: Major migration required
-
-**Risk Levels**:
-- **CRITICAL**: Project-blocking issues
-- **HIGH**: Significant development effort required
-- **MEDIUM**: Moderate effort with some risk
-- **LOW**: Minimal effort, low risk
-
-**Next Steps**: Begin with Phase 1 (Core Framework Migration) after stakeholder approval.
\ No newline at end of file
diff --git a/keycloak-plugin/README.md b/keycloak-plugin/README.md
index b43b7c72a4..9a23ba8e22 100644
--- a/keycloak-plugin/README.md
+++ b/keycloak-plugin/README.md
@@ -16,6 +16,7 @@ This plugin doesn't come with Role and Group management, because Entando Core ro
## Properties
>- `keycloak.enabled`: Enables this plugin. (The default is `false`)
+> > ⚠️ **WARNING:** When `keycloak.enabled=false`, Basic Authentication is used instead. This is **UNSAFE FOR PRODUCTION** and should only be used for local development.
>- `keycloak.auth.url`: It's the Keycloak auth url. Example: `https://is.yourdomain.com/auth`. (The default is `http://localhost:8081/auth`)
>- `keycloak.realm`: The keycloak realm. See https://www.keycloak.org/docs/3.2/server_admin/topics/overview/concepts.html . (The default is `entando`)
>- `keycloak.client.id`: The keycloak confidential client id. (The default is `entando-app`)
diff --git a/keycloak-plugin/keycloak/docker-compose.yml b/keycloak-plugin/keycloak/docker-compose.yml
index fb605544ec..e984aee710 100644
--- a/keycloak-plugin/keycloak/docker-compose.yml
+++ b/keycloak-plugin/keycloak/docker-compose.yml
@@ -1,40 +1,21 @@
-version: '2.1'
+version: '3.8'
services:
- database:
- container_name: database
- hostname: database
- image: "mysql:5.7"
+ keycloak-dev:
+ image: entando/entando-keycloak:v7.3.1-fix.3
+ container_name: dev-docker-keycloak24
ports:
- - "3307:3306"
+ - "127.0.0.1:9080:8080"
+ - "127.0.0.1:9443:9443"
+ - "127.0.0.1:10990:10990"
environment:
- - MYSQL_USER=keycloak
- - MYSQL_PASSWORD=password
- - MYSQL_ROOT_PASSWORD=password
- healthcheck:
- test: "/usr/bin/mysql --user=root --password=password --execute \"CREATE DATABASE IF NOT EXISTS keycloak; GRANT ALL ON keycloak.* TO 'keycloak'@'%';\""
- interval: 3s
- timeout: 1s
- retries: 10
+ - KEYCLOAK_USER=admin
+ - KEYCLOAK_PASSWORD=admin
+ - DB_VENDOR=dev-file
+ entrypoint: ["/bin/bash", "-c", "source /opt/convert-vars.sh && mkdir -p /opt/keycloak/data/import && cp /tmp/realm-export.json /opt/keycloak/data/import/ && /opt/keycloak/bin/kc.sh start --import-realm"]
+ volumes:
+ - ./realm-export.json:/tmp/realm-export.json:ro
+ - keycloak-data:/opt/keycloak/data
- keycloak:
-# image: entando/keycloak
- image: jboss/keycloak
- hostname: keycloak
- ports:
- - "8081:8080"
- environment:
- KEYCLOAK_USER: entando-admin
- KEYCLOAK_PASSWORD: qwe123
- DB_ADDR: database
- DB_PORT: 3306
- DB_DATABASE: keycloak
- DB_PASSWORD: password
- DB_USER: keycloak
- DB_VENDOR: mysql
- PROXY_ADDRESS_FORWARDING: "true"
- depends_on:
- database:
- condition: service_healthy
- links:
- - database:database
\ No newline at end of file
+volumes:
+ keycloak-data:
\ No newline at end of file
diff --git a/keycloak-plugin/keycloak/realm-export.json b/keycloak-plugin/keycloak/realm-export.json
index 57006507ce..90f6d67f39 100644
--- a/keycloak-plugin/keycloak/realm-export.json
+++ b/keycloak-plugin/keycloak/realm-export.json
@@ -415,6 +415,39 @@
}
},
"groups": [],
+ "users": [
+ {
+ "id": "admin-user-id",
+ "username": "admin",
+ "enabled": true,
+ "emailVerified": true,
+ "firstName": "Admin",
+ "lastName": "User",
+ "email": "admin@entando.local",
+ "credentials": [
+ {
+ "type": "password",
+ "value": "admin",
+ "temporary": false
+ }
+ ],
+ "realmRoles": ["offline_access", "uma_authorization"],
+ "clientRoles": {
+ "entando-core": ["superuser"],
+ "account": ["manage-account", "view-profile"]
+ }
+ },
+ {
+ "id": "service-account-entando-core-id",
+ "username": "service-account-entando-core",
+ "enabled": true,
+ "serviceAccountClientId": "entando-core",
+ "realmRoles": ["offline_access", "uma_authorization"],
+ "clientRoles": {
+ "realm-management": ["realm-admin"]
+ }
+ }
+ ],
"defaultRoles": [
"uma_authorization",
"offline_access"
@@ -627,7 +660,7 @@
"redirectUris": [
"http://localhost:8080/*"
],
- "webOrigins": [],
+ "webOrigins": ["http://localhost:8080"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
diff --git a/keycloak-plugin/src/main/java/org/entando/entando/aps/servlet/security/BasicAuthFilter.java b/keycloak-plugin/src/main/java/org/entando/entando/aps/servlet/security/BasicAuthFilter.java
new file mode 100644
index 0000000000..a91a115680
--- /dev/null
+++ b/keycloak-plugin/src/main/java/org/entando/entando/aps/servlet/security/BasicAuthFilter.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2022-Present Entando S.r.l. (http://www.entando.com) All rights reserved.
+ *
+ * This library is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+ * details.
+ */
+package org.entando.entando.aps.servlet.security;
+
+import com.agiletec.aps.system.SystemConstants;
+import com.agiletec.aps.system.services.user.IAuthenticationProviderManager;
+import com.agiletec.aps.system.services.user.IUserManager;
+import com.agiletec.aps.system.services.user.UserDetails;
+import com.agiletec.aps.util.ApsTenantApplicationUtils;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.commons.lang3.StringUtils;
+import org.entando.entando.ent.exception.EntException;
+import org.entando.entando.ent.util.EntLogging;
+import org.entando.entando.web.common.model.RestError;
+import org.entando.entando.web.common.model.RestResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import static org.entando.entando.aps.servlet.security.KeycloakSecurityConfig.API_PATH;
+
+/**
+ * Authentication filter that validates Basic Auth credentials against the database.
+ * Used when Keycloak is disabled (keycloak.enabled=false).
+ */
+public class BasicAuthFilter extends AbstractAuthenticationProcessingFilter implements AuthenticationFailureHandler {
+
+ private static final EntLogging.EntLogger log = EntLogging.EntLogFactory.getSanitizedLogger(BasicAuthFilter.class);
+
+ private final ObjectMapper objectMapper;
+ private final IUserManager userManager;
+ private final IAuthenticationProviderManager authenticationProviderManager;
+
+ public BasicAuthFilter(final IUserManager userManager,
+ final IAuthenticationProviderManager authenticationProviderManager) {
+ super("/api/**");
+ this.objectMapper = new ObjectMapper();
+ this.userManager = userManager;
+ this.authenticationProviderManager = authenticationProviderManager;
+ this.setAuthenticationManager(authenticationProviderManager);
+ }
+
+ @Override
+ public Authentication attemptAuthentication(final HttpServletRequest request,
+ final HttpServletResponse response) throws AuthenticationException {
+ ApsTenantApplicationUtils.extractCurrentTenantCode(request)
+ .filter(StringUtils::isNotBlank)
+ .ifPresentOrElse(ApsTenantApplicationUtils::setTenant, ApsTenantApplicationUtils::removeTenant);
+ log.warn("Keycloak disabled, handling Basic Auth");
+ final String authorization = request.getHeader("Authorization");
+
+ // No Authorization header - return guest
+ if (authorization == null || authorization.isBlank()) {
+ return createGuestAuthentication(request);
+ }
+
+ // Handle Basic Auth
+ if (authorization.toLowerCase().startsWith("basic ")) {
+ return handleBasicAuth(request, authorization);
+ }
+
+ // Handle Bearer token (validate against DB-stored tokens)
+ if (authorization.toLowerCase().startsWith("bearer ")) {
+ return handleBearerToken(request, authorization);
+ }
+
+ // Unknown auth scheme - return guest
+ return createGuestAuthentication(request);
+ }
+
+ private Authentication handleBasicAuth(HttpServletRequest request, String authorization) {
+ try {
+ String base64Credentials = authorization.substring("Basic ".length()).trim();
+ byte[] decodedBytes = Base64.getDecoder().decode(base64Credentials);
+ String credentials = new String(decodedBytes, StandardCharsets.UTF_8);
+
+ int colonIndex = credentials.indexOf(':');
+ if (colonIndex == -1) {
+ throw new BadCredentialsException("Invalid Basic Auth format");
+ }
+
+ String username = credentials.substring(0, colonIndex);
+ String password = credentials.substring(colonIndex + 1);
+
+ if (username.isBlank() || password.isBlank()) {
+ throw new BadCredentialsException("Username and password are required");
+ }
+
+ // Validate credentials against the database
+ UserDetails user = authenticationProviderManager.getUser(username, password);
+
+ if (user == null) {
+ throw new BadCredentialsException("Invalid username or password");
+ }
+
+ if (user.isDisabled()) {
+ throw new BadCredentialsException("User account is disabled");
+ }
+
+ if (!user.isAccountNotExpired()) {
+ throw new BadCredentialsException("User account has expired");
+ }
+
+ log.warn("Successfully authenticated user '{}' via Basic Auth", username);
+
+ UserAuthentication userAuthentication = new UserAuthentication(user);
+ setUserOnContext(request, user, userAuthentication);
+ return userAuthentication;
+
+ } catch (EntException e) {
+ log.error("Error during Basic Auth authentication", e);
+ throw new BadCredentialsException("Authentication error", e);
+ } catch (IllegalArgumentException e) {
+ log.warn("Invalid Base64 in Basic Auth header");
+ throw new BadCredentialsException("Invalid Basic Auth encoding");
+ }
+ }
+
+ private Authentication handleBearerToken(HttpServletRequest request, String authorization) {
+ try {
+ String token = authorization.substring("Bearer ".length()).trim();
+
+ // Try to find user by access token in the database
+ UserDetails user = findUserByAccessToken(token);
+
+ if (user == null) {
+ log.debug("No valid user found for Bearer token");
+ throw new BadCredentialsException("Invalid or expired token");
+ }
+
+ log.debug("Successfully authenticated user '{}' via Bearer token", user.getUsername());
+
+ UserAuthentication userAuthentication = new UserAuthentication(user);
+ setUserOnContext(request, user, userAuthentication);
+ return userAuthentication;
+
+ } catch (EntException e) {
+ log.error("Error during Bearer token authentication", e);
+ throw new BadCredentialsException("Authentication error", e);
+ }
+ }
+
+ private UserDetails findUserByAccessToken(String token) throws EntException {
+ // The token was generated by AuthenticationProviderManager.extractUser()
+ // which stores it in the OAuth2 token manager
+ // We need to look up the user by their stored access token
+ // For now, we'll try to get the user from the token manager
+ // This requires the token to be valid and stored in the database
+
+ // Note: This is a simplified implementation. In a production scenario,
+ // you might want to validate the token through the OAuth2 token manager
+ // and extract the username from there.
+ return null; // Bearer tokens require Keycloak validation - return null to reject
+ }
+
+ private Authentication createGuestAuthentication(HttpServletRequest request) {
+ UserDetails guestUser = userManager.getGuestUser();
+ GuestAuthentication guestAuthentication = new GuestAuthentication(guestUser);
+ setUserOnContext(request, guestUser, guestAuthentication);
+ return guestAuthentication;
+ }
+
+ private void setUserOnContext(HttpServletRequest request, UserDetails user, Authentication authentication) {
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ if (API_PATH.equals(request.getServletPath())) {
+ request.setAttribute("user", user);
+ } else {
+ request.getSession().setAttribute("user", user);
+ request.getSession().setAttribute(SystemConstants.SESSIONPARAM_CURRENT_USER, user);
+ }
+ }
+
+ @Override
+ protected void successfulAuthentication(final HttpServletRequest request,
+ final HttpServletResponse response,
+ final FilterChain chain,
+ final Authentication authResult) throws IOException, ServletException {
+ chain.doFilter(request, response);
+ }
+
+ @Override
+ protected void unsuccessfulAuthentication(final HttpServletRequest request,
+ final HttpServletResponse response,
+ final AuthenticationException failed) throws IOException {
+ this.onAuthenticationFailure(request, response, failed);
+ }
+
+ @Override
+ public void onAuthenticationFailure(HttpServletRequest request,
+ HttpServletResponse response,
+ AuthenticationException exception) throws IOException {
+ final RestResponse restResponse = new RestResponse<>(null, null);
+ restResponse.addError(new RestError(HttpStatus.UNAUTHORIZED.toString(), exception.getMessage()));
+
+ response.setStatus(HttpStatus.UNAUTHORIZED.value());
+ response.addHeader("Content-Type", "application/json");
+ response.getOutputStream().println(objectMapper.writeValueAsString(restResponse));
+ }
+}
\ No newline at end of file
diff --git a/keycloak-plugin/src/main/java/org/entando/entando/aps/servlet/security/KeycloakSecurityConfig.java b/keycloak-plugin/src/main/java/org/entando/entando/aps/servlet/security/KeycloakSecurityConfig.java
index d10057b3ea..ee73d768f5 100644
--- a/keycloak-plugin/src/main/java/org/entando/entando/aps/servlet/security/KeycloakSecurityConfig.java
+++ b/keycloak-plugin/src/main/java/org/entando/entando/aps/servlet/security/KeycloakSecurityConfig.java
@@ -1,14 +1,14 @@
package org.entando.entando.aps.servlet.security;
+import com.agiletec.aps.system.services.user.IAuthenticationProviderManager;
+import com.agiletec.aps.system.services.user.IUserManager;
import org.apache.commons.lang3.StringUtils;
-import org.entando.entando.aps.system.services.userprofile.api.ApiMyUserProfileInterface;
import org.entando.entando.ent.util.EntLogging;
import org.entando.entando.keycloak.services.KeycloakConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
-import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
@@ -29,12 +29,18 @@ public class KeycloakSecurityConfig extends AuthorizationServerConfiguration {
private final KeycloakAuthenticationFilter keycloakAuthenticationFilter;
private final KeycloakConfiguration configuration;
+ private final IUserManager userManager;
+ private final IAuthenticationProviderManager authenticationProviderManager;
@Autowired
public KeycloakSecurityConfig(final KeycloakAuthenticationFilter keycloakAuthenticationFilter,
- final KeycloakConfiguration configuration) {
+ final KeycloakConfiguration configuration,
+ final IUserManager userManager,
+ final IAuthenticationProviderManager authenticationProviderManager) {
this.keycloakAuthenticationFilter = keycloakAuthenticationFilter;
this.configuration = configuration;
+ this.userManager = userManager;
+ this.authenticationProviderManager = authenticationProviderManager;
}
@Bean
@@ -74,7 +80,33 @@ public SecurityFilterChain keycloakSecurityFilterChain(HttpSecurity http) throws
.cors(cors -> cors.configurationSource(corsConfigurationSource()));
return http.build();
} else {
- return super.filterChain(http);
+ // Keycloak disabled - use BasicAuthFilter for DB authentication
+ _logger.warn("▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒");
+ _logger.warn("▒▒▒ [SECURITY] Keycloak disabled; using Basic Auth. UNSAFE FOR PRODUCTION. ▒▒▒");
+ _logger.warn("▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒");
+
+ BasicAuthFilter basicAuthFilter = new BasicAuthFilter(userManager, authenticationProviderManager);
+
+ http.authorizeHttpRequests(authorize -> {
+ authorize
+ .requestMatchers(new AntPathRequestMatcher("/api/health")).permitAll()
+ .requestMatchers(new AntPathRequestMatcher("/api/v3/api-docs")).permitAll()
+ .requestMatchers(new AntPathRequestMatcher("/api/v3/api-docs/**")).permitAll()
+ .requestMatchers(new AntPathRequestMatcher("/api/swagger-ui/**")).permitAll()
+ .requestMatchers(new AntPathRequestMatcher("/api/swagger-ui.html")).permitAll()
+ .requestMatchers(new AntPathRequestMatcher("/api/webjars/**")).permitAll()
+ .requestMatchers(new AntPathRequestMatcher("/api/**")).authenticated()
+ .anyRequest().permitAll();
+ });
+
+ http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
+ .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
+ .addFilterBefore(basicAuthFilter, BasicAuthenticationFilter.class)
+ .anonymous(AbstractHttpConfigurer::disable)
+ .csrf(AbstractHttpConfigurer::disable) //NOSONAR
+ .cors(cors -> cors.configurationSource(corsConfigurationSource()));
+
+ return http.build();
}
}
diff --git a/keycloak-plugin/src/test/java/org/entando/entando/aps/servlet/security/BasicAuthFilterTest.java b/keycloak-plugin/src/test/java/org/entando/entando/aps/servlet/security/BasicAuthFilterTest.java
new file mode 100644
index 0000000000..ae2dcb8f70
--- /dev/null
+++ b/keycloak-plugin/src/test/java/org/entando/entando/aps/servlet/security/BasicAuthFilterTest.java
@@ -0,0 +1,438 @@
+/*
+ * Copyright 2022-Present Entando S.r.l. (http://www.entando.com) All rights reserved.
+ *
+ * This library is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+ * details.
+ */
+package org.entando.entando.aps.servlet.security;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+
+import com.agiletec.aps.system.SystemConstants;
+import com.agiletec.aps.system.services.user.IAuthenticationProviderManager;
+import com.agiletec.aps.system.services.user.IUserManager;
+import com.agiletec.aps.system.services.user.User;
+import com.agiletec.aps.system.services.user.UserDetails;
+import com.google.common.net.HttpHeaders;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import org.entando.entando.aps.system.services.tenants.ITenantManager;
+import org.entando.entando.aps.util.UrlUtils;
+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.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.context.support.WebApplicationContextUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+@ExtendWith(MockitoExtension.class)
+class BasicAuthFilterTest {
+
+ private static final String USERNAME = "testuser";
+ private static final String PASSWORD = "testpassword";
+
+ @Mock
+ private IUserManager userManager;
+ @Mock
+ private IAuthenticationProviderManager authenticationProviderManager;
+ @Mock
+ private HttpServletRequest request;
+ @Mock
+ private HttpServletResponse response;
+ @Mock
+ private HttpSession session;
+ @Mock
+ private ServletContext servletContext;
+ @Mock
+ private WebApplicationContext webApplicationContext;
+ @Mock
+ private ITenantManager tenantManager;
+ @Mock
+ private FilterChain filterChain;
+ @Mock
+ private ServletOutputStream outputStream;
+
+ private BasicAuthFilter basicAuthFilter;
+
+ @BeforeEach
+ void setUp() {
+ basicAuthFilter = new BasicAuthFilter(userManager, authenticationProviderManager);
+
+ Mockito.lenient().when(request.getSession()).thenReturn(session);
+ Mockito.lenient().when(request.getHeader(UrlUtils.ENTANDO_TENANT_CODE_CUSTOM_HEADER)).thenReturn(null);
+ Mockito.lenient().when(request.getHeader(HttpHeaders.X_FORWARDED_HOST)).thenReturn(null);
+ Mockito.lenient().when(request.getHeader(HttpHeaders.HOST)).thenReturn("localhost");
+ Mockito.lenient().when(request.getServerName()).thenReturn("localhost");
+ Mockito.lenient().when(session.getServletContext()).thenReturn(servletContext);
+ Mockito.lenient().when(webApplicationContext.getBean(ITenantManager.class)).thenReturn(tenantManager);
+ }
+
+ @Test
+ void attemptAuthentication_noAuthorizationHeader_shouldReturnGuestAuthentication() {
+ when(request.getHeader("Authorization")).thenReturn(null);
+ when(request.getServletPath()).thenReturn("/api");
+
+ User guestUser = createGuestUser();
+ when(userManager.getGuestUser()).thenReturn(guestUser);
+
+ try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) {
+ wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext);
+
+ Authentication auth = basicAuthFilter.attemptAuthentication(request, response);
+
+ assertNotNull(auth);
+ assertInstanceOf(GuestAuthentication.class, auth);
+ assertFalse(auth.isAuthenticated());
+ verify(request).setAttribute(eq("user"), any());
+ }
+ }
+
+ @Test
+ void attemptAuthentication_emptyAuthorizationHeader_shouldReturnGuestAuthentication() {
+ when(request.getHeader("Authorization")).thenReturn("");
+ when(request.getServletPath()).thenReturn("/api");
+
+ User guestUser = createGuestUser();
+ when(userManager.getGuestUser()).thenReturn(guestUser);
+
+ try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) {
+ wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext);
+
+ Authentication auth = basicAuthFilter.attemptAuthentication(request, response);
+
+ assertNotNull(auth);
+ assertInstanceOf(GuestAuthentication.class, auth);
+ }
+ }
+
+ @Test
+ void attemptAuthentication_validBasicAuth_shouldReturnUserAuthentication() throws Exception {
+ String credentials = USERNAME + ":" + PASSWORD;
+ String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
+ when(request.getHeader("Authorization")).thenReturn("Basic " + encodedCredentials);
+ when(request.getServletPath()).thenReturn("/api");
+
+ User user = createActiveUser();
+ when(authenticationProviderManager.getUser(USERNAME, PASSWORD)).thenReturn(user);
+
+ try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) {
+ wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext);
+
+ Authentication auth = basicAuthFilter.attemptAuthentication(request, response);
+
+ assertNotNull(auth);
+ assertInstanceOf(UserAuthentication.class, auth);
+ assertTrue(auth.isAuthenticated());
+ assertEquals(user, auth.getPrincipal());
+ verify(request).setAttribute(eq("user"), eq(user));
+ }
+ }
+
+ @Test
+ void attemptAuthentication_validBasicAuthLowercase_shouldReturnUserAuthentication() throws Exception {
+ String credentials = USERNAME + ":" + PASSWORD;
+ String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
+ when(request.getHeader("Authorization")).thenReturn("basic " + encodedCredentials);
+ when(request.getServletPath()).thenReturn("/api");
+
+ User user = createActiveUser();
+ when(authenticationProviderManager.getUser(USERNAME, PASSWORD)).thenReturn(user);
+
+ try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) {
+ wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext);
+
+ Authentication auth = basicAuthFilter.attemptAuthentication(request, response);
+
+ assertNotNull(auth);
+ assertInstanceOf(UserAuthentication.class, auth);
+ assertTrue(auth.isAuthenticated());
+ }
+ }
+
+ @Test
+ void attemptAuthentication_invalidCredentials_shouldThrowBadCredentialsException() throws Exception {
+ String credentials = USERNAME + ":" + PASSWORD;
+ String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
+ when(request.getHeader("Authorization")).thenReturn("Basic " + encodedCredentials);
+
+ when(authenticationProviderManager.getUser(USERNAME, PASSWORD)).thenReturn(null);
+
+ try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) {
+ wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext);
+
+ BadCredentialsException exception = assertThrows(BadCredentialsException.class,
+ () -> basicAuthFilter.attemptAuthentication(request, response));
+
+ assertEquals("Invalid username or password", exception.getMessage());
+ }
+ }
+
+ @Test
+ void attemptAuthentication_disabledUser_shouldThrowBadCredentialsException() throws Exception {
+ String credentials = USERNAME + ":" + PASSWORD;
+ String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
+ when(request.getHeader("Authorization")).thenReturn("Basic " + encodedCredentials);
+
+ User user = createDisabledUser();
+ when(authenticationProviderManager.getUser(USERNAME, PASSWORD)).thenReturn(user);
+
+ try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) {
+ wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext);
+
+ BadCredentialsException exception = assertThrows(BadCredentialsException.class,
+ () -> basicAuthFilter.attemptAuthentication(request, response));
+
+ assertEquals("User account is disabled", exception.getMessage());
+ }
+ }
+
+ @Test
+ void attemptAuthentication_expiredAccount_shouldThrowBadCredentialsException() throws Exception {
+ String credentials = USERNAME + ":" + PASSWORD;
+ String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
+ when(request.getHeader("Authorization")).thenReturn("Basic " + encodedCredentials);
+
+ // Use a mock to control isAccountNotExpired() return value
+ UserDetails user = mock(UserDetails.class);
+ Mockito.lenient().when(user.getUsername()).thenReturn(USERNAME);
+ when(user.isDisabled()).thenReturn(false);
+ when(user.isAccountNotExpired()).thenReturn(false); // Account is expired
+
+ when(authenticationProviderManager.getUser(USERNAME, PASSWORD)).thenReturn(user);
+
+ try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) {
+ wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext);
+
+ BadCredentialsException exception = assertThrows(BadCredentialsException.class,
+ () -> basicAuthFilter.attemptAuthentication(request, response));
+
+ assertEquals("User account has expired", exception.getMessage());
+ }
+ }
+
+ @Test
+ void attemptAuthentication_invalidBase64Encoding_shouldThrowBadCredentialsException() {
+ when(request.getHeader("Authorization")).thenReturn("Basic not-valid-base64!!!");
+
+ try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) {
+ wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext);
+
+ BadCredentialsException exception = assertThrows(BadCredentialsException.class,
+ () -> basicAuthFilter.attemptAuthentication(request, response));
+
+ assertEquals("Invalid Basic Auth encoding", exception.getMessage());
+ }
+ }
+
+ @Test
+ void attemptAuthentication_noColonInCredentials_shouldThrowBadCredentialsException() {
+ String invalidCredentials = "usernameWithoutPassword";
+ String encodedCredentials = Base64.getEncoder().encodeToString(invalidCredentials.getBytes(StandardCharsets.UTF_8));
+ when(request.getHeader("Authorization")).thenReturn("Basic " + encodedCredentials);
+
+ try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) {
+ wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext);
+
+ BadCredentialsException exception = assertThrows(BadCredentialsException.class,
+ () -> basicAuthFilter.attemptAuthentication(request, response));
+
+ assertEquals("Invalid Basic Auth format", exception.getMessage());
+ }
+ }
+
+ @Test
+ void attemptAuthentication_emptyUsername_shouldThrowBadCredentialsException() {
+ String credentials = ":" + PASSWORD;
+ String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
+ when(request.getHeader("Authorization")).thenReturn("Basic " + encodedCredentials);
+
+ try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) {
+ wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext);
+
+ BadCredentialsException exception = assertThrows(BadCredentialsException.class,
+ () -> basicAuthFilter.attemptAuthentication(request, response));
+
+ assertEquals("Username and password are required", exception.getMessage());
+ }
+ }
+
+ @Test
+ void attemptAuthentication_emptyPassword_shouldThrowBadCredentialsException() {
+ String credentials = USERNAME + ":";
+ String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
+ when(request.getHeader("Authorization")).thenReturn("Basic " + encodedCredentials);
+
+ try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) {
+ wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext);
+
+ BadCredentialsException exception = assertThrows(BadCredentialsException.class,
+ () -> basicAuthFilter.attemptAuthentication(request, response));
+
+ assertEquals("Username and password are required", exception.getMessage());
+ }
+ }
+
+ @Test
+ void attemptAuthentication_bearerToken_shouldThrowBadCredentialsException() {
+ when(request.getHeader("Authorization")).thenReturn("Bearer some-jwt-token");
+
+ try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) {
+ wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext);
+
+ BadCredentialsException exception = assertThrows(BadCredentialsException.class,
+ () -> basicAuthFilter.attemptAuthentication(request, response));
+
+ assertEquals("Invalid or expired token", exception.getMessage());
+ }
+ }
+
+ @Test
+ void attemptAuthentication_unknownAuthScheme_shouldReturnGuestAuthentication() {
+ when(request.getHeader("Authorization")).thenReturn("Digest somevalue");
+ when(request.getServletPath()).thenReturn("/api");
+
+ User guestUser = createGuestUser();
+ when(userManager.getGuestUser()).thenReturn(guestUser);
+
+ try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) {
+ wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext);
+
+ Authentication auth = basicAuthFilter.attemptAuthentication(request, response);
+
+ assertNotNull(auth);
+ assertInstanceOf(GuestAuthentication.class, auth);
+ }
+ }
+
+ @Test
+ void attemptAuthentication_nonApiPath_shouldSetSessionAttributes() throws Exception {
+ String credentials = USERNAME + ":" + PASSWORD;
+ String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
+ when(request.getHeader("Authorization")).thenReturn("Basic " + encodedCredentials);
+ when(request.getServletPath()).thenReturn("/do/something");
+
+ User user = createActiveUser();
+ when(authenticationProviderManager.getUser(USERNAME, PASSWORD)).thenReturn(user);
+
+ try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) {
+ wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext);
+
+ basicAuthFilter.attemptAuthentication(request, response);
+
+ verify(session).setAttribute(eq("user"), eq(user));
+ verify(session).setAttribute(eq(SystemConstants.SESSIONPARAM_CURRENT_USER), eq(user));
+ }
+ }
+
+ @Test
+ void attemptAuthentication_authenticationProviderThrowsException_shouldThrowBadCredentialsException() throws Exception {
+ String credentials = USERNAME + ":" + PASSWORD;
+ String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
+ when(request.getHeader("Authorization")).thenReturn("Basic " + encodedCredentials);
+
+ when(authenticationProviderManager.getUser(USERNAME, PASSWORD)).thenThrow(new EntException("DB error"));
+
+ try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) {
+ wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext);
+
+ BadCredentialsException exception = assertThrows(BadCredentialsException.class,
+ () -> basicAuthFilter.attemptAuthentication(request, response));
+
+ assertEquals("Authentication error", exception.getMessage());
+ }
+ }
+
+ @Test
+ void onAuthenticationFailure_shouldReturnUnauthorizedResponse() throws Exception {
+ when(response.getOutputStream()).thenReturn(outputStream);
+
+ BadCredentialsException exception = new BadCredentialsException("Test error message");
+
+ basicAuthFilter.onAuthenticationFailure(request, response, exception);
+
+ verify(response).setStatus(HttpStatus.UNAUTHORIZED.value());
+ verify(response).addHeader("Content-Type", "application/json");
+ verify(outputStream).println(anyString());
+ }
+
+ @Test
+ void successfulAuthentication_shouldContinueFilterChain() throws Exception {
+ Authentication auth = mock(Authentication.class);
+
+ basicAuthFilter.successfulAuthentication(request, response, filterChain, auth);
+
+ verify(filterChain).doFilter(request, response);
+ }
+
+ @Test
+ void attemptAuthentication_passwordWithColons_shouldParseCorrectly() throws Exception {
+ String passwordWithColons = "pass:word:with:colons";
+ String credentials = USERNAME + ":" + passwordWithColons;
+ String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
+ when(request.getHeader("Authorization")).thenReturn("Basic " + encodedCredentials);
+ when(request.getServletPath()).thenReturn("/api");
+
+ User user = createActiveUser();
+ when(authenticationProviderManager.getUser(USERNAME, passwordWithColons)).thenReturn(user);
+
+ try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) {
+ wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext);
+
+ Authentication auth = basicAuthFilter.attemptAuthentication(request, response);
+
+ assertNotNull(auth);
+ assertInstanceOf(UserAuthentication.class, auth);
+ verify(authenticationProviderManager).getUser(USERNAME, passwordWithColons);
+ }
+ }
+
+ // Helper methods to create test users
+
+ private User createGuestUser() {
+ User user = new User();
+ user.setUsername("guest");
+ return user;
+ }
+
+ private User createActiveUser() {
+ User user = new User();
+ user.setUsername(USERNAME);
+ user.setDisabled(false);
+ return user;
+ }
+
+ private User createDisabledUser() {
+ User user = new User();
+ user.setUsername(USERNAME);
+ user.setDisabled(true);
+ return user;
+ }
+
+}
\ No newline at end of file
diff --git a/keycloak-plugin/src/test/java/org/entando/entando/aps/servlet/security/KeycloakSecurityConfigTest.java b/keycloak-plugin/src/test/java/org/entando/entando/aps/servlet/security/KeycloakSecurityConfigTest.java
index 30d97248be..e8f971ae86 100644
--- a/keycloak-plugin/src/test/java/org/entando/entando/aps/servlet/security/KeycloakSecurityConfigTest.java
+++ b/keycloak-plugin/src/test/java/org/entando/entando/aps/servlet/security/KeycloakSecurityConfigTest.java
@@ -13,19 +13,31 @@
*/
package org.entando.entando.aps.servlet.security;
+import com.agiletec.aps.system.services.user.IAuthenticationProviderManager;
+import com.agiletec.aps.system.services.user.IUserManager;
+import jakarta.servlet.Filter;
import org.entando.entando.keycloak.services.KeycloakConfiguration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.web.DefaultSecurityFilterChain;
+import org.springframework.security.web.SecurityFilterChain;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-//TODO verificare queste modifiche con l'as-is javaxEE8
@ExtendWith(MockitoExtension.class)
class KeycloakSecurityConfigTest {
@@ -34,12 +46,18 @@ class KeycloakSecurityConfigTest {
@Mock
private KeycloakConfiguration configuration;
+
+ @Mock
+ private IUserManager userManager;
+
+ @Mock
+ private IAuthenticationProviderManager authenticationProviderManager;
private KeycloakSecurityConfig securityConfig;
@BeforeEach
public void setUp() {
- this.securityConfig = new KeycloakSecurityConfig(keycloakAuthenticationFilter, configuration);
+ this.securityConfig = new KeycloakSecurityConfig(keycloakAuthenticationFilter, configuration, userManager, authenticationProviderManager);
}
@Test
@@ -95,5 +113,105 @@ void shouldExecuteSessionSettings() {
// which includes the SessionCreationPolicy.ALWAYS setting in the lambda
// This functionally verifies the same behavior as the original test
}
-
+
+
+ @Test
+ void keycloakEnabled_shouldAddKeycloakAuthenticationFilter() throws Exception {
+ // Setup
+ when(configuration.isEnabled()).thenReturn(true);
+ Mockito.lenient().when(configuration.getSecureUris()).thenReturn("");
+
+ HttpSecurity httpSecurity = mock(HttpSecurity.class, Mockito.RETURNS_DEEP_STUBS);
+ DefaultSecurityFilterChain filterChain = mock(DefaultSecurityFilterChain.class);
+
+ when(httpSecurity.authorizeHttpRequests(any(Customizer.class))).thenReturn(httpSecurity);
+ when(httpSecurity.sessionManagement(any(Customizer.class))).thenReturn(httpSecurity);
+ when(httpSecurity.headers(any(Customizer.class))).thenReturn(httpSecurity);
+ when(httpSecurity.addFilterBefore(any(Filter.class), any(Class.class))).thenReturn(httpSecurity);
+ when(httpSecurity.anonymous(any(Customizer.class))).thenReturn(httpSecurity);
+ when(httpSecurity.csrf(any(Customizer.class))).thenReturn(httpSecurity);
+ when(httpSecurity.cors(any(Customizer.class))).thenReturn(httpSecurity);
+ when(httpSecurity.build()).thenReturn(filterChain);
+
+ // Execute
+ SecurityFilterChain result = securityConfig.keycloakSecurityFilterChain(httpSecurity);
+
+ // Verify
+ assertNotNull(result);
+
+ // Capture the filter added to verify it's the KeycloakAuthenticationFilter
+ ArgumentCaptor filterCaptor = ArgumentCaptor.forClass(Filter.class);
+ verify(httpSecurity).addFilterBefore(filterCaptor.capture(), any(Class.class));
+
+ Filter addedFilter = filterCaptor.getValue();
+ assertInstanceOf(KeycloakAuthenticationFilter.class, addedFilter,
+ "When Keycloak is enabled, KeycloakAuthenticationFilter should be added");
+ }
+
+ @Test
+ void keycloakDisabled_shouldAddBasicAuthFilter() throws Exception {
+ // Setup
+ when(configuration.isEnabled()).thenReturn(false);
+
+ HttpSecurity httpSecurity = mock(HttpSecurity.class, Mockito.RETURNS_DEEP_STUBS);
+ DefaultSecurityFilterChain filterChain = mock(DefaultSecurityFilterChain.class);
+
+ when(httpSecurity.authorizeHttpRequests(any(Customizer.class))).thenReturn(httpSecurity);
+ when(httpSecurity.sessionManagement(any(Customizer.class))).thenReturn(httpSecurity);
+ when(httpSecurity.headers(any(Customizer.class))).thenReturn(httpSecurity);
+ when(httpSecurity.addFilterBefore(any(Filter.class), any(Class.class))).thenReturn(httpSecurity);
+ when(httpSecurity.anonymous(any(Customizer.class))).thenReturn(httpSecurity);
+ when(httpSecurity.csrf(any(Customizer.class))).thenReturn(httpSecurity);
+ when(httpSecurity.cors(any(Customizer.class))).thenReturn(httpSecurity);
+ when(httpSecurity.build()).thenReturn(filterChain);
+
+ // Execute
+ SecurityFilterChain result = securityConfig.keycloakSecurityFilterChain(httpSecurity);
+
+ // Verify
+ assertNotNull(result);
+
+ // Capture the filter added to verify it's the BasicAuthFilter
+ ArgumentCaptor filterCaptor = ArgumentCaptor.forClass(Filter.class);
+ verify(httpSecurity).addFilterBefore(filterCaptor.capture(), any(Class.class));
+
+ Filter addedFilter = filterCaptor.getValue();
+ assertInstanceOf(BasicAuthFilter.class, addedFilter,
+ "When Keycloak is disabled, BasicAuthFilter should be added");
+ }
+
+ @Test
+ void keycloakEnabled_shouldConfigureHttpSecurityCorrectly() throws Exception {
+ // Setup
+ when(configuration.isEnabled()).thenReturn(true);
+ Mockito.lenient().when(configuration.getSecureUris()).thenReturn("/api/secure1,/api/secure2");
+
+ HttpSecurity httpSecurity = mock(HttpSecurity.class, Mockito.RETURNS_DEEP_STUBS);
+ DefaultSecurityFilterChain filterChain = mock(DefaultSecurityFilterChain.class);
+
+ when(httpSecurity.authorizeHttpRequests(any(Customizer.class))).thenReturn(httpSecurity);
+ when(httpSecurity.sessionManagement(any(Customizer.class))).thenReturn(httpSecurity);
+ when(httpSecurity.headers(any(Customizer.class))).thenReturn(httpSecurity);
+ when(httpSecurity.addFilterBefore(any(Filter.class), any(Class.class))).thenReturn(httpSecurity);
+ when(httpSecurity.anonymous(any(Customizer.class))).thenReturn(httpSecurity);
+ when(httpSecurity.csrf(any(Customizer.class))).thenReturn(httpSecurity);
+ when(httpSecurity.cors(any(Customizer.class))).thenReturn(httpSecurity);
+ when(httpSecurity.build()).thenReturn(filterChain);
+
+ // Execute
+ SecurityFilterChain result = securityConfig.keycloakSecurityFilterChain(httpSecurity);
+
+ // Verify filter chain was built successfully
+ assertNotNull(result);
+
+ // Verify all HttpSecurity configuration methods were called
+ verify(httpSecurity).authorizeHttpRequests(any(Customizer.class));
+ verify(httpSecurity).sessionManagement(any(Customizer.class));
+ verify(httpSecurity).headers(any(Customizer.class));
+ verify(httpSecurity).anonymous(any(Customizer.class));
+ verify(httpSecurity).csrf(any(Customizer.class));
+ verify(httpSecurity).cors(any(Customizer.class));
+ verify(httpSecurity).build();
+ }
+
}
diff --git a/mail-plugin/src/main/java/com/agiletec/plugins/jpmail/aps/services/mail/MailManager.java b/mail-plugin/src/main/java/com/agiletec/plugins/jpmail/aps/services/mail/MailManager.java
index 83049b1649..ffe8043b5b 100644
--- a/mail-plugin/src/main/java/com/agiletec/plugins/jpmail/aps/services/mail/MailManager.java
+++ b/mail-plugin/src/main/java/com/agiletec/plugins/jpmail/aps/services/mail/MailManager.java
@@ -39,6 +39,8 @@
import jakarta.mail.internet.*;
import java.util.Date;
import java.util.Iterator;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
@@ -83,7 +85,7 @@ private void loadConfigs() throws EntException {
@Override
public MailConfig getMailConfig() throws EntException {
try {
- return (MailConfig) this._config.clone();
+ return (MailConfig) this.getConfig().clone();
} catch (Throwable t) {
throw new EntException("Error loading mail service configuration", t);
}
@@ -250,7 +252,7 @@ protected Session prepareSession(MailConfig config) {
switch (config.getSmtpProtocol()) {
case JpmailSystemConstants.PROTO_SSL:
props.put("mail.smtp.socketFactory.port", port);
- props.put("mail.smtp.socketFactory.class", "jakarta.net.ssl.SSLSocketFactory");
+ props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
props.put("mail.smtp.ssl.checkserveridentity", String.valueOf(config.isCheckServerIdentity()));
props.put("mail.transport.protocol", "smtps");
break;
@@ -367,7 +369,7 @@ protected void closeTransport(Transport transport) throws EntException {
* @return The mail service configuration.
*/
protected MailConfig getConfig() {
- return _config;
+ return this.tenantConfigs.get(this.getTenantCode());
}
/**
@@ -375,7 +377,8 @@ protected MailConfig getConfig() {
* @param config The mail service configuration.
*/
protected void setConfig(MailConfig config) {
- this._config = config;
+ String tenantCode = this.getTenantCode();
+ this.tenantConfigs.put(tenantCode, config);
}
protected Boolean isActive() {
@@ -406,7 +409,7 @@ public void setConfigManager(ConfigInterface configManager) {
}
private Boolean _active;
- private MailConfig _config;
+ private Map tenantConfigs = new HashMap<>();
private ConfigInterface _configManager;
/*
diff --git a/migrate-engine-namespaces.sh b/migrate-engine-namespaces.sh
deleted file mode 100755
index ac53e6efdc..0000000000
--- a/migrate-engine-namespaces.sh
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/bin/bash
-
-echo "=== Jakarta EE 10 Namespace Migration for Engine Module ==="
-echo "Starting migration of javax.* imports to jakarta.* in engine module..."
-
-cd engine/
-
-# Backup original files
-echo "Creating backup..."
-find . -name "*.java" -exec cp {} {}.pre-jakarta \; 2>/dev/null
-
-# Count files before migration
-echo "Files to migrate:"
-echo " - Servlet API: $(find . -name "*.java" -exec grep -l "import javax\.servlet\." {} \; | wc -l) files"
-echo " - Annotation API: $(find . -name "*.java" -exec grep -l "import javax\.annotation\." {} \; | wc -l) files"
-echo " - Validation API: $(find . -name "*.java" -exec grep -l "import javax\.validation\." {} \; | wc -l) files"
-echo " - EL API: $(find . -name "*.java" -exec grep -l "import javax\.el\." {} \; | wc -l) files"
-echo " - JAX-RS API: $(find . -name "*.java" -exec grep -l "import javax\.ws\.rs\." {} \; | wc -l) files"
-echo " - JAXB API: $(find . -name "*.java" -exec grep -l "import javax\.xml\.bind\." {} \; | wc -l) files"
-
-echo ""
-echo "Performing namespace migration..."
-
-# Servlet API migration
-echo " → Migrating javax.servlet.* to jakarta.servlet.*"
-find . -name "*.java" -exec sed -i 's/import javax\.servlet\./import jakarta.servlet./g' {} \;
-find . -name "*.java" -exec sed -i 's/javax\.servlet\./jakarta.servlet./g' {} \;
-
-# Annotation API migration
-echo " → Migrating javax.annotation.* to jakarta.annotation.*"
-find . -name "*.java" -exec sed -i 's/import javax\.annotation\./import jakarta.annotation./g' {} \;
-find . -name "*.java" -exec sed -i 's/javax\.annotation\./jakarta.annotation./g' {} \;
-
-# Validation API migration
-echo " → Migrating javax.validation.* to jakarta.validation.*"
-find . -name "*.java" -exec sed -i 's/import javax\.validation\./import jakarta.validation./g' {} \;
-find . -name "*.java" -exec sed -i 's/javax\.validation\./jakarta.validation./g' {} \;
-
-# EL API migration
-echo " → Migrating javax.el.* to jakarta.el.*"
-find . -name "*.java" -exec sed -i 's/import javax\.el\./import jakarta.el./g' {} \;
-find . -name "*.java" -exec sed -i 's/javax\.el\./jakarta.el./g' {} \;
-
-# JAX-RS API migration
-echo " → Migrating javax.ws.rs.* to jakarta.ws.rs.*"
-find . -name "*.java" -exec sed -i 's/import javax\.ws\.rs\./import jakarta.ws.rs./g' {} \;
-find . -name "*.java" -exec sed -i 's/javax\.ws\.rs\./jakarta.ws.rs./g' {} \;
-
-# JAXB API migration
-echo " → Migrating javax.xml.bind.* to jakarta.xml.bind.*"
-find . -name "*.java" -exec sed -i 's/import javax\.xml\.bind\./import jakarta.xml.bind./g' {} \;
-find . -name "*.java" -exec sed -i 's/javax\.xml\.bind\./jakarta.xml.bind./g' {} \;
-
-# Count files after migration
-echo ""
-echo "Migration completed! Files affected:"
-echo " - Total Java files processed: $(find . -name "*.java" | wc -l)"
-echo " - Files with jakarta.servlet imports: $(find . -name "*.java" -exec grep -l "import jakarta\.servlet\." {} \; | wc -l)"
-echo " - Files with jakarta.annotation imports: $(find . -name "*.java" -exec grep -l "import jakarta\.annotation\." {} \; | wc -l)"
-echo " - Files with jakarta.validation imports: $(find . -name "*.java" -exec grep -l "import jakarta\.validation\." {} \; | wc -l)"
-
-echo ""
-echo "=== Migration Summary ==="
-echo "Namespace migration completed for engine module"
-echo "Backup files created with .pre-jakarta extension"
-echo "Ready for compilation testing"
\ No newline at end of file
diff --git a/migrate-spring-security-oauth2.sh b/migrate-spring-security-oauth2.sh
deleted file mode 100755
index 95d7c469eb..0000000000
--- a/migrate-spring-security-oauth2.sh
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/bin/bash
-
-echo "Migrating Java source imports from javax to jakarta..."
-
-# Update servlet imports
-find . -name "*.java" -type f -exec sed -i 's/import javax\.servlet\./import jakarta.servlet./g' {} +
-find . -name "*.java" -type f -exec sed -i 's/import javax\.servlet\.http\./import jakarta.servlet.http./g' {} +
-find . -name "*.java" -type f -exec sed -i 's/import javax\.servlet\.jsp\./import jakarta.servlet.jsp./g' {} +
-
-# Update validation imports
-find . -name "*.java" -type f -exec sed -i 's/import javax\.validation\./import jakarta.validation./g' {} +
-
-# Update xml.bind imports
-find . -name "*.java" -type f -exec sed -i 's/import javax\.xml\.bind\./import jakarta.xml.bind./g' {} +
-
-# Update ws.rs imports
-find . -name "*.java" -type f -exec sed -i 's/import javax\.ws\.rs\./import jakarta.ws.rs./g' {} +
-
-# Update annotation imports
-find . -name "*.java" -type f -exec sed -i 's/import javax\.annotation\./import jakarta.annotation./g' {} +
-
-echo "Java source import migration completed."
diff --git a/pom.xml.backup-pre-jakarta-migration b/pom.xml.backup-pre-jakarta-migration
deleted file mode 100644
index 9b7499d2f4..0000000000
--- a/pom.xml.backup-pre-jakarta-migration
+++ /dev/null
@@ -1,1651 +0,0 @@
-
-
- 4.0.0
-
- org.entando
- entando-maven-root
- 7.4.0-ENG-5316-PR-21
-
- org.entando
-
- app-engine
- 7.5.0
- pom
-
-
- engine
- admin-console
- portal-ui
- keycloak-plugin
- cms-plugin
- mail-plugin
- redis-plugin
- cds-plugin
- seo-plugin
- versioning-plugin
- contentscheduler-plugin
- solr-plugin
- webapp
-
-
- Entando App Engine Parent POM
- Entando App Engine Parent POM
- 2019
- https://central.entando.com
-
-
- GNU LESSER GENERAL PUBLIC LICENSE, Version 3, 29 June 2007
- https://www.gnu.org/licenses/lgpl-3.0.txt
- repo
-
-
-
- Entando Inc.
- https://www.entando.com/
-
-
-
- nexus-jx
- https://nexus-jx.apps.serv.run/repository/maven-releases/
-
- true
-
-
- false
-
-
-
-
- scm:git:git@github.com:${github.organization}/${project.artifactId}.git
- scm:git:git@github.com:${github.organization}/${project.artifactId}.git
-
- https://github.com/${github.organization}/${project.artifactId}/
- HEAD
-
-
- 17
- 17
- true
-
-
-
- **/*.js
-
- org.apache.derby.jdbc.EmbeddedDriver
- localhost
- 1527
- agile
- agile
- jdbc:derby:memory:testPort;create=true
- jdbc:derby:memory:testServ;create=true
-
- 5.3.39
- 5.8.16
- 2.5.2.RELEASE
- 6.7.4
- 2.16.2
- 2.16.2
- 6.0.20.Final
- 8.11.4
- 2.9.0
- 2.10.5
- 1.11.6
- 33.4.8-jre
- 3.5.6
- 1.11.0
- 1.2
- 1.12.0
- 1.16.1
- 3.2.2
- 2.9.0
- 2.3.32
- 1.9.0
- 1.2.5
- 3.20.2
- 2.1
- 1.5.18
- 0.1.5
- 0.1.5
- 1.9.7
- 4.40
- 4.8.0
- 2.0.17
- 2.20.0
- 1.5.4
- 4.5.14
- 3.0.1
- 3.0.1
- 10.15.2.0
- 2.0.1.Final
- 2.2.6
- 3.0.0
- 10.0.8
- 1.4.0
- 1.0
- 2.10.10
- 2.2
- 1.3
- 3.11.2
- 5.11.4
- 1.17.6
- 2.1.3
- 4.0.1
- 1.3.2
- 1.1.3
-
- 2.0.6.1
- 3.28.0-GA
- 2.4.1
- 3.3.5
- 2.19.0
- 4.8.172
- 3.18.0
- 1.3.5
- 1.1
- 1.6
- 1.4.01
- 2.0.8
- 1.3.7
- 2.7.0.0
- 3.2.2
- 0.9.5.5
- 0.8
- 2.18.0
- 4.5.0-M2
- 1.18.38
- 3.3.0
- 3.5.2
- 2.5
- 2.2.1
- 5.8.0
- 9.8
- 1.6.7
- 3.1.7
- 2.0.24
- 2.0.1
- 4.2.2
- 2.3.1
- 1.1.1
- 1.2.2
- 1.28.0
- 2.5.12
- 2.7.4
- 6.1.5.RELEASE
- 1.5.20
- 2.9.0
- 2.5.2
- 2.4.0
- 2.1.6
- 6.0.20.Final
- 2.3.3
- 1.2.3
- 1.2.3
- 4.4.16
- 2.20.0
- 1.3.4
- 2.5.3
- 2.2
- 2.3.4
- 1.3.5
- 3.25.5
- 2.12.2
- 7.4.1
- 4.1.125.Final
- 1.6.0
-
- 9.8.0
-
- 1.5.6
- 7.1.0
- 1.1.10.7
- 3.2.1
- 2.39.0
- 3.9.3
-
-
- Github
- https://github.com/${github.organization}/${project.artifactId}/issues/
-
-
-
-
-
-
- org.owasp
- dependency-check-maven
- 12.1.0
-
- 7
- false
-
- HTML
- JSON
-
-
-
-
- verify
-
- check
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-dependency-plugin
- ${maven-dependency-plugin.version}
-
-
-
- org.apache.maven.plugins
- maven-surefire-plugin
- ${maven-surefire-plugin.version}
-
- ${argLine} -Dliquibase.supportPropertyEscaping=true -Dlogback.statusListenerClass=ch.qos.logback.core.status.NopStatusListener
- 1
- exit
- methods
- 8
- alphabetical
-
- **/*BaseTestCase*
- **/Abstract*
-
-
- WARN
-
- 1
-
-
-
-
- org.apache.maven.plugins
- maven-assembly-plugin
- ${maven-assembly-plugin.version}
-
-
- package-classes
- package
-
- single
-
-
-
- ${project.basedir}/src/main/assembly/classes.xml
-
-
-
-
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-resources-plugin
- ${maven-resources-plugin.version}
-
-
- copy-resources-test
-
- generate-test-resources
-
- copy-resources
-
-
- ${project.build.directory}/test
- UTF-8
-
-
- ${project.basedir}/src/test/config
- true
-
-
-
-
-
- copy-resources-file-test-
-
- generate-test-resources
-
- copy-resources
-
-
- ${project.build.directory}/test/resources/
- UTF-8
-
-
- ${project.basedir}/src/test/config
- true
-
-
-
-
-
-
-
-
-
-
-
-
- org.apache.lucene
- lucene-core
- ${lucene-core.version}
- jar
-
-
- org.apache.lucene
- lucene-analyzers-common
- ${lucene-core.version}
- jar
-
-
-
- com.jayway.jsonpath
- json-path-assert
- ${json-path-assert.version}
- test
-
-
-
-
- org.apache.solr
- solr-solrj
- ${solr-solrj.version}
-
-
-
- org.eclipse.jetty
- *
-
-
- org.eclipse.jetty.http2
- *
-
-
- io.netty
- netty-transport-native-epoll
-
-
-
-
-
- io.springfox
- springfox-swagger2
- ${springfox-swagger2.version}
-
-
- com.google.guava
- guava
-
-
-
-
- io.springfox
- springfox-swagger-ui
- ${springfox-swagger2.version}
-
-
- com.google.guava
- guava
-
-
- net.bytebuddy
- byte-buddy
-
-
-
-
- io.springfox
- springfox-spring-webmvc
- ${springfox-swagger2.version}
-
-
- net.java.dev.jna
- jna
- ${jna.version}
-
-
- net.bytebuddy
- byte-buddy
- ${byte-buddy.version}
-
-
-
- com.google.guava
- guava
- ${guava.version}
-
-
- org.springframework.data
- spring-data-rest-webmvc
- ${spring-data-rest-webmvc.version}
-
-
- org.springframework
- spring-tx
-
-
-
-
- org.springframework.data
- spring-data-redis
- ${spring-data-redis.version}
-
-
- org.springframework.session
- spring-session-data-redis
- ${spring-session-data-redis.version}
-
-
- commons-beanutils
- commons-beanutils
- ${commons-beanutils.version}
- jar
-
-
-
- commons-chain
- commons-chain
- ${commons-chain.version}
- jar
-
-
-
- org.apache.commons
- commons-text
- ${commons-text.version}
- jar
-
-
-
- commons-codec
- commons-codec
- ${commons-codec.version}
- jar
-
-
-
- commons-collections
- commons-collections
- ${commons-collections.version}
- jar
-
-
-
- org.apache.commons
- commons-dbcp2
- ${commons-dbcp2.version}
- jar
-
-
-
- commons-digester
- commons-digester
- ${commons-digester.version}
- jar
-
-
-
- commons-io
- commons-io
- ${commons-io.version}
- jar
-
-
- io.github.classgraph
- classgraph
- ${classgraph.version}
-
-
-
-
-
-
-
-
- org.apache.commons
- commons-lang3
- ${commons-lang3.version}
- jar
-
-
-
- commons-logging
- commons-logging
- ${commons-logging.version}
- jar
-
-
-
- commons-logging
- commons-logging-api
- ${commons-logging-api.version}
- jar
-
-
-
- commons-pool
- commons-pool
- ${commons-pool.version}
- jar
-
-
-
- commons-validator
- commons-validator
- ${commons-validator.version}
- jar
-
-
-
- javax.validation
- validation-api
- ${javax-validator.version}
- jar
-
-
-
- org.freemarker
- freemarker
- ${freemarker.version}
- jar
-
-
-
- org.apache.taglibs
- taglibs-standard-impl
- ${taglibs-standard-impl.version}
-
-
-
- ch.qos.logback
- logback-core
- ${logback.version}
-
-
-
- ch.qos.logback
- logback-classic
- ${logback.version}
-
-
-
- ch.qos.logback.contrib
- logback-json-classic
- ${logback-json.version}
-
-
- ch.qos.logback.contrib
- logback-jackson
- ${logback-jackson.version}
-
-
- org.springframework
- spring-expression
- ${spring.version}
- jar
-
-
- org.springframework
- spring-aop
- ${spring.version}
- jar
-
-
- org.apache.tika
- tika-core
- ${tika-version}
-
-
- org.apache.tika
- tika-parsers-standard-package
- ${tika-version}
-
-
- org.apache.cxf
- cxf-core
-
-
- org.apache.cxf
- cxf-rt-frontend-jaxrs
-
-
- joda-time
- joda-time
-
-
- edu.usc.ir
- sentiment-analysis-parser
-
-
- c3p0
- c3p0
-
-
- com.fasterxml.jackson.core
- jackson-core
-
-
- com.fasterxml.jackson.core
- jackson-databind
-
-
- com.fasterxml.jackson.core
- jackson-annotations
-
-
- org.gagravarr
- vorbis-java-tika
-
-
- org.apache.commons
- commons-compress
-
-
- org.quartz-scheduler
- quartz
-
-
-
-
-
- com.mchange
- c3p0
- ${c3p0.version}
-
-
- org.ow2.asm
- asm
- ${asm.version}
-
-
- org.jvnet.staxex
- stax-ex
- ${stax-ex.version}
-
-
- org.codehaus.woodstox
- stax2-api
- ${stax2-api.version}
-
-
- org.gagravarr
- vorbis-java-tika
- ${vorbis-java-tika.version}
-
-
- org.apache.tika
- tika-parsers
-
-
- org.apache.tika
- tika-core
-
-
-
-
- com.drewnoakes
- metadata-extractor
- ${drewnoakes-metadata-extractor.version}
-
-
- org.apache.commons
- commons-collections4
- ${commons-collections4.version}
- jar
-
-
- org.projectlombok
- lombok
- ${lombok.version}
-
-
-
- org.springframework
- spring-beans
- ${spring.version}
- jar
-
-
-
- org.springframework
- spring-context
- ${spring.version}
- jar
-
-
-
- org.springframework
- spring-context-support
- ${spring.version}
- jar
-
-
-
- org.springframework
- spring-core
- ${spring.version}
- jar
-
-
- org.springframework
- spring-web
- ${spring.version}
- jar
-
-
- org.springframework
- spring-webmvc
- ${spring.version}
- jar
-
-
- org.springframework
- spring-tx
- ${spring.version}
- jar
-
-
- org.springframework
- spring-oxm
- ${spring.version}
- jar
-
-
- org.springframework
- spring-jcl
- ${spring.version}
- jar
-
-
- org.springframework.security
- spring-security-web
- ${springsecurity.version}
-
-
- org.springframework
- spring-context
-
-
- org.springframework
- spring-beans
-
-
- org.springframework
- spring-core
-
-
- org.springframework
- spring-expression
-
-
- org.springframework
- spring-aop
-
-
- org.springframework
- spring-web
-
-
-
-
- org.springframework.security
- spring-security-config
- ${springsecurity.version}
-
-
- org.springframework
- spring-context
-
-
- org.springframework
- spring-beans
-
-
- org.springframework
- spring-core
-
-
- org.springframework
- spring-aop
-
-
- org.springframework
- spring-web
-
-
-
-
- org.springframework.security.oauth
- spring-security-oauth2
- ${springsecurityoauth2.version}
-
-
- org.springframework
- spring-core
-
-
- org.springframework
- spring-context
-
-
- org.springframework
- spring-web
-
-
- org.springframework
- spring-webmvc
-
-
- org.springframework
- spring-beans
-
-
- org.springframework
- spring-aop
-
-
- org.springframework
- spring-expression
-
-
- org.springframework.security
- spring-security-config
-
-
- org.springframework.security
- spring-security-web
-
-
- org.springframework.security
- spring-security-core
-
-
- org.codehaus.jackson
- jackson-mapper-asl
-
-
-
-
-
- xml-apis
- xml-apis
- ${xml-apis.version}
-
-
-
- oro
- oro
- ${oro.version}
- jar
-
-
-
-
- org.apache.struts
- struts2-core
- ${struts2.version}
- jar
-
-
- org.apache.logging.log4j
- log4j-api
-
-
-
-
- org.apache.struts
- struts2-spring-plugin
- ${struts2.version}
- jar
-
-
-
- org.springframework
- spring-core
-
-
- org.springframework
- spring-web
-
-
- org.springframework
- spring-context
-
-
- org.springframework
- spring-beans
-
-
-
-
- org.apache.struts
- struts2-tiles-plugin
- ${struts2.version}
- jar
-
-
- ognl
- ognl
- ${ognl.version}
-
-
- javassist
- javassist
-
-
-
-
- org.apache.struts
- struts2-json-plugin
- ${struts2.version}
- jar
-
-
-
- org.apache.velocity
- velocity-engine-core
- ${velocity.version}
- jar
-
-
- commons-io
- commons-io
-
-
-
-
- org.javassist
- javassist
- ${javassist.version}
- jar
-
-
-
-
- org.jdom
- jdom2
- ${jdom2.version}
- jar
-
-
- org.apache.tiles
- tiles-api
- ${tiles-core.version}
- jar
-
-
- javax.servlet
- javax.servlet-api
- ${javax.servlet-api.version}
- provided
-
-
- javax.annotation
- javax.annotation-api
- ${javax.annotation-api.version}
-
-
-
- org.junit.jupiter
- junit-jupiter
- ${junit-jupiter.version}
- test
-
-
- org.junit.jupiter
- junit-jupiter-api
- ${junit-jupiter.version}
- test
-
-
- org.mockito
- mockito-junit-jupiter
- ${mockito.version}
- test
-
-
- org.mockito
- mockito-core
- ${mockito.version}
- test
-
-
- org.mockito
- mockito-inline
- ${mockito.version}
- test
-
-
- org.hamcrest
- hamcrest-all
- ${hamcrest-all.version}
- test
-
-
- uk.org.webcompere
- system-stubs-jupiter
- ${system-stubs-jupiter.version}
- test
-
-
- org.apache.tomcat
- tomcat-servlet-api
- ${tomcat-servlet-api.version}
- test
-
-
- org.testcontainers
- testcontainers-bom
- ${testcontainers.version}
- pom
- import
-
-
- javax.el
- javax.el-api
- ${javax.el-api.version}
-
-
- org.glassfish.web
- javax.el
- ${javax.el.version}
-
-
- org.im4java
- im4java
- ${im4java.version}
-
-
-
-
- org.springframework
- spring-test
- ${spring.version}
- test
-
-
- org.apache.derby
- derby
- ${derby.version}
- test
-
-
- org.apache.derby
- derbyclient
- ${derby.version}
- test
-
-
- org.apache.derby
- derbytools
- ${derby.version}
- test
-
-
-
- javax.servlet.jsp
- jsp-api
- ${jsp-api.version}
- provided
-
-
-
- joda-time
- joda-time
- ${joda-time.version}
-
-
-
- org.apache.httpcomponents
- httpclient
- ${httpclient.version}
-
-
- com.sun.xml.bind
- jaxb-impl
- ${jaxb-impl.version}
-
-
- com.sun.xml.bind
- jaxb-core
- ${jaxb-impl.version}
-
-
- com.sun.xml.bind
- jaxb-xjc
- ${jaxb-xjc.version}
-
-
- org.codehaus.jettison
- jettison
- ${jettison.version}
-
-
-
- stax-api
- stax
-
-
-
-
- org.liquibase
- liquibase-core
- ${liquibase-core.version}
- jar
-
-
- org.aspectj
- aspectjrt
- ${aspectj.version}
-
-
- org.aspectj
- aspectjweaver
- ${aspectj.version}
-
-
- aopalliance
- aopalliance
- ${aopalliance.version}
-
-
- org.slf4j
- slf4j-api
- ${slf4j.version}
- jar
-
-
-
- org.slf4j
- slf4j-jdk14
- ${slf4j.version}
- jar
-
-
-
- org.slf4j
- slf4j-log4j12
- ${slf4j.version}
-
-
- log4j
- log4j
-
-
-
-
- org.slf4j
- jcl-over-slf4j
- ${slf4j.version}
-
-
- org.slf4j
- log4j-over-slf4j
- ${slf4j.version}
-
-
- org.apache.logging.log4j
- log4j-to-slf4j
- ${log4j2slf4j.version}
-
-
-
-
- com.fasterxml.jackson.core
- jackson-core
- ${jackson.version}
-
-
- com.fasterxml.jackson.core
- jackson-annotations
- ${jackson.version}
-
-
- com.fasterxml.jackson.core
- jackson-databind
- ${jackson-databind.version}
-
-
- com.fasterxml.jackson.module
- jackson-module-jaxb-annotations
- ${jackson.version}
-
-
- com.fasterxml.jackson.dataformat
- jackson-dataformat-xml
- ${jackson.version}
-
-
- com.fasterxml.jackson.datatype
- jackson-datatype-jdk8
- ${jackson.version}
-
-
-
- org.hibernate
- hibernate-validator
- ${hibernate-validator-version}
-
-
-
- org.assertj
- assertj-core
- ${assertj-core.version}
- test
-
-
- org.owasp.esapi
- esapi
- ${esapi.version}
-
-
- org.beanshell
- bsh-core
-
-
- xerces
- xercesImpl
-
-
- xml-apis
- xml-apis
-
-
- commons-fileupload
- commons-fileupload
-
-
- commons-beanutils
- commons-beanutils-core
-
-
-
- org.apache.xmlgraphics
- batik-ext
-
-
-
- org.apache.xmlgraphics
- batik-css
-
-
-
- xalan
- xalan
-
-
- xom
- xom
-
-
- org.owasp.antisamy
- antisamy
-
-
- log4j
- log4j
-
-
- commons-lang
- commons-lang
-
-
- commons-configuration
- commons-configuration
-
-
-
-
-
- xom
- xom
- ${xom.version}
-
-
-
-
- xerces
- xercesImpl
-
-
-
- xalan
- xalan
-
-
-
-
- com.sun.mail
- mailapi
- ${sun-mail.version}
-
-
- com.sun.activation
- jakarta.activation
-
-
-
-
- com.sun.mail
- smtp
- ${sun-mail.version}
-
-
- org.subethamail
- subethasmtp
- ${subethasmtp.version}
- test
-
-
- slf4j-api
- org.slf4j
-
-
-
-
- org.apache.pdfbox
- pdfbox
- ${pdfbox.version}
-
-
- org.apache.pdfbox
- pdfbox-tools
- ${pdfbox.version}
-
-
- org.apache.pdfbox
- preflight
- ${pdfbox.version}
-
-
- org.apache.pdfbox
- xmpbox
- ${pdfbox.version}
-
-
- javax.xml.bind
- jaxb-api
- ${jaxb-api.version}
-
-
- javax.activation
- activation
- ${activation.version}
-
-
- jakarta.activation
- jakarta.activation-api
- ${jakarta-activation.version}
-
-
- org.apache.commons
- commons-compress
- ${commons-compress.version}
-
-
- io.lettuce
- lettuce-core
- ${lettuce-core.version}
-
-
- io.swagger
- swagger-annotations
- ${swagger-annotations.version}
-
-
- com.jayway.jsonpath
- json-path
- ${json-path.version}
-
-
- net.minidev
- json-smart
- ${json-smart.version}
-
-
- org.quartz-scheduler
- quartz
- ${quartz.version}
-
-
-
-
-
-
-
- jakarta.ws.rs
- jakarta.ws.rs-api
- ${jakarta.ws.rs-api.version}
-
-
- org.hibernate.validator
- hibernate-validator
- ${hibernate-validator.version}
-
-
- jakarta.xml.bind
- jakarta.xml.bind-api
- ${jakarta.xml.bind-api.version}
-
-
- org.owasp.encoder
- encoder
- ${encoder.version}
-
-
- org.owasp.encoder
- encoder-jsp
- ${encoder-jsp.version}
-
-
-
-
-
-
-
- org.apache.httpcomponents
- httpcore
- ${httpcore.version}
-
-
- org.apache.logging.log4j
- log4j-core
- ${log4j-core.version}
-
-
- org.springframework.security
- spring-security-core
- ${springsecurity.version}
-
-
- com.fasterxml
- classmate
- ${classmate.version}
-
-
- org.springframework.data
- spring-data-commons
- ${spring-data-commons.version}
-
-
- io.springfox
- springfox-core
- ${springfox-swagger2.version}
-
-
- io.springfox
- springfox-swagger-common
- ${springfox-swagger2.version}
-
-
- io.springfox
- springfox-spi
- ${springfox-swagger2.version}
-
-
- org.springframework.security
- spring-security-crypto
- ${springsecurity.version}
-
-
- org.hamcrest
- hamcrest
- ${hamcrest.version}
-
-
- org.glassfish.jaxb
- jaxb-runtime
- ${jaxb-runtime.version}
-
-
- io.springfox
- springfox-spring-web
- ${springfox-swagger2.version}
-
-
- jakarta.annotation
- jakarta.annotation-api
- ${jakarta.annotation-api.version}
-
-
- com.google.protobuf
- protobuf-java
- ${protobuf-java.version}
-
-
- xerces
- xercesImpl
- ${xercesImpl.version}
-
-
- com.github.junrar
- junrar
- ${junrar.version}
-
-
- io.netty
- netty-common
- ${netty.version}
-
-
- io.netty
- netty-buffer
- ${netty.version}
-
-
- io.netty
- netty-codec
- ${netty.version}
-
-
- io.netty
- netty-handler
- ${netty.version}
-
-
- io.netty
- netty-resolver
- ${netty.version}
-
-
- io.netty
- netty-transport
- ${netty.version}
-
-
- io.netty
- netty-transport-classes-epoll
- ${netty.version}
-
-
- io.netty
- netty-transport-native-epoll
- ${netty.version}
-
-
- io.netty
- netty-transport-native-unix-common
- ${netty.version}
-
-
- commons-fileupload
- commons-fileupload
- ${commons-fileupload.version}
-
-
- org.springframework.hateoas
- spring-hateoas
- ${spring-hateoas.version}
-
-
-
- com.fasterxml.woodstox
- woodstox-core
- ${fasterxml-woodstox.version}
-
-
- org.xerial.snappy
- snappy-java
- ${snappy-java.version}
-
-
- com.github.ben-manes.caffeine
- caffeine
- ${caffeine.version}
-
-
- com.google.errorprone
- error_prone_annotations
- ${google-errorprone.version}
-
-
- org.apache.zookeeper
- zookeeper-jute
- ${zookeeper.version}
-
-
- org.apache.zookeeper
- zookeeper
- ${zookeeper.version}
-
-
-
-
-
- local-dev
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
-
-
- org.apache.maven.plugins
- maven-jar-plugin
-
-
- org.apache.maven.plugins
- maven-source-plugin
-
-
-
-
-
-
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..760bafc409
--- /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.junit.jupiter.MockitoExtension;
+
+import java.lang.reflect.Method;
+import java.sql.SQLException;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * 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..dd6201cef3 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
@@ -47,6 +47,7 @@
import java.util.Map;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
/**
* @author G.Cocco
@@ -58,6 +59,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 +83,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 +92,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 +111,7 @@ void testGetVersion() throws Throwable {
assertEquals("admin", contentVersion.getUsername());
}
+ @Test
void testGetLastVersion() throws Throwable {
ContentVersion contentVersion = this.versioningManager.getLastVersion("CNG12");
assertNull(contentVersion);
@@ -109,6 +129,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,12 +149,20 @@ 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) {
@@ -145,11 +174,13 @@ 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);
@@ -214,19 +245,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
diff --git a/webapp/README.md b/webapp/README.md
index 5ec00a23bd..21f9886bd9 100644
--- a/webapp/README.md
+++ b/webapp/README.md
@@ -12,6 +12,8 @@ Here the command to use
mvn clean package jetty:run-war -Pjetty-local -Pderby
```
+> ⚠️ **WARNING:** Running without `-Pkeycloak` disables Keycloak and enables Basic Authentication instead. This is **UNSAFE FOR PRODUCTION** and should only be used for local development.
+
If you want to use keycloak as external authorization service, add the keycloak profile and update the proper
variables (you can find them in the `properties` tag in the pom)
diff --git a/webapp/pom.xml b/webapp/pom.xml
index 14b513f204..b145f6d23e 100644
--- a/webapp/pom.xml
+++ b/webapp/pom.xml
@@ -48,7 +48,7 @@
10.1.46
- true
+ false
http://localhost:8081/auth
entando
entando-app
@@ -344,6 +344,12 @@
oracle.jdbc.OracleDriver
+
+ keycloak
+
+ true
+
+
contentscheduler
diff --git a/webapp/src/main/webapp/WEB-INF/web.xml b/webapp/src/main/webapp/WEB-INF/web.xml
index c7c6314a96..db0b3ddc62 100644
--- a/webapp/src/main/webapp/WEB-INF/web.xml
+++ b/webapp/src/main/webapp/WEB-INF/web.xml
@@ -107,6 +107,14 @@
XSSFilter
/pages/*
+
+ XSSFilter
+ /page/*
+
+
+ XSSFilter
+ /preview/*
+
CharacterEncodingFilter
*