Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
88fd4c7
WW-3476 docs: add design spec for package-scoped constants
lukaszlenart Oct 10, 2026
43115aa
WW-3476 docs: add implementation plan for package-scoped constants
lukaszlenart Oct 10, 2026
b2a5b0d
WW-3476 feat(config): store scoped constants on PackageConfig
lukaszlenart Oct 10, 2026
06d6e3c
WW-3476 feat(config): add struts-7.5.dtd with <scoped-constant>
lukaszlenart Oct 10, 2026
2050da7
WW-3476 feat(config): reject unregistered scoped constants at startup
lukaszlenart Oct 10, 2026
617faf1
WW-3476 feat(config): add ScopedConstantProvider resolving package ov…
lukaszlenart Oct 10, 2026
545c570
WW-3476 test(config): pin ScopedConstantProvider wiring on the Dispat…
lukaszlenart Oct 10, 2026
4931d34
WW-3476 test(config): drop the xwork prefix from scoped-constant fixt…
lukaszlenart Oct 10, 2026
6562ac2
WW-3476 fix(config): skip ScopableConstants bean names without an ins…
lukaszlenart Oct 10, 2026
dc4305a
WW-3476 refactor(config): resolve Configuration and scopable names fr…
lukaszlenart Oct 10, 2026
7645bc6
WW-3476 refactor(config): address Sonar findings on scoped constants
lukaszlenart Oct 10, 2026
2b25184
WW-3476 test(config): name scoped-constant fixtures struts-*.xml
lukaszlenart Oct 10, 2026
197f2cc
WW-3476 fix(mock): return an empty set from MockContainer.getInstance…
lukaszlenart Oct 10, 2026
8a81250
WW-3476 docs(config): document ScopedConstantProvider as a bean-selec…
lukaszlenart Oct 10, 2026
c939805
WW-3476 refactor(config): move StrutsScopedConstantProvider to config…
lukaszlenart Oct 11, 2026
c8fa01c
WW-3476 refactor(config): complete the StrutsScopedConstantProvider move
lukaszlenart Oct 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions core/src/main/java/org/apache/struts2/StrutsConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,14 @@ public final class StrutsConstants {
*/
public static final String STRUTS_PARAMETER_ALLOWLISTER = "struts.parameterAllowlister";

/**
* Name of the Struts bean implementing {@link org.apache.struts2.config.ScopedConstantProvider}, selected by
* {@link org.apache.struts2.config.StrutsBeanSelectionProvider}.
*
* @since 7.5.0
*/
public static final String STRUTS_SCOPED_CONSTANT_PROVIDER = "struts.scopedConstantProvider";
Comment thread
lukaszlenart marked this conversation as resolved.

/**
* Enables evaluation of OGNL expressions
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.config;

import org.apache.struts2.inject.Container;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/**
* Declares constants a package may override with {@code <scoped-constant>}. Register implementations as named beans;
* the scopable set is the union over all of them.
*
* @since 7.5.0
*/
public interface ScopableConstants {

Set<String> getNames();

static Set<String> collectNames(Container container) {
Set<String> names = new HashSet<>();
for (String beanName : container.getInstanceNames(ScopableConstants.class)) {
ScopableConstants scopableConstants = container.getInstance(ScopableConstants.class, beanName);
if (scopableConstants != null) {
names.addAll(scopableConstants.getNames());
}
}
return Collections.unmodifiableSet(names);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.config;

/**
* Resolves a scopable constant for the current action invocation: the package's {@code <scoped-constant>}, inherited
* through {@code extends}, or else the global constant.
*
* @since 7.5.0
*/
public interface ScopedConstantProvider {

/**
* @param name a constant registered through a {@link ScopableConstants} bean
* @return the scoped value, or the global value ({@code null} when there is none)
* @throws IllegalArgumentException when {@code name} is not scopable
*/
String getValue(String name);
}
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,12 @@
* <td>singleton</td>
* <td>Provides access to resource bundles used to localise messages (since 2.5.11)</td>
* </tr>
* <tr>
* <td>org.apache.struts2.config.ScopedConstantProvider</td>
* <td>struts.scopedConstantProvider</td>
* <td>singleton</td>
* <td>Resolves package-scoped constants declared with &lt;scoped-constant&gt; (since 7.5.0)</td>
* </tr>
* </table>
* <p>
* <!-- END SNIPPET: extensionPoints -->
Expand Down Expand Up @@ -454,6 +460,7 @@ public void register(ContainerBuilder builder, LocatableProperties props) {
alias(ProxyService.class, StrutsConstants.STRUTS_PROXYSERVICE, builder, props, Scope.SINGLETON);
alias(ParameterAuthorizer.class, StrutsConstants.STRUTS_PARAMETER_AUTHORIZER, builder, props, Scope.SINGLETON);
alias(ParameterAllowlister.class, StrutsConstants.STRUTS_PARAMETER_ALLOWLISTER, builder, props, Scope.SINGLETON);
alias(ScopedConstantProvider.class, StrutsConstants.STRUTS_SCOPED_CONSTANT_PROVIDER, builder, props, Scope.SINGLETON);

alias(SecurityMemberAccess.class, StrutsConstants.STRUTS_MEMBER_ACCESS, builder, props, Scope.PROTOTYPE);
alias(OgnlGuard.class, StrutsConstants.STRUTS_OGNL_GUARD, builder, props, Scope.SINGLETON);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ public class StrutsXmlConfigurationProvider extends XmlConfigurationProvider {
"-//Apache Software Foundation//DTD Struts Configuration 2.3//EN", "struts-2.3.dtd",
"-//Apache Software Foundation//DTD Struts Configuration 2.5//EN", "struts-2.5.dtd",
"-//Apache Software Foundation//DTD Struts Configuration 6.0//EN", "struts-6.0.dtd",
"-//Apache Software Foundation//DTD Struts Configuration 6.5//EN", "struts-6.5.dtd");
"-//Apache Software Foundation//DTD Struts Configuration 6.5//EN", "struts-6.5.dtd",
"-//Apache Software Foundation//DTD Struts Configuration 7.5//EN", "struts-7.5.dtd");

private File baseDir = null;
private final String filename;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ public class PackageConfig extends Located implements Comparable<PackageConfig>,
protected boolean isFinal = false; // a final package is unextendable
protected boolean needsRefresh;
protected boolean strictMethodInvocation = true;
private Map<String, String> scopedConstants;
private Map<String, String> allScopedConstants;

protected PackageConfig(String name) {
this.name = name;
Expand All @@ -69,6 +71,8 @@ protected PackageConfig(String name) {
resultTypeConfigs = new LinkedHashMap<>();
globalExceptionMappingConfigs = new ArrayList<>();
parents = new ArrayList<>();
scopedConstants = new LinkedHashMap<>();
allScopedConstants = Collections.emptyMap();
}

protected PackageConfig(PackageConfig orig) {
Expand All @@ -90,6 +94,8 @@ protected PackageConfig(PackageConfig orig) {
this.parents = new ArrayList<>(orig.parents);
this.location = orig.location;
this.strictMethodInvocation = orig.strictMethodInvocation;
this.scopedConstants = new LinkedHashMap<>(orig.scopedConstants);
Comment thread
lukaszlenart marked this conversation as resolved.
this.allScopedConstants = orig.allScopedConstants;
Comment thread
lukaszlenart marked this conversation as resolved.
}

public boolean isAbstract() {
Expand Down Expand Up @@ -189,6 +195,22 @@ public Map<String, ResultTypeConfig> getAllResultTypeConfigs() {
return retMap;
}

/**
* @return scoped constants declared by this package itself, without inherited ones
* @since 7.5.0
*/
public Map<String, String> getScopedConstants() {
return scopedConstants;
}

/**
* @return scoped constants in effect for this package: inherited ones, overridden by its own
* @since 7.5.0
*/
public Map<String, String> getAllScopedConstants() {
return allScopedConstants;
}

/**
* returns the List of all the ExceptionMappingConfigs available in the current package.
* ExceptionMappingConfigs defined in ancestor packages will be included in this list.
Expand Down Expand Up @@ -391,6 +413,7 @@ public boolean equals(Object o) {
if (!Objects.equals(globalExceptionMappingConfigs, that.globalExceptionMappingConfigs))
return false;
if (!Objects.equals(parents, that.parents)) return false;
if (!Objects.equals(scopedConstants, that.scopedConstants)) return false;
if (!Objects.equals(defaultInterceptorRef, that.defaultInterceptorRef))
return false;
if (!Objects.equals(defaultActionRef, that.defaultActionRef))
Expand All @@ -412,6 +435,7 @@ public int hashCode() {
result = 31 * result + (resultTypeConfigs != null ? resultTypeConfigs.hashCode() : 0);
result = 31 * result + (globalExceptionMappingConfigs != null ? globalExceptionMappingConfigs.hashCode() : 0);
result = 31 * result + (parents != null ? parents.hashCode() : 0);
result = 31 * result + (scopedConstants != null ? scopedConstants.hashCode() : 0);
result = 31 * result + (defaultInterceptorRef != null ? defaultInterceptorRef.hashCode() : 0);
result = 31 * result + (defaultActionRef != null ? defaultActionRef.hashCode() : 0);
result = 31 * result + (defaultResultType != null ? defaultResultType.hashCode() : 0);
Expand Down Expand Up @@ -605,6 +629,14 @@ public Builder addResultTypeConfig(ResultTypeConfig config) {
return this;
}

/**
* @since 7.5.0
*/
public Builder addScopedConstant(String name, String value) {
target.scopedConstants.put(name, value);
return this;
}

public Builder location(Location loc) {
target.location = loc;
return this;
Expand Down Expand Up @@ -655,11 +687,22 @@ public PackageConfig build() {
target.resultTypeConfigs = Collections.unmodifiableMap(target.resultTypeConfigs);
target.globalExceptionMappingConfigs = Collections.unmodifiableList(target.globalExceptionMappingConfigs);
target.parents = Collections.unmodifiableList(target.parents);
target.scopedConstants = Collections.unmodifiableMap(target.scopedConstants);
target.allScopedConstants = mergeScopedConstants(target);
PackageConfig result = target;
target = new PackageConfig(result);
return result;
}

private static Map<String, String> mergeScopedConstants(PackageConfig config) {
Map<String, String> merged = new LinkedHashMap<>();
for (PackageConfig parent : config.parents) {
merged.putAll(parent.getAllScopedConstants());
}
merged.putAll(config.scopedConstants);
return Collections.unmodifiableMap(merged);
}

@Override
public String toString() {
return "[BUILDER] " + target.toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.apache.struts2.config.FileManagerProvider;
import org.apache.struts2.config.PackageProvider;
import org.apache.struts2.config.RuntimeConfiguration;
import org.apache.struts2.config.ScopableConstants;
import org.apache.struts2.config.entities.ActionConfig;
import org.apache.struts2.config.entities.InterceptorMapping;
import org.apache.struts2.config.entities.PackageConfig;
Expand Down Expand Up @@ -449,6 +450,8 @@ public static ContainerBuilder bootstrapTypeConverters(ContainerBuilder builder)
* @throws ConfigurationException in case of any configuration errors
*/
protected synchronized RuntimeConfiguration buildRuntimeConfiguration() throws ConfigurationException {
validateScopedConstants();

Map<String, Map<String, ActionConfig>> namespaceActionConfigs = new LinkedHashMap<>();
Map<String, String> namespaceConfigs = new LinkedHashMap<>();

Expand Down Expand Up @@ -488,6 +491,28 @@ protected synchronized RuntimeConfiguration buildRuntimeConfiguration() throws C
Collections.unmodifiableMap(namespaceConfigs), matcher, appendNamedParameters, fallbackToEmptyNamespace);
}

/**
* @since 7.5.0
*/
protected void validateScopedConstants() throws ConfigurationException {
Set<String> scopableNames = ScopableConstants.collectNames(container);
for (PackageConfig packageConfig : packageContexts.values()) {
for (String name : packageConfig.getScopedConstants().keySet()) {
if (!scopableNames.contains(name)) {
throw new ConfigurationException(String.format("Package [%s] declares scoped constant [%s], which is not scopable. %s",
packageConfig.getName(), name, describeScopable(scopableNames)), packageConfig);
}
}
}
}

private static String describeScopable(Set<String> scopableNames) {
if (scopableNames.isEmpty()) {
return "No constants are scopable in this configuration.";
}
return "Scopable constants: " + new TreeSet<>(scopableNames) + ".";
}

private void setDefaultResults(Map<String, ResultConfig> results, PackageConfig packageContext) {
String defaultResult = packageContext.getFullDefaultResultType();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.apache.struts2.config.Configuration;
import org.apache.struts2.config.ConfigurationException;
import org.apache.struts2.config.ConfigurationProvider;
import org.apache.struts2.config.ScopedConstantProvider;
import org.apache.struts2.config.impl.DefaultConfiguration;
import org.apache.struts2.conversion.NullHandler;
import org.apache.struts2.conversion.impl.ArrayConverter;
Expand Down Expand Up @@ -130,6 +131,8 @@ public void register(ContainerBuilder builder, LocatableProperties props) throws

.factory(UnknownHandlerManager.class, DefaultUnknownHandlerManager.class, Scope.SINGLETON)

.factory(ScopedConstantProvider.class, StrutsScopedConstantProvider.class, Scope.SINGLETON)

// silly workarounds for ognl since there is no way to flush its caches
.factory(PropertyAccessor.class, List.class.getName(), XWorkListPropertyAccessor.class, Scope.SINGLETON)
.factory(PropertyAccessor.class, ArrayList.class.getName(), XWorkListPropertyAccessor.class, Scope.SINGLETON)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.config.providers;

import org.apache.struts2.ActionContext;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.ActionProxy;
import org.apache.struts2.config.Configuration;
import org.apache.struts2.config.ScopableConstants;
import org.apache.struts2.config.ScopedConstantProvider;
import org.apache.struts2.config.entities.PackageConfig;
import org.apache.struts2.inject.Container;
import org.apache.struts2.inject.Inject;

import java.util.Optional;
import java.util.Set;

/**
* @since 7.5.0
*/
public class StrutsScopedConstantProvider implements ScopedConstantProvider {

private Container container;
private Configuration configuration;
private Set<String> scopableNames;

@Inject
public void setContainer(Container container) {
this.container = container;
this.configuration = container.getInstance(Configuration.class);
this.scopableNames = ScopableConstants.collectNames(container);
}
Comment thread
lukaszlenart marked this conversation as resolved.

@Override
public String getValue(String name) {
if (!scopableNames.contains(name)) {
throw new IllegalArgumentException(String.format(
"Constant [%s] is not scopable; register it through a %s bean", name, ScopableConstants.class.getName()));
}
return currentPackage()
.map(packageConfig -> packageConfig.getAllScopedConstants().get(name))
.orElseGet(() -> container.getInstance(String.class, name));
}

private Optional<PackageConfig> currentPackage() {
ActionContext context = ActionContext.getContext();
ActionInvocation invocation = context != null ? context.getActionInvocation() : null;
ActionProxy proxy = invocation != null ? invocation.getProxy() : null;
if (proxy == null || proxy.getConfig() == null) {
return Optional.empty();
}
return Optional.ofNullable(configuration.getPackageConfig(proxy.getConfig().getPackageName()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,8 @@ protected PackageConfig addPackage(Element packageElement) throws ConfigurationE

LOG.debug("Loaded {}", newPackage);

loadScopedConstants(newPackage, packageElement);

// add result types (and default result) to this package
addResultTypes(newPackage, packageElement);

Expand Down Expand Up @@ -839,6 +841,21 @@ protected void loadDefaultActionRef(PackageConfig.Builder packageContext, Elemen
}
}

/**
* @since 7.5.0
*/
protected void loadScopedConstants(PackageConfig.Builder packageContext, Element packageElement) {
Set<String> declared = new HashSet<>();
iterateChildrenByTagName(packageElement, "scoped-constant", constantElement -> {
String name = constantElement.getAttribute("name");
if (!declared.add(name)) {
throw new ConfigurationException(format("Package [%s] declares scoped constant [%s] more than once",
packageContext.getName(), name), constantElement);
}
packageContext.addScopedConstant(name, constantElement.getAttribute("value"));
});
}

/**
* Load all the global results for this package from the XML element.
*
Expand Down
Loading
Loading