diff --git a/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/model/attribute/AbstractResourceAttribute.java b/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/model/attribute/AbstractResourceAttribute.java index d5d1ff0ab2..5358293bdc 100644 --- a/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/model/attribute/AbstractResourceAttribute.java +++ b/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/model/attribute/AbstractResourceAttribute.java @@ -17,6 +17,7 @@ import com.agiletec.aps.system.common.entity.model.attribute.TextAttribute; import com.agiletec.aps.system.services.group.Group; import com.agiletec.aps.system.services.lang.Lang; +import com.agiletec.aps.util.ApplicationContextProvider; import com.agiletec.plugins.jacms.aps.system.services.content.model.CmsAttributeReference; import com.agiletec.plugins.jacms.aps.system.services.resource.IResourceManager; import com.agiletec.plugins.jacms.aps.system.services.resource.model.ResourceInterface; @@ -30,8 +31,6 @@ import org.entando.entando.ent.util.EntLogging.EntLogFactory; import org.entando.entando.ent.util.EntLogging.EntLogger; import org.jdom2.Element; -import org.springframework.web.context.ContextLoader; -import org.springframework.web.context.WebApplicationContext; /** * Classe astratta di appoggio agli attributi di tipo Risorsa. @@ -386,7 +385,7 @@ public void valueFrom(AbstractJAXBAttribute jaxbAttribute, String langCode) { return; } try { - ResourceInterface resource = this.getResourceManager().loadResource(resourceId.toString()); + ResourceInterface resource = this.getResourceManager().loadResource(resourceId.toString()); //NOSONAR if (null != resource) { this.setResource(resource, this.getDefaultLangCode()); } @@ -419,6 +418,7 @@ public Map getResources() { } protected IResourceManager getResourceManager() { + this.resourceManager = ApplicationContextProvider.resolveIfNull(this.resourceManager, IResourceManager.class); return resourceManager; } @@ -430,11 +430,9 @@ public void setResourceManager(IResourceManager resourceManager) { private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); - WebApplicationContext ctx = ContextLoader.getCurrentWebApplicationContext(); - if (ctx == null) { + this.resourceManager = ApplicationContextProvider.resolveBean(IResourceManager.class); + if (this.resourceManager == null) { logger.warn("Null WebApplicationContext during deserialization"); - return; } - this.resourceManager = ctx.getBean(IResourceManager.class); } } diff --git a/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/model/attribute/CmsHypertextAttribute.java b/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/model/attribute/CmsHypertextAttribute.java index 3cf1777128..1de65ec9c7 100644 --- a/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/model/attribute/CmsHypertextAttribute.java +++ b/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/model/attribute/CmsHypertextAttribute.java @@ -18,6 +18,7 @@ import com.agiletec.aps.system.common.entity.model.attribute.HypertextAttribute; import com.agiletec.aps.system.services.lang.ILangManager; import com.agiletec.aps.system.services.lang.Lang; +import com.agiletec.aps.util.ApplicationContextProvider; import com.agiletec.aps.system.services.page.IPageManager; import com.agiletec.plugins.jacms.aps.system.services.content.IContentManager; import com.agiletec.plugins.jacms.aps.system.services.content.model.CmsAttributeReference; @@ -32,8 +33,6 @@ import org.entando.entando.ent.util.EntLogging.EntLogFactory; import org.entando.entando.ent.util.EntLogging.EntLogger; import org.springframework.beans.factory.BeanFactory; -import org.springframework.web.context.ContextLoader; -import org.springframework.web.context.WebApplicationContext; /** * Rappresenta una informazione di tipo "ipertesto" specifico per il cms. @@ -205,6 +204,7 @@ private SymbolicLinkValidator getSymbolicLinkValidator(BeanFactory beanFactory) @Deprecated protected IContentManager getContentManager() { + this.contentManager = ApplicationContextProvider.resolveIfNull(this.contentManager, IContentManager.class); return contentManager; } @@ -215,6 +215,7 @@ public void setContentManager(IContentManager contentManager) { @Deprecated protected IPageManager getPageManager() { + this.pageManager = ApplicationContextProvider.resolveIfNull(this.pageManager, IPageManager.class); return pageManager; } @@ -225,6 +226,7 @@ public void setPageManager(IPageManager pageManager) { @Deprecated public IResourceManager getResourceManager() { + this.resourceManager = ApplicationContextProvider.resolveIfNull(this.resourceManager, IResourceManager.class); return resourceManager; } @@ -245,14 +247,15 @@ public boolean isSearchableOptionSupported() { private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); - WebApplicationContext ctx = ContextLoader.getCurrentWebApplicationContext(); - if (ctx == null) { + this.contentManager = ApplicationContextProvider.resolveBean(IContentManager.class); + this.pageManager = ApplicationContextProvider.resolveBean(IPageManager.class); + this.resourceManager = ApplicationContextProvider.resolveBean(IResourceManager.class); + ILangManager langManager = ApplicationContextProvider.resolveBean(ILangManager.class); + if (langManager != null) { + this.setLangManager(langManager); + } + if (this.resourceManager == null) { logger.warn("Null WebApplicationContext during deserialization"); - return; } - this.contentManager = ctx.getBean(IContentManager.class); - this.pageManager = ctx.getBean(IPageManager.class); - this.resourceManager = ctx.getBean(IResourceManager.class); - this.setLangManager(ctx.getBean(ILangManager.class)); } } diff --git a/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/model/attribute/LinkAttribute.java b/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/model/attribute/LinkAttribute.java index 0d1d36ad78..dd1609109a 100644 --- a/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/model/attribute/LinkAttribute.java +++ b/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/model/attribute/LinkAttribute.java @@ -19,6 +19,7 @@ import com.agiletec.aps.system.common.entity.model.attribute.AbstractJAXBAttribute; import com.agiletec.aps.system.common.entity.model.attribute.TextAttribute; import com.agiletec.aps.system.services.lang.ILangManager; +import com.agiletec.aps.util.ApplicationContextProvider; import com.agiletec.aps.system.services.lang.Lang; import com.agiletec.aps.system.services.page.IPageManager; import com.agiletec.plugins.jacms.aps.system.services.content.IContentManager; @@ -40,8 +41,6 @@ import org.entando.entando.ent.util.EntLogging.EntLogger; import org.jdom2.Element; import org.springframework.beans.factory.BeanFactory; -import org.springframework.web.context.ContextLoader; -import org.springframework.web.context.WebApplicationContext; /** * Rappresenta una informazione di tipo "link". La destinazione del link รจ la @@ -331,6 +330,7 @@ private String getProperty(String key, String langCode) { @Deprecated protected IContentManager getContentManager() { + this.contentManager = ApplicationContextProvider.resolveIfNull(this.contentManager, IContentManager.class); return contentManager; } @@ -341,6 +341,7 @@ public void setContentManager(IContentManager contentManager) { @Deprecated protected IPageManager getPageManager() { + this.pageManager = ApplicationContextProvider.resolveIfNull(this.pageManager, IPageManager.class); return pageManager; } @@ -351,6 +352,7 @@ public void setPageManager(IPageManager pageManager) { @Deprecated protected ILinkResolverManager getLinkResolverManager() { + this.linkResolverManager = ApplicationContextProvider.resolveIfNull(this.linkResolverManager, ILinkResolverManager.class); return linkResolverManager; } @@ -361,6 +363,7 @@ public void setLinkResolverManager(ILinkResolverManager linkResolverManager) { @Deprecated public IResourceManager getResourceManager() { + this.resourceManager = ApplicationContextProvider.resolveIfNull(this.resourceManager, IResourceManager.class); return resourceManager; } @@ -381,16 +384,17 @@ public void setResourceManager(IResourceManager resourceManager) { private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); - WebApplicationContext ctx = ContextLoader.getCurrentWebApplicationContext(); - if (ctx == null) { + this.contentManager = ApplicationContextProvider.resolveBean(IContentManager.class); + this.pageManager = ApplicationContextProvider.resolveBean(IPageManager.class); + this.linkResolverManager = ApplicationContextProvider.resolveBean(ILinkResolverManager.class); + this.resourceManager = ApplicationContextProvider.resolveBean(IResourceManager.class); + ILangManager langManager = ApplicationContextProvider.resolveBean(ILangManager.class); + if (langManager != null) { + this.setLangManager(langManager); + } + if (this.resourceManager == null) { logger.warn("Null WebApplicationContext during deserialization"); - return; } - this.contentManager = ctx.getBean(IContentManager.class); - this.pageManager = ctx.getBean(IPageManager.class); - this.linkResolverManager = ctx.getBean(ILinkResolverManager.class); - this.resourceManager = ctx.getBean(IResourceManager.class); - this.setLangManager(ctx.getBean(ILangManager.class)); } } diff --git a/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/parse/attribute/ResourceAttributeHandler.java b/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/parse/attribute/ResourceAttributeHandler.java index c5c7ad7d7e..451cd12a34 100644 --- a/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/parse/attribute/ResourceAttributeHandler.java +++ b/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/parse/attribute/ResourceAttributeHandler.java @@ -14,14 +14,13 @@ package com.agiletec.plugins.jacms.aps.system.services.content.parse.attribute; import com.agiletec.aps.system.common.entity.parse.attribute.TextAttributeHandler; +import com.agiletec.aps.util.ApplicationContextProvider; import com.agiletec.plugins.jacms.aps.system.services.content.model.attribute.ResourceAttributeInterface; import com.agiletec.plugins.jacms.aps.system.services.resource.IResourceManager; import com.agiletec.plugins.jacms.aps.system.services.resource.model.ResourceInterface; import java.io.IOException; import org.entando.entando.ent.util.EntLogging.EntLogFactory; import org.entando.entando.ent.util.EntLogging.EntLogger; -import org.springframework.web.context.ContextLoader; -import org.springframework.web.context.WebApplicationContext; import org.xml.sax.Attributes; import org.xml.sax.SAXException; @@ -126,6 +125,7 @@ private void endResource() { */ @Deprecated protected IResourceManager getResourceManager() { + this.resourceManager = ApplicationContextProvider.resolveIfNull(this.resourceManager, IResourceManager.class); return this.resourceManager; } @@ -164,11 +164,9 @@ protected void setMetadataKey(String metadataKey) { private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); - WebApplicationContext ctx = ContextLoader.getCurrentWebApplicationContext(); - if (ctx == null) { + this.resourceManager = ApplicationContextProvider.resolveBean(IResourceManager.class); + if (this.resourceManager == null) { _logger.warn("Null WebApplicationContext during deserialization"); - return; } - this.resourceManager = ctx.getBean(IResourceManager.class); } } diff --git a/cms-plugin/src/test/java/com/agiletec/plugins/jacms/aps/system/services/content/model/attribute/AttributeSerializationIntegrationTest.java b/cms-plugin/src/test/java/com/agiletec/plugins/jacms/aps/system/services/content/model/attribute/AttributeSerializationIntegrationTest.java index fb25540bd5..c3e12e4971 100644 --- a/cms-plugin/src/test/java/com/agiletec/plugins/jacms/aps/system/services/content/model/attribute/AttributeSerializationIntegrationTest.java +++ b/cms-plugin/src/test/java/com/agiletec/plugins/jacms/aps/system/services/content/model/attribute/AttributeSerializationIntegrationTest.java @@ -5,6 +5,7 @@ import com.agiletec.aps.system.services.baseconfig.ConfigInterface; import com.agiletec.aps.system.services.lang.ILangManager; import com.agiletec.aps.system.services.page.IPageManager; +import com.agiletec.aps.util.ApplicationContextProvider; import com.agiletec.plugins.jacms.aps.system.JacmsSystemConstants; import com.agiletec.plugins.jacms.aps.system.services.content.IContentManager; import com.agiletec.plugins.jacms.aps.system.services.content.parse.attribute.ResourceAttributeHandler; @@ -46,10 +47,16 @@ void testSerializeAttachAttribute() throws Exception { AttachAttribute attribute = new AttachAttribute(); attribute.setName("testAttach"); attribute.setResourceManager(resourceManager); + // current web application context available attribute = testSerializeAndDeserialize(attribute); Assertions.assertNotNull(attribute.getResourceManager()); + // no current web application context -> re-wired through the ApplicationContextProvider fallback attribute = testSerializeAndDeserializeNullApplicationContext(attribute); - Assertions.assertNull(attribute.getResourceManager()); + Assertions.assertNotNull(attribute.getResourceManager()); + // neither the current context nor the provider are available -> stays null (warn path). + // The raw field is checked (not the getter) so the self-healing getter does not re-resolve. + attribute = testSerializeAndDeserializeNoContextAvailable(attribute); + Assertions.assertNull(ReflectionTestUtils.getField(attribute, "resourceManager")); } @Test @@ -60,15 +67,23 @@ void testSerializeCmsHypertextAttribute() throws Exception { attribute.setPageManager(pageManager); attribute.setResourceManager(resourceManager); attribute.setLangManager(langManager); + // current web application context available attribute = testSerializeAndDeserialize(attribute); Assertions.assertNotNull(attribute.getContentManager()); Assertions.assertNotNull(attribute.getPageManager()); Assertions.assertNotNull(attribute.getResourceManager()); Assertions.assertNotNull(ReflectionTestUtils.invokeGetterMethod(attribute, "langManager")); + // no current web application context -> re-wired through the ApplicationContextProvider fallback attribute = testSerializeAndDeserializeNullApplicationContext(attribute); - Assertions.assertNull(attribute.getContentManager()); - Assertions.assertNull(attribute.getPageManager()); - Assertions.assertNull(attribute.getResourceManager()); + Assertions.assertNotNull(attribute.getContentManager()); + Assertions.assertNotNull(attribute.getPageManager()); + Assertions.assertNotNull(attribute.getResourceManager()); + // neither the current context nor the provider are available -> stays null (warn path). + // Raw fields are checked (not the getters) so the self-healing getters do not re-resolve. + attribute = testSerializeAndDeserializeNoContextAvailable(attribute); + Assertions.assertNull(ReflectionTestUtils.getField(attribute, "contentManager")); + Assertions.assertNull(ReflectionTestUtils.getField(attribute, "pageManager")); + Assertions.assertNull(ReflectionTestUtils.getField(attribute, "resourceManager")); } @Test @@ -80,33 +95,58 @@ void testSerializeLinkAttributeAttribute() throws Exception { attribute.setResourceManager(resourceManager); attribute.setLinkResolverManager(linkResolverManager); attribute.setLangManager(langManager); + // current web application context available attribute = testSerializeAndDeserialize(attribute); Assertions.assertNotNull(attribute.getContentManager()); Assertions.assertNotNull(attribute.getPageManager()); Assertions.assertNotNull(attribute.getResourceManager()); Assertions.assertNotNull(attribute.getLinkResolverManager()); Assertions.assertNotNull(ReflectionTestUtils.invokeGetterMethod(attribute, "langManager")); + // no current web application context -> re-wired through the ApplicationContextProvider fallback attribute = testSerializeAndDeserializeNullApplicationContext(attribute); - Assertions.assertNull(attribute.getContentManager()); - Assertions.assertNull(attribute.getPageManager()); - Assertions.assertNull(attribute.getResourceManager()); - Assertions.assertNull(attribute.getLinkResolverManager()); - Assertions.assertNull(ReflectionTestUtils.invokeGetterMethod(attribute, "langManager")); + Assertions.assertNotNull(attribute.getContentManager()); + Assertions.assertNotNull(attribute.getPageManager()); + Assertions.assertNotNull(attribute.getResourceManager()); + Assertions.assertNotNull(attribute.getLinkResolverManager()); + // neither the current context nor the provider are available -> stays null (warn path). + // Raw fields are checked (not the getters) so the self-healing getters do not re-resolve. + attribute = testSerializeAndDeserializeNoContextAvailable(attribute); + Assertions.assertNull(ReflectionTestUtils.getField(attribute, "contentManager")); + Assertions.assertNull(ReflectionTestUtils.getField(attribute, "pageManager")); + Assertions.assertNull(ReflectionTestUtils.getField(attribute, "resourceManager")); + Assertions.assertNull(ReflectionTestUtils.getField(attribute, "linkResolverManager")); + Assertions.assertNull(ReflectionTestUtils.getField(attribute, "_langManager")); } @Test void testSerializeResourceAttributeHandler() throws Exception { ResourceAttributeHandler attributeHandler = new ResourceAttributeHandler(); attributeHandler.setResourceManager(resourceManager); + // current web application context available attributeHandler = testSerializeAndDeserialize(attributeHandler); Assertions.assertNotNull(ReflectionTestUtils.invokeGetterMethod(attributeHandler, "resourceManager")); + // no current web application context -> re-wired through the ApplicationContextProvider fallback attributeHandler = testSerializeAndDeserializeNullApplicationContext(attributeHandler); - Assertions.assertNull(ReflectionTestUtils.invokeGetterMethod(attributeHandler, "resourceManager")); + Assertions.assertNotNull(ReflectionTestUtils.invokeGetterMethod(attributeHandler, "resourceManager")); + // neither the current context nor the provider are available -> stays null (warn path). + attributeHandler = testSerializeAndDeserializeNoContextAvailable(attributeHandler); + Assertions.assertNull(ReflectionTestUtils.getField(attributeHandler, "resourceManager")); } private T testSerializeAndDeserializeNullApplicationContext(T attribute) throws Exception { try (MockedStatic contextLoader = Mockito.mockStatic(ContextLoader.class)) { - contextLoader.when(() -> ContextLoader.getCurrentWebApplicationContext()).thenReturn(null); + contextLoader.when(ContextLoader::getCurrentWebApplicationContext).thenReturn(null); + return testSerializeAndDeserialize(attribute); + } + } + + private T testSerializeAndDeserializeNoContextAvailable(T attribute) throws Exception { + try (MockedStatic contextLoader = Mockito.mockStatic(ContextLoader.class); + MockedStatic provider = + Mockito.mockStatic(ApplicationContextProvider.class)) { + contextLoader.when(ContextLoader::getCurrentWebApplicationContext).thenReturn(null); + provider.when(() -> ApplicationContextProvider.resolveBean(Mockito.any())).thenReturn(null); + provider.when(ApplicationContextProvider::getBeanFactory).thenReturn(null); return testSerializeAndDeserialize(attribute); } } diff --git a/engine/src/main/java/com/agiletec/aps/system/common/entity/model/attribute/AbstractAttribute.java b/engine/src/main/java/com/agiletec/aps/system/common/entity/model/attribute/AbstractAttribute.java index 24d4d8a7c2..62c9a18e5f 100644 --- a/engine/src/main/java/com/agiletec/aps/system/common/entity/model/attribute/AbstractAttribute.java +++ b/engine/src/main/java/com/agiletec/aps/system/common/entity/model/attribute/AbstractAttribute.java @@ -22,6 +22,7 @@ import com.agiletec.aps.system.common.entity.parse.attribute.AttributeHandlerInterface; import com.agiletec.aps.system.common.searchengine.IndexableAttributeInterface; import com.agiletec.aps.system.services.lang.ILangManager; +import com.agiletec.aps.util.ApplicationContextProvider; import com.agiletec.aps.util.ApsProperties; import com.agiletec.aps.util.ApsPropertiesDOM; import java.io.IOException; @@ -38,8 +39,6 @@ import org.jdom2.Element; import org.jdom2.output.XMLOutputter; import org.springframework.beans.factory.BeanFactory; -import org.springframework.web.context.ContextLoader; -import org.springframework.web.context.WebApplicationContext; /** * This abstract class must be used when implementing Entity Attributes. @@ -606,6 +605,7 @@ public void setAttributeManagerClassName(String attributeManagerClassName) { @Deprecated protected ILangManager getLangManager() { + this._langManager = ApplicationContextProvider.resolveIfNull(this._langManager, ILangManager.class); return _langManager; } @@ -617,9 +617,9 @@ public void setLangManager(ILangManager langManager) { private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); - WebApplicationContext ctx = ContextLoader.getCurrentWebApplicationContext(); - if (ctx != null) { - this.setLangManager(ctx.getBean(ILangManager.class)); + ILangManager langManager = ApplicationContextProvider.resolveBean(ILangManager.class); + if (langManager != null) { + this.setLangManager(langManager); } else { _logger.warn("Null WebApplicationContext during deserialization of Attribute '{}'", this.getName()); } diff --git a/engine/src/main/java/com/agiletec/aps/system/common/entity/model/attribute/EnumeratorAttribute.java b/engine/src/main/java/com/agiletec/aps/system/common/entity/model/attribute/EnumeratorAttribute.java index 33d7543794..51f706a74b 100644 --- a/engine/src/main/java/com/agiletec/aps/system/common/entity/model/attribute/EnumeratorAttribute.java +++ b/engine/src/main/java/com/agiletec/aps/system/common/entity/model/attribute/EnumeratorAttribute.java @@ -14,6 +14,7 @@ package com.agiletec.aps.system.common.entity.model.attribute; import com.agiletec.aps.system.common.entity.model.attribute.util.EnumeratorAttributeItemsExtractor; +import com.agiletec.aps.util.ApplicationContextProvider; import java.io.IOException; import java.util.List; import java.util.regex.Pattern; @@ -188,6 +189,7 @@ public void setCustomSeparator(String customSeparator) { @Deprecated protected BeanFactory getBeanFactory() { + this._beanFactory = ApplicationContextProvider.beanFactoryIfNull(this._beanFactory); return this._beanFactory; } @@ -219,7 +221,12 @@ private void readObject(java.io.ObjectInputStream in) } else if (ctx != null) { this.setBeanFactory(ctx); } else { - _logger.warn("Null WebApplicationContext during deserialization of Attribute '{}'", this.getName()); + BeanFactory fallback = ApplicationContextProvider.getBeanFactory(); + if (fallback != null) { + this.setBeanFactory(fallback); + } else { + _logger.warn("Null WebApplicationContext during deserialization of Attribute '{}'", this.getName()); + } } } } diff --git a/engine/src/main/java/com/agiletec/aps/util/ApplicationContextProvider.java b/engine/src/main/java/com/agiletec/aps/util/ApplicationContextProvider.java new file mode 100644 index 0000000000..1b50381457 --- /dev/null +++ b/engine/src/main/java/com/agiletec/aps/util/ApplicationContextProvider.java @@ -0,0 +1,128 @@ +/* + * Copyright 2015-Present Entando Inc. (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 com.agiletec.aps.util; + +import java.util.concurrent.atomic.AtomicReference; +import org.entando.entando.ent.util.EntLogging.EntLogFactory; +import org.entando.entando.ent.util.EntLogging.EntLogger; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.web.context.ContextLoader; +import org.springframework.web.context.WebApplicationContext; + +/** + * Thread-independent, startup-safe holder for the root application bean factory. + * + *

Implemented as a {@link BeanFactoryPostProcessor} so the reference is captured during + * {@code invokeBeanFactoryPostProcessors}, which always completes before {@code preInstantiateSingletons}. + * This guarantees the factory is available before any singleton initialization (including the + * startup post-init self-REST seeding) and before any attribute deserialization can occur.

+ * + *

It is used as a fallback for re-wiring transient manager references on attributes that + * are deserialized on threads where {@code ContextLoader.getCurrentWebApplicationContext()} returns + * {@code null} (e.g. the Redis/lettuce event loop, or the main thread while still inside + * {@code refresh()} during startup).

+ */ +public class ApplicationContextProvider implements BeanFactoryPostProcessor { + + private static final EntLogger logger = EntLogFactory.getSanitizedLogger(ApplicationContextProvider.class); + + private static final AtomicReference BEAN_FACTORY = new AtomicReference<>(); + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + BEAN_FACTORY.set(beanFactory); + } + + /** + * @return the captured bean factory, or {@code null} if not available yet. + */ + public static ConfigurableListableBeanFactory getBeanFactory() { + return BEAN_FACTORY.get(); + } + + /** + * Resolves a singleton bean by type from the captured bean factory. + * + * @param type the bean type. + * @return the bean instance, or {@code null} if the factory is not available yet or the bean + * cannot be resolved (callers must treat {@code null} as "not available"). + */ + public static T getBean(Class type) { + ConfigurableListableBeanFactory factory = BEAN_FACTORY.get(); + if (factory == null) { + return null; + } + try { + return factory.getBean(type); + } catch (BeansException e) { + logger.warn("Unable to resolve bean of type '{}' from ApplicationContextProvider", type.getName(), e); + return null; + } + } + + /** + * Resolves a singleton bean trying the current {@link WebApplicationContext} first (preserving the + * original behaviour), then falling back to the captured bean factory when the current context is + * not available on the calling thread. + * + * @param type the bean type. + * @return the bean instance, or {@code null} if it cannot be resolved from either source. + */ + public static T resolveBean(Class type) { + WebApplicationContext ctx = ContextLoader.getCurrentWebApplicationContext(); + if (ctx != null) { + try { + return ctx.getBean(type); + } catch (BeansException e) { + logger.warn("Unable to resolve bean of type '{}' from current WebApplicationContext", type.getName(), e); + } + } + return getBean(type); + } + + /** + * Keeps an already wired reference, resolving it only when missing. Meant for the "self-healing" + * getters of the attributes whose transient manager references may not have survived + * deserialization or JAXB/SAX instantiation. + * + * @param current the reference currently held by the caller, possibly {@code null}. + * @param type the bean type to resolve when {@code current} is {@code null}. + * @return {@code current} when already set, otherwise the resolved bean (which may be + * {@code null} when the context is not available yet). + */ + public static T resolveIfNull(T current, Class type) { + return (null != current) ? current : resolveBean(type); + } + + /** + * Same as {@link #resolveIfNull(Object, Class)} for the bean factory itself, which cannot be + * resolved by type. + * + * @param current the factory currently held by the caller, possibly {@code null}. + * @return {@code current} when already set, otherwise the captured bean factory. + */ + public static BeanFactory beanFactoryIfNull(BeanFactory current) { + return (null != current) ? current : getBeanFactory(); + } + + /** + * Test hook to reset the captured factory. + */ + public static void clear() { + BEAN_FACTORY.set(null); + } +} diff --git a/engine/src/main/java/org/entando/entando/aps/servlet/StartupListener.java b/engine/src/main/java/org/entando/entando/aps/servlet/StartupListener.java index 75d527ba12..52771ce5e1 100644 --- a/engine/src/main/java/org/entando/entando/aps/servlet/StartupListener.java +++ b/engine/src/main/java/org/entando/entando/aps/servlet/StartupListener.java @@ -18,6 +18,7 @@ import com.agiletec.aps.util.ApsWebApplicationUtils; import jakarta.servlet.*; import org.entando.entando.aps.system.exception.CSRFProtectionException; +import org.entando.entando.aps.system.init.InitializerManager; import org.entando.entando.aps.system.services.tenants.ITenantInitializerService; import org.entando.entando.aps.system.services.tenants.ITenantInitializerService.InitializationTenantFilter; import org.entando.entando.aps.util.UrlUtils; @@ -54,6 +55,14 @@ public void contextInitialized(ServletContextEvent event) { msg = this.getClass().getName() + ": INIT DONE " + svCtx.getServletContextName(); ApsSystemUtils.directStdoutTrace(msg, true); + // Deferred from SystemPostProcessor: execute the post-init processes (self-REST seeding + // of default content types/models/contents) only now that the WebApplicationContext is fully + // refreshed and registered as current. Running them during refresh() exposed the seeding + // calls - and the background threads they trigger (e.g. Redis/lettuce deserialization, + // event notification) - to a half-initialized context. + InitializerManager initializerManager = ApsWebApplicationUtils.getBean(InitializerManager.class, svCtx); + initializerManager.executePostInitProcesses(); + boolean isActive = Objects.nonNull(System.getenv(SystemConstants.ENTANDO_CSRF_PROTECTION)); String whiteList = System.getenv(SystemConstants.ENTANDO_CSRF_ALLOWED_DOMAINS); diff --git a/engine/src/main/java/org/entando/entando/aps/system/init/SystemPostProcessor.java b/engine/src/main/java/org/entando/entando/aps/system/init/SystemPostProcessor.java index ab064f178f..dd2f58ec6d 100644 --- a/engine/src/main/java/org/entando/entando/aps/system/init/SystemPostProcessor.java +++ b/engine/src/main/java/org/entando/entando/aps/system/init/SystemPostProcessor.java @@ -18,21 +18,26 @@ /** * @author E.Santoboni + * @deprecated the post-init processes are no longer executed during the Spring refresh phase: + * doing so ran the self-REST seeding against a half-initialized context (causing failures with + * background threads, e.g. Redis deserialization, on first boot). They are now triggered by + * {@link org.entando.entando.aps.servlet.StartupListener} after the WebApplicationContext is + * fully refreshed and registered. This class is kept as a no-op to avoid breaking external + * Spring configurations that may still declare it. */ +@Deprecated(since = "7.5.1") public class SystemPostProcessor implements BeanPostProcessor { - + @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { //Nothing to do return bean; } - + @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof InitializerManager) { - ((InitializerManager) bean).executePostInitProcesses(); - } + //Nothing to do: post-init processes are executed by StartupListener after context refresh return bean; } - + } diff --git a/engine/src/main/resources/spring/aps/servicesConfig.xml b/engine/src/main/resources/spring/aps/servicesConfig.xml index 65c9905104..52ffa92947 100644 --- a/engine/src/main/resources/spring/aps/servicesConfig.xml +++ b/engine/src/main/resources/spring/aps/servicesConfig.xml @@ -9,6 +9,10 @@ + + + diff --git a/engine/src/main/resources/spring/baseSystemConfig.xml b/engine/src/main/resources/spring/baseSystemConfig.xml index 2f49fca8cc..67e8239418 100644 --- a/engine/src/main/resources/spring/baseSystemConfig.xml +++ b/engine/src/main/resources/spring/baseSystemConfig.xml @@ -142,7 +142,9 @@ - + diff --git a/engine/src/test/java/com/agiletec/ConfigTestUtils.java b/engine/src/test/java/com/agiletec/ConfigTestUtils.java index c5584df7f6..58905717aa 100644 --- a/engine/src/test/java/com/agiletec/ConfigTestUtils.java +++ b/engine/src/test/java/com/agiletec/ConfigTestUtils.java @@ -20,6 +20,7 @@ import jakarta.servlet.ServletContext; import org.apache.commons.dbcp2.BasicDataSource; +import org.entando.entando.aps.system.init.InitializerManager; import org.entando.entando.ent.util.EntLogging; import org.springframework.context.ApplicationContext; import org.springframework.context.support.AbstractApplicationContext; @@ -55,6 +56,10 @@ public ApplicationContext createApplicationContext(ServletContext srvCtx) { ContextLoader contextLoader = new ContextLoader(applicationContext); contextLoader.initWebApplicationContext(srvCtx); applicationContext.refresh(); + // The post-init processes are no longer triggered while the context is being refreshed: + // in production the StartupListener runs them once the context is fully initialized, so the + // test context replicates the very same behaviour here. + applicationContext.getBean(InitializerManager.class).executePostInitProcesses(); return applicationContext; } diff --git a/engine/src/test/java/com/agiletec/aps/system/common/entity/model/attribute/AttributeSerializationIntegrationTest.java b/engine/src/test/java/com/agiletec/aps/system/common/entity/model/attribute/AttributeSerializationIntegrationTest.java index 6b6678f8c7..44bca9e86f 100644 --- a/engine/src/test/java/com/agiletec/aps/system/common/entity/model/attribute/AttributeSerializationIntegrationTest.java +++ b/engine/src/test/java/com/agiletec/aps/system/common/entity/model/attribute/AttributeSerializationIntegrationTest.java @@ -3,6 +3,7 @@ import com.agiletec.aps.BaseTestCase; import com.agiletec.aps.system.SystemConstants; import com.agiletec.aps.system.services.lang.ILangManager; +import com.agiletec.aps.util.ApplicationContextProvider; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; @@ -13,6 +14,7 @@ import org.junit.jupiter.api.parallel.Isolated; import org.mockito.MockedStatic; import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.context.ContextLoader; import org.springframework.web.context.WebApplicationContext; @@ -31,10 +33,17 @@ void testSerializeTextAttribute() throws Exception { TextAttribute attribute = new TextAttribute(); attribute.setName("testAttribute"); attribute.setLangManager(langManager); + // current web application context available -> manager re-wired from it attribute = testSerializeAndDeserializeWithApplicationContext(attribute); Assertions.assertNotNull(attribute.getLangManager()); + // no current web application context (e.g. Redis/lettuce thread or startup): + // manager is re-wired through the ApplicationContextProvider fallback attribute = testSerializeAndDeserializeNullApplicationContext(attribute); - Assertions.assertNull(attribute.getLangManager()); + Assertions.assertNotNull(attribute.getLangManager()); + // neither the current context nor the provider are available -> stays null (warn path). + // The raw field is checked (not the getter) so the self-healing getter does not re-resolve. + attribute = testSerializeAndDeserializeNoContextAvailable(attribute); + Assertions.assertNull(ReflectionTestUtils.getField(attribute, "_langManager")); } @Test @@ -43,12 +52,19 @@ void testSerializeEnumeratorAttribute() throws Exception { attribute.setName("testEnumerator"); attribute.setBeanFactory(this.getApplicationContext()); attribute.setLangManager(langManager); + // current web application context available attribute = testSerializeAndDeserializeWithApplicationContext(attribute); Assertions.assertNotNull(attribute.getBeanFactory()); Assertions.assertNotNull(attribute.getLangManager()); + // no current web application context -> re-wired through the ApplicationContextProvider fallback attribute = testSerializeAndDeserializeNullApplicationContext(attribute); - Assertions.assertNull(attribute.getBeanFactory()); - Assertions.assertNull(attribute.getLangManager()); + Assertions.assertNotNull(attribute.getBeanFactory()); + Assertions.assertNotNull(attribute.getLangManager()); + // neither the current context nor the provider are available -> stays null (warn path). + // The raw fields are checked (not the getters) so the self-healing getters do not re-resolve. + attribute = testSerializeAndDeserializeNoContextAvailable(attribute); + Assertions.assertNull(ReflectionTestUtils.getField(attribute, "_beanFactory")); + Assertions.assertNull(ReflectionTestUtils.getField(attribute, "_langManager")); } private T testSerializeAndDeserializeWithApplicationContext(T attribute) throws Exception { @@ -66,6 +82,17 @@ private T testSerializeAndDeserializeNullApplicationContext(T attribute) thr } } + private T testSerializeAndDeserializeNoContextAvailable(T attribute) throws Exception { + try (MockedStatic contextLoader = Mockito.mockStatic(ContextLoader.class); + MockedStatic provider = + Mockito.mockStatic(ApplicationContextProvider.class)) { + contextLoader.when(ContextLoader::getCurrentWebApplicationContext).thenReturn(null); + provider.when(() -> ApplicationContextProvider.resolveBean(Mockito.any())).thenReturn(null); + provider.when(ApplicationContextProvider::getBeanFactory).thenReturn(null); + return testSerializeAndDeserialize(attribute); + } + } + private T testSerializeAndDeserialize(T attribute) throws Exception { byte[] data; diff --git a/engine/src/test/java/com/agiletec/aps/util/ApplicationContextProviderTest.java b/engine/src/test/java/com/agiletec/aps/util/ApplicationContextProviderTest.java new file mode 100644 index 0000000000..1ae5459b67 --- /dev/null +++ b/engine/src/test/java/com/agiletec/aps/util/ApplicationContextProviderTest.java @@ -0,0 +1,157 @@ +/* + * Copyright 2015-Present Entando Inc. (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 com.agiletec.aps.util; + +import com.agiletec.aps.system.services.lang.ILangManager; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Isolated; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.web.context.ContextLoader; +import org.springframework.web.context.WebApplicationContext; + +@Isolated +class ApplicationContextProviderTest { + + private ConfigurableListableBeanFactory previousBeanFactory; + + @BeforeEach + void setUp() { + this.previousBeanFactory = ApplicationContextProvider.getBeanFactory(); + ApplicationContextProvider.clear(); + } + + @AfterEach + void tearDown() { + ApplicationContextProvider.clear(); + if (null != this.previousBeanFactory) { + new ApplicationContextProvider().postProcessBeanFactory(this.previousBeanFactory); + } + } + + @Test + void shouldCaptureAndReleaseTheBeanFactory() { + ConfigurableListableBeanFactory factory = Mockito.mock(ConfigurableListableBeanFactory.class); + new ApplicationContextProvider().postProcessBeanFactory(factory); + Assertions.assertSame(factory, ApplicationContextProvider.getBeanFactory()); + ApplicationContextProvider.clear(); + Assertions.assertNull(ApplicationContextProvider.getBeanFactory()); + } + + @Test + void getBeanShouldReturnNullWhenTheFactoryIsNotAvailableYet() { + Assertions.assertNull(ApplicationContextProvider.getBean(ILangManager.class)); + } + + @Test + void getBeanShouldReturnTheBeanOfTheCapturedFactory() { + ILangManager langManager = Mockito.mock(ILangManager.class); + new ApplicationContextProvider().postProcessBeanFactory(this.mockFactory(langManager)); + Assertions.assertSame(langManager, ApplicationContextProvider.getBean(ILangManager.class)); + } + + @Test + void getBeanShouldReturnNullWhenTheFactoryCannotResolveTheBean() { + ConfigurableListableBeanFactory factory = Mockito.mock(ConfigurableListableBeanFactory.class); + Mockito.when(factory.getBean(ILangManager.class)) + .thenThrow(new NoSuchBeanDefinitionException(ILangManager.class)); + new ApplicationContextProvider().postProcessBeanFactory(factory); + Assertions.assertNull(ApplicationContextProvider.getBean(ILangManager.class)); + } + + @Test + void resolveBeanShouldPreferTheCurrentWebApplicationContext() { + ILangManager fromContext = Mockito.mock(ILangManager.class); + new ApplicationContextProvider().postProcessBeanFactory(this.mockFactory(Mockito.mock(ILangManager.class))); + WebApplicationContext ctx = Mockito.mock(WebApplicationContext.class); + Mockito.when(ctx.getBean(ILangManager.class)).thenReturn(fromContext); + try (MockedStatic contextLoader = Mockito.mockStatic(ContextLoader.class)) { + contextLoader.when(ContextLoader::getCurrentWebApplicationContext).thenReturn(ctx); + Assertions.assertSame(fromContext, ApplicationContextProvider.resolveBean(ILangManager.class)); + } + } + + @Test + void resolveBeanShouldFallBackWhenNoCurrentWebApplicationContextIsBoundToTheThread() { + ILangManager fromFactory = Mockito.mock(ILangManager.class); + new ApplicationContextProvider().postProcessBeanFactory(this.mockFactory(fromFactory)); + try (MockedStatic contextLoader = Mockito.mockStatic(ContextLoader.class)) { + contextLoader.when(ContextLoader::getCurrentWebApplicationContext).thenReturn(null); + Assertions.assertSame(fromFactory, ApplicationContextProvider.resolveBean(ILangManager.class)); + } + } + + @Test + void resolveBeanShouldFallBackWhenTheCurrentWebApplicationContextCannotResolveTheBean() { + ILangManager fromFactory = Mockito.mock(ILangManager.class); + new ApplicationContextProvider().postProcessBeanFactory(this.mockFactory(fromFactory)); + WebApplicationContext ctx = Mockito.mock(WebApplicationContext.class); + Mockito.when(ctx.getBean(ILangManager.class)) + .thenThrow(new NoSuchBeanDefinitionException(ILangManager.class)); + try (MockedStatic contextLoader = Mockito.mockStatic(ContextLoader.class)) { + contextLoader.when(ContextLoader::getCurrentWebApplicationContext).thenReturn(ctx); + Assertions.assertSame(fromFactory, ApplicationContextProvider.resolveBean(ILangManager.class)); + } + } + + @Test + void resolveBeanShouldReturnNullWhenNoSourceIsAvailable() { + try (MockedStatic contextLoader = Mockito.mockStatic(ContextLoader.class)) { + contextLoader.when(ContextLoader::getCurrentWebApplicationContext).thenReturn(null); + Assertions.assertNull(ApplicationContextProvider.resolveBean(ILangManager.class)); + } + } + + @Test + void resolveIfNullShouldKeepTheReferenceAlreadyWired() { + ILangManager current = Mockito.mock(ILangManager.class); + Assertions.assertSame(current, ApplicationContextProvider.resolveIfNull(current, ILangManager.class)); + } + + @Test + void resolveIfNullShouldResolveAMissingReference() { + ILangManager fromFactory = Mockito.mock(ILangManager.class); + new ApplicationContextProvider().postProcessBeanFactory(this.mockFactory(fromFactory)); + try (MockedStatic contextLoader = Mockito.mockStatic(ContextLoader.class)) { + contextLoader.when(ContextLoader::getCurrentWebApplicationContext).thenReturn(null); + Assertions.assertSame(fromFactory, ApplicationContextProvider.resolveIfNull(null, ILangManager.class)); + } + } + + @Test + void beanFactoryIfNullShouldKeepTheFactoryAlreadyWired() { + ConfigurableListableBeanFactory current = Mockito.mock(ConfigurableListableBeanFactory.class); + Assertions.assertSame(current, ApplicationContextProvider.beanFactoryIfNull(current)); + } + + @Test + void beanFactoryIfNullShouldFallBackToTheCapturedFactory() { + ConfigurableListableBeanFactory factory = Mockito.mock(ConfigurableListableBeanFactory.class); + new ApplicationContextProvider().postProcessBeanFactory(factory); + Assertions.assertSame(factory, ApplicationContextProvider.beanFactoryIfNull(null)); + ApplicationContextProvider.clear(); + Assertions.assertNull(ApplicationContextProvider.beanFactoryIfNull(null)); + } + + private ConfigurableListableBeanFactory mockFactory(ILangManager langManager) { + ConfigurableListableBeanFactory factory = Mockito.mock(ConfigurableListableBeanFactory.class); + Mockito.when(factory.getBean(ILangManager.class)).thenReturn(langManager); + return factory; + } +} diff --git a/engine/src/test/java/org/entando/entando/aps/servlet/StartupListenerTest.java b/engine/src/test/java/org/entando/entando/aps/servlet/StartupListenerTest.java index d623bcc7d0..d63e951293 100644 --- a/engine/src/test/java/org/entando/entando/aps/servlet/StartupListenerTest.java +++ b/engine/src/test/java/org/entando/entando/aps/servlet/StartupListenerTest.java @@ -20,6 +20,7 @@ import jakarta.servlet.ServletRegistration; import jakarta.servlet.SessionCookieConfig; import org.entando.entando.aps.system.exception.CSRFProtectionException; +import org.entando.entando.aps.system.init.InitializerManager; import org.entando.entando.aps.system.services.tenants.ITenantInitializerService; import org.entando.entando.aps.system.services.tenants.ITenantInitializerService.InitializationTenantFilter; import org.junit.jupiter.api.AfterEach; @@ -63,6 +64,9 @@ class StartupListenerTest { @Mock private ITenantInitializerService tenantInitializerService; + @Mock + private InitializerManager initializerManager; + @Mock private SessionCookieConfig sessionCookieConfig; @@ -82,6 +86,7 @@ void setUp() { when(servletContext.getInitParameter(any())).thenReturn(null); when(servletContext.getResourceAsStream(any())).thenReturn(null); when(webApplicationContext.getBean(ITenantInitializerService.class)).thenReturn(tenantInitializerService); + when(webApplicationContext.getBean(InitializerManager.class)).thenReturn(initializerManager); when(tenantInitializerService.startTenantsInitialization(any(), eq(InitializationTenantFilter.REQUIRED_INIT_AT_START))) .thenReturn(CompletableFuture.completedFuture(null)); when(tenantInitializerService.startTenantsInitialization(any(), eq(InitializationTenantFilter.NOT_REQUIRED_INIT_AT_START))) @@ -100,6 +105,8 @@ void testContextInitialized_WithCsrfProtectionEnabled() throws Exception { assertDoesNotThrow(() -> startupListener.contextInitialized(servletContextEvent)); // Assert + // post-init processes are deferred from SystemPostProcessor and executed by the listener + verify(initializerManager).executePostInitProcesses(); verify(tenantInitializerService).startTenantsInitialization( eq(servletContext), eq(InitializationTenantFilter.REQUIRED_INIT_AT_START)); verify(tenantInitializerService).startTenantsInitialization(