diff --git a/core/src/main/java/com/opensymphony/xwork2/config/BeanSelectionProvider.java b/core/src/main/java/com/opensymphony/xwork2/config/BeanSelectionProvider.java
index 687943d364..a62500a14f 100644
--- a/core/src/main/java/com/opensymphony/xwork2/config/BeanSelectionProvider.java
+++ b/core/src/main/java/com/opensymphony/xwork2/config/BeanSelectionProvider.java
@@ -19,7 +19,25 @@
package com.opensymphony.xwork2.config;
/**
- * When implemented allows to alias already existing beans
+ * A {@link ConfigurationProvider} that selects and aliases bean implementations.
+ *
+ * Implementations of this interface are responsible for selecting which bean implementation
+ * to use for a given interface type. The selection is typically based on configuration properties
+ * that specify the bean name or class name.
+ *
+ *
+ * The aliasing mechanism works as follows:
+ *
+ *
+ *
Look for a bean by the name specified in the configuration property
+ *
If found, alias it to the default name so it becomes the default implementation
+ *
If not found, try to load the value as a class name and register it as a factory
+ *
If class loading fails, delegate to {@link org.apache.struts2.ObjectFactory} at runtime
+ * (useful for Spring bean names)
+ *
+ *
+ * @see AbstractBeanSelectionProvider
+ * @see StrutsBeanSelectionProvider
*/
public interface BeanSelectionProvider extends ConfigurationProvider {
diff --git a/core/src/main/java/com/opensymphony/xwork2/config/impl/DefaultConfiguration.java b/core/src/main/java/com/opensymphony/xwork2/config/impl/DefaultConfiguration.java
index d1560f53f1..8990ba129e 100644
--- a/core/src/main/java/com/opensymphony/xwork2/config/impl/DefaultConfiguration.java
+++ b/core/src/main/java/com/opensymphony/xwork2/config/impl/DefaultConfiguration.java
@@ -106,6 +106,8 @@
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.conversion.StrutsConversionPropertiesProcessor;
+import org.apache.struts2.conversion.UserConversionPropertiesProcessor;
+import org.apache.struts2.conversion.UserConversionPropertiesProvider;
import org.apache.struts2.conversion.StrutsTypeConverterCreator;
import org.apache.struts2.conversion.StrutsTypeConverterHolder;
import org.apache.struts2.factory.StrutsResultFactory;
@@ -125,12 +127,8 @@
import java.util.TreeMap;
import java.util.TreeSet;
-
/**
* DefaultConfiguration
- *
- * @author Jason Carreira
- * Created Feb 24, 2003 7:38:06 AM
*/
public class DefaultConfiguration implements Configuration {
@@ -224,7 +222,7 @@ public void addPackageConfig(String name, PackageConfig packageContext) {
name, packageContext.getLocation());
} else {
throw new ConfigurationException("The package name '" + name
- + "' at location "+packageContext.getLocation()
+ + "' at location " + packageContext.getLocation()
+ " is already been used by another package at location " + check.getLocation(),
packageContext);
}
@@ -257,7 +255,6 @@ public void rebuildRuntimeConfiguration() {
*
* @param providers list of ContainerProvider
* @return list of package providers
- *
* @throws ConfigurationException in case of any configuration errors
*/
@Override
@@ -269,8 +266,7 @@ public synchronized List reloadContainer(List type() {
setContext(container);
objectFactory = container.getInstance(ObjectFactory.class);
+ // Trigger late initialization of user conversion properties (WW-4291)
+ // This must happen after full container is built so SpringObjectFactory is available
+ container.getInstance(UserConversionPropertiesProcessor.class);
+
// Process the configuration providers first
- for (final ContainerProvider containerProvider : providers)
- {
+ for (final ContainerProvider containerProvider : providers) {
if (containerProvider instanceof PackageProvider) {
container.inject(containerProvider);
- ((PackageProvider)containerProvider).loadPackages();
- packageProviders.add((PackageProvider)containerProvider);
+ ((PackageProvider) containerProvider).loadPackages();
+ packageProviders.add((PackageProvider) containerProvider);
}
}
@@ -380,6 +379,8 @@ public static ContainerBuilder bootstrapFactories(ContainerBuilder builder) {
.factory(ConversionAnnotationProcessor.class, DefaultConversionAnnotationProcessor.class, Scope.SINGLETON)
.factory(TypeConverterCreator.class, StrutsTypeConverterCreator.class, Scope.SINGLETON)
.factory(TypeConverterHolder.class, StrutsTypeConverterHolder.class, Scope.SINGLETON)
+ .factory(UserConversionPropertiesProvider.class, StrutsConversionPropertiesProcessor.class, Scope.SINGLETON)
+ .factory(UserConversionPropertiesProcessor.class, Scope.SINGLETON)
.factory(TextProvider.class, "system", DefaultTextProvider.class, Scope.SINGLETON)
.factory(LocalizedTextProvider.class, StrutsLocalizedTextProvider.class, Scope.SINGLETON)
@@ -443,10 +444,9 @@ protected synchronized RuntimeConfiguration buildRuntimeConfiguration() throws C
Map actionConfigs = packageConfig.getAllActionConfigs();
- for (Object o : actionConfigs.keySet()) {
- String actionName = (String) o;
- ActionConfig baseConfig = actionConfigs.get(actionName);
- configs.put(actionName, buildFullActionConfig(packageConfig, baseConfig));
+ for (Map.Entry entry : actionConfigs.entrySet()) {
+ ActionConfig baseConfig = entry.getValue();
+ configs.put(entry.getKey(), buildFullActionConfig(packageConfig, baseConfig));
}
namespaceActionConfigs.put(namespace, configs);
@@ -487,8 +487,7 @@ private void setDefaultResults(Map results, PackageConfig
* @param baseConfig the ActionConfig which holds only the configuration specific to itself, without the defaults
* and inheritance
* @return a full ActionConfig for runtime configuration with all of the inherited and default params
- * @throws com.opensymphony.xwork2.config.ConfigurationException
- *
+ * @throws com.opensymphony.xwork2.config.ConfigurationException in case of any configuration errors
*/
private ActionConfig buildFullActionConfig(PackageConfig packageContext, ActionConfig baseConfig) throws ConfigurationException {
Map params = new TreeMap<>(baseConfig.getParams());
@@ -500,7 +499,7 @@ private ActionConfig buildFullActionConfig(PackageConfig packageContext, ActionC
results.putAll(packageContext.getAllGlobalResults());
}
- results.putAll(baseConfig.getResults());
+ results.putAll(baseConfig.getResults());
setDefaultResults(results, packageContext);
@@ -511,7 +510,7 @@ private ActionConfig buildFullActionConfig(PackageConfig packageContext, ActionC
if (defaultInterceptorRefName != null) {
interceptors.addAll(InterceptorBuilder.constructInterceptorReference(new PackageConfig.Builder(packageContext), defaultInterceptorRefName,
- new LinkedHashMap(), packageContext.getLocation(), objectFactory));
+ new LinkedHashMap<>(), packageContext.getLocation(), objectFactory));
}
}
@@ -523,14 +522,14 @@ private ActionConfig buildFullActionConfig(PackageConfig packageContext, ActionC
LOG.debug("Using pattern [{}] to match allowed methods when SMI is disabled!", methodRegex);
return new ActionConfig.Builder(baseConfig)
- .addParams(params)
- .addResultConfigs(results)
- .defaultClassName(packageContext.getDefaultClassRef()) // fill in default if non class has been provided
- .interceptors(interceptors)
- .setStrictMethodInvocation(packageContext.isStrictMethodInvocation())
- .setDefaultMethodRegex(methodRegex)
- .addExceptionMappings(packageContext.getAllExceptionMappingConfigs())
- .build();
+ .addParams(params)
+ .addResultConfigs(results)
+ .defaultClassName(packageContext.getDefaultClassRef()) // fill in default if non class has been provided
+ .interceptors(interceptors)
+ .setStrictMethodInvocation(packageContext.isStrictMethodInvocation())
+ .setDefaultMethodRegex(methodRegex)
+ .addExceptionMappings(packageContext.getAllExceptionMappingConfigs())
+ .build();
}
@@ -546,8 +545,7 @@ public RuntimeConfigurationImpl(Map> namespace
Map namespaceConfigs,
PatternMatcher matcher,
boolean appendNamedParameters,
- boolean fallbackToEmptyNamespace)
- {
+ boolean fallbackToEmptyNamespace) {
this.namespaceActionConfigs = namespaceActionConfigs;
this.namespaceConfigs = namespaceConfigs;
this.fallbackToEmptyNamespace = fallbackToEmptyNamespace;
@@ -630,7 +628,7 @@ private ActionConfig findActionConfigInNamespace(String namespace, String name)
* @return a Map of namespace - > Map of ActionConfig objects, with the key being the action name
*/
@Override
- public Map> getActionConfigs() {
+ public Map> getActionConfigs() {
return namespaceActionConfigs;
}
@@ -664,7 +662,7 @@ public Object setProperty(String key, String value) {
public void setConstants(ContainerBuilder builder) {
for (Object keyobj : keySet()) {
- String key = (String)keyobj;
+ String key = (String) keyobj;
builder.factory(String.class, key, new LocatableConstantFactory<>(getProperty(key), getPropertyLocation(key)));
}
}
diff --git a/core/src/main/java/org/apache/struts2/StrutsConstants.java b/core/src/main/java/org/apache/struts2/StrutsConstants.java
index 44cb014687..e666df61aa 100644
--- a/core/src/main/java/org/apache/struts2/StrutsConstants.java
+++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java
@@ -411,6 +411,7 @@ public final class StrutsConstants {
public static final String STRUTS_CONVERTER_ANNOTATION_PROCESSOR = "struts.converter.annotation.processor";
public static final String STRUTS_CONVERTER_CREATOR = "struts.converter.creator";
public static final String STRUTS_CONVERTER_HOLDER = "struts.converter.holder";
+ public static final String STRUTS_CONVERTER_USER_PROPERTIES_PROVIDER = "struts.converter.userPropertiesProvider";
public static final String STRUTS_EXPRESSION_PARSER = "struts.expression.parser";
diff --git a/core/src/main/java/org/apache/struts2/config/AbstractBeanSelectionProvider.java b/core/src/main/java/org/apache/struts2/config/AbstractBeanSelectionProvider.java
index f672ae0418..903c361b9c 100644
--- a/core/src/main/java/org/apache/struts2/config/AbstractBeanSelectionProvider.java
+++ b/core/src/main/java/org/apache/struts2/config/AbstractBeanSelectionProvider.java
@@ -30,7 +30,44 @@
import java.util.Properties;
/**
- * TODO lukaszlenart: write a JavaDoc
+ * Base implementation of {@link BeanSelectionProvider} that provides bean aliasing functionality.
+ *
+ * This class provides the {@link #alias(Class, String, ContainerBuilder, Properties, Scope)} method
+ * which is used to select and register bean implementations based on configuration properties.
+ *
+ *
+ *
Bean Selection Process
+ *
+ * The {@code alias} method selects a bean implementation using the following process:
+ *
+ *
+ *
Read the property value for the given key from the configuration properties
+ *
If no property is set, use {@value #DEFAULT_BEAN_NAME} as the default bean name
+ *
Check if a bean with that name already exists in the container:
+ *
+ *
If found, alias it to {@link Container#DEFAULT_NAME} making it the default
+ *
If not found, try to load the property value as a fully qualified class name
+ *
+ *
+ *
If class loading succeeds, register the class as a factory for the interface type
+ *
If class loading fails and the name is not the default, create a delegate factory
+ * that will resolve the bean through {@link ObjectFactory} at runtime. This allows
+ * Spring bean names to be used in configuration.
+ *
+ *
+ *
Usage Example
+ *
+ * // In struts.properties or struts.xml:
+ * // struts.objectFactory = spring
+ * // struts.converter.collection = myCustomCollectionConverter
+ *
+ * // In a subclass:
+ * alias(ObjectFactory.class, StrutsConstants.STRUTS_OBJECTFACTORY, builder, props);
+ * alias(CollectionConverter.class, StrutsConstants.STRUTS_CONVERTER_COLLECTION, builder, props);
+ *
+ *
+ * @see BeanSelectionProvider
+ * @see StrutsBeanSelectionProvider
*/
public abstract class AbstractBeanSelectionProvider implements BeanSelectionProvider {
@@ -73,7 +110,7 @@ protected void alias(Class type, String key, ContainerBuilder builder, Propertie
// Perhaps a spring bean id, so we'll delegate to the object factory at runtime
LOG.trace("Choosing bean ({}) for ({}) to be loaded from the ObjectFactory", foundName, type.getName());
if (DEFAULT_BEAN_NAME.equals(foundName)) {
- // Probably an optional bean, will ignore
+ LOG.trace("No bean registered for type ({}) with default name '{}', skipping as optional", type.getName(), DEFAULT_BEAN_NAME);
} else {
if (ObjectFactory.class != type) {
builder.factory(type, new ObjectFactoryDelegateFactory(foundName, type), scope);
@@ -103,7 +140,7 @@ public Object create(Context context) throws Exception {
try {
return objFactory.buildBean(name, null, true);
} catch (ClassNotFoundException ex) {
- throw new ConfigurationException("Unable to load bean "+type.getName()+" ("+name+")");
+ throw new ConfigurationException(String.format("Unable to load bean %s (name = %s)", type.getName(), name));
}
}
diff --git a/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java b/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java
index e914b04524..e956d92865 100644
--- a/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java
+++ b/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java
@@ -65,6 +65,7 @@
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.components.UrlRenderer;
import org.apache.struts2.components.date.DateFormatter;
+import org.apache.struts2.conversion.UserConversionPropertiesProvider;
import org.apache.struts2.dispatcher.DispatcherErrorHandler;
import org.apache.struts2.dispatcher.StaticContentLoader;
import org.apache.struts2.dispatcher.mapper.ActionMapper;
@@ -88,7 +89,7 @@
*
*
* The following is a list of the allowed extension points:
- *
+ *
*
*
*
@@ -353,7 +354,7 @@
*
Provides access to resource bundles used to localise messages (since 2.5.11)
*
*
- *
+ *
*
*
*
@@ -405,6 +406,7 @@ public void register(ContainerBuilder builder, LocatableProperties props) {
alias(ConversionAnnotationProcessor.class, StrutsConstants.STRUTS_CONVERTER_ANNOTATION_PROCESSOR, builder, props);
alias(TypeConverterCreator.class, StrutsConstants.STRUTS_CONVERTER_CREATOR, builder, props);
alias(TypeConverterHolder.class, StrutsConstants.STRUTS_CONVERTER_HOLDER, builder, props);
+ alias(UserConversionPropertiesProvider.class, StrutsConstants.STRUTS_CONVERTER_USER_PROPERTIES_PROVIDER, builder, props);
alias(TextProvider.class, StrutsConstants.STRUTS_TEXT_PROVIDER, builder, props, Scope.PROTOTYPE);
alias(TextProviderFactory.class, StrutsConstants.STRUTS_TEXT_PROVIDER_FACTORY, builder, props, Scope.PROTOTYPE);
diff --git a/core/src/main/java/org/apache/struts2/conversion/StrutsConversionPropertiesProcessor.java b/core/src/main/java/org/apache/struts2/conversion/StrutsConversionPropertiesProcessor.java
index 0f509dfb47..a7257f0a07 100644
--- a/core/src/main/java/org/apache/struts2/conversion/StrutsConversionPropertiesProcessor.java
+++ b/core/src/main/java/org/apache/struts2/conversion/StrutsConversionPropertiesProcessor.java
@@ -35,7 +35,7 @@
import java.util.Map;
import java.util.Properties;
-public class StrutsConversionPropertiesProcessor implements ConversionPropertiesProcessor, EarlyInitializable {
+public class StrutsConversionPropertiesProcessor implements ConversionPropertiesProcessor, EarlyInitializable, UserConversionPropertiesProvider {
private static final Logger LOG = LogManager.getLogger(StrutsConversionPropertiesProcessor.class);
@@ -58,8 +58,27 @@ public void setTypeConverterHolder(TypeConverterHolder converterHolder) {
@Override
public void init() {
- LOG.debug("Processing default conversion properties files");
+ // Early phase: Only process framework defaults (class names only)
+ // User properties are processed later in initUserConversions() when
+ // SpringObjectFactory is available for bean name resolution (WW-4291)
+ LOG.debug("Processing default conversion properties files (early phase)");
processRequired(STRUTS_DEFAULT_CONVERSION_PROPERTIES);
+ }
+
+ /**
+ * Process user conversion properties. Called during late initialization
+ * when SpringObjectFactory is available for bean name resolution.
+ *
+ * This allows users to reference Spring bean names in struts-conversion.properties
+ * instead of only fully qualified class names.
+ *
+ *
+ * @see WW-4291
+ * @since 7.2.0
+ */
+ @Override
+ public void initUserConversions() {
+ LOG.debug("Processing user conversion properties files (late phase)");
process(STRUTS_CONVERSION_PROPERTIES);
process(XWORK_CONVERSION_PROPERTIES);
}
@@ -78,7 +97,7 @@ public void loadConversionProperties(String propsName, boolean require) {
while (resources.hasNext()) {
if (XWORK_CONVERSION_PROPERTIES.equals(propsName)) {
LOG.warn("Instead of using deprecated {} please use the new file name {}",
- XWORK_CONVERSION_PROPERTIES, STRUTS_CONVERSION_PROPERTIES);
+ XWORK_CONVERSION_PROPERTIES, STRUTS_CONVERSION_PROPERTIES);
}
URL url = resources.next();
Properties props = new Properties();
@@ -86,8 +105,7 @@ public void loadConversionProperties(String propsName, boolean require) {
LOG.debug("Processing conversion file [{}]", propsName);
- for (Object o : props.entrySet()) {
- Map.Entry entry = (Map.Entry) o;
+ for (Map.Entry