This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch WW-3476-scoped-constants in repository https://gitbox.apache.org/repos/asf/struts.git
commit 2050da78c94b0cc7d96cb4de5909cf1af3b3cb0e Author: Lukasz Lenart <[email protected]> AuthorDate: Sat Oct 10 17:13:04 2026 +0200 WW-3476 feat(config): reject unregistered scoped constants at startup Co-Authored-By: Claude Opus 5.5 <[email protected]> --- .../apache/struts2/config/ScopableConstants.java | 44 +++++++++++ .../struts2/config/impl/DefaultConfiguration.java | 25 ++++++ .../struts2/config/SampleScopableConstants.java | 29 +++++++ .../config/ScopedConstantsValidationTest.java | 89 ++++++++++++++++++++++ .../struts2/config/SecondScopableConstants.java | 29 +++++++ .../scoped-constants-abstract-unregistered.xml | 32 ++++++++ .../config/scoped-constants-no-registry.xml | 29 +++++++ .../struts2/config/scoped-constants-registered.xml | 32 ++++++++ .../struts2/config/scoped-constants-two-beans.xml | 35 +++++++++ .../config/scoped-constants-unregistered.xml | 32 ++++++++ 10 files changed, 376 insertions(+) diff --git a/core/src/main/java/org/apache/struts2/config/ScopableConstants.java b/core/src/main/java/org/apache/struts2/config/ScopableConstants.java new file mode 100644 index 000000000..fd21ac513 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/config/ScopableConstants.java @@ -0,0 +1,44 @@ +/* + * 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)) { + names.addAll(container.getInstance(ScopableConstants.class, beanName).getNames()); + } + return Collections.unmodifiableSet(names); + } +} diff --git a/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java b/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java index 53bf20dc8..4f908bfb6 100644 --- a/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java +++ b/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java @@ -36,6 +36,7 @@ import org.apache.struts2.config.FileManagerFactoryProvider; 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; @@ -449,6 +450,8 @@ public class DefaultConfiguration implements Configuration { * @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<>(); @@ -488,6 +491,28 @@ public class DefaultConfiguration implements Configuration { 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(); diff --git a/core/src/test/java/org/apache/struts2/config/SampleScopableConstants.java b/core/src/test/java/org/apache/struts2/config/SampleScopableConstants.java new file mode 100644 index 000000000..c09d58755 --- /dev/null +++ b/core/src/test/java/org/apache/struts2/config/SampleScopableConstants.java @@ -0,0 +1,29 @@ +/* + * 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 java.util.Set; + +public class SampleScopableConstants implements ScopableConstants { + + @Override + public Set<String> getNames() { + return Set.of("sample.scopable", "sample.unset"); + } +} diff --git a/core/src/test/java/org/apache/struts2/config/ScopedConstantsValidationTest.java b/core/src/test/java/org/apache/struts2/config/ScopedConstantsValidationTest.java new file mode 100644 index 000000000..5cbb1a341 --- /dev/null +++ b/core/src/test/java/org/apache/struts2/config/ScopedConstantsValidationTest.java @@ -0,0 +1,89 @@ +/* + * 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.ActionContext; +import org.apache.struts2.config.providers.StrutsDefaultConfigurationProvider; +import org.apache.struts2.inject.Container; +import org.junit.After; +import org.junit.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ScopedConstantsValidationTest { + + private ConfigurationManager configurationManager; + + @After + public void tearDown() { + if (configurationManager != null) { + configurationManager.destroyConfiguration(); + } + ActionContext.clear(); + } + + @Test + public void registeredNameLoads() { + Configuration configuration = load("scoped-constants-registered.xml"); + + assertThat(configuration.getPackageConfig("registered").getScopedConstants()) + .isEqualTo(Map.of("sample.scopable", "x")); + } + + @Test + public void unregisteredNameFails() { + assertThatThrownBy(() -> load("scoped-constants-unregistered.xml")) + .isInstanceOf(ConfigurationException.class) + .hasStackTraceContaining("Package [unregistered] declares scoped constant [sample.unknown], which is not scopable. " + + "Scopable constants: [sample.scopable, sample.unset]."); + } + + @Test + public void abstractPackageIsValidatedToo() { + assertThatThrownBy(() -> load("scoped-constants-abstract-unregistered.xml")) + .isInstanceOf(ConfigurationException.class) + .hasStackTraceContaining("Package [abstractBase] declares scoped constant [sample.unknown], which is not scopable."); + } + + @Test + public void anyScopedConstantFailsWhenNothingIsScopable() { + assertThatThrownBy(() -> load("scoped-constants-no-registry.xml")) + .isInstanceOf(ConfigurationException.class) + .hasStackTraceContaining("Package [unregistered] declares scoped constant [sample.scopable], which is not scopable. " + + "No constants are scopable in this configuration."); + } + + @Test + public void namesFromAllBeansAreScopable() { + Configuration configuration = load("scoped-constants-two-beans.xml"); + + assertThat(configuration.getPackageConfig("both").getScopedConstants()) + .isEqualTo(Map.of("sample.scopable", "x", "second.scopable", "y")); + } + + private Configuration load(String fixture) { + configurationManager = new ConfigurationManager(Container.DEFAULT_NAME); + configurationManager.addContainerProvider(new StrutsDefaultConfigurationProvider()); + configurationManager.addContainerProvider(new StrutsXmlConfigurationProvider("org/apache/struts2/config/" + fixture)); + return configurationManager.getConfiguration(); + } +} diff --git a/core/src/test/java/org/apache/struts2/config/SecondScopableConstants.java b/core/src/test/java/org/apache/struts2/config/SecondScopableConstants.java new file mode 100644 index 000000000..9534dab7f --- /dev/null +++ b/core/src/test/java/org/apache/struts2/config/SecondScopableConstants.java @@ -0,0 +1,29 @@ +/* + * 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 java.util.Set; + +public class SecondScopableConstants implements ScopableConstants { + + @Override + public Set<String> getNames() { + return Set.of("second.scopable"); + } +} diff --git a/core/src/test/resources/org/apache/struts2/config/scoped-constants-abstract-unregistered.xml b/core/src/test/resources/org/apache/struts2/config/scoped-constants-abstract-unregistered.xml new file mode 100644 index 000000000..6eb2c623c --- /dev/null +++ b/core/src/test/resources/org/apache/struts2/config/scoped-constants-abstract-unregistered.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/* + * 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. + */ +--> +<!DOCTYPE struts PUBLIC + "-//Apache Software Foundation//DTD Struts Configuration 7.5//EN" + "struts-7.5.dtd"> +<struts> + <bean type="org.apache.struts2.config.ScopableConstants" name="sample" + class="org.apache.struts2.config.SampleScopableConstants"/> + + <package name="abstractBase" abstract="true"> + <scoped-constant name="sample.unknown" value="x"/> + </package> +</struts> diff --git a/core/src/test/resources/org/apache/struts2/config/scoped-constants-no-registry.xml b/core/src/test/resources/org/apache/struts2/config/scoped-constants-no-registry.xml new file mode 100644 index 000000000..6d622d8ac --- /dev/null +++ b/core/src/test/resources/org/apache/struts2/config/scoped-constants-no-registry.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/* + * 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. + */ +--> +<!DOCTYPE struts PUBLIC + "-//Apache Software Foundation//DTD Struts Configuration 7.5//EN" + "struts-7.5.dtd"> +<struts> + <package name="unregistered" namespace="/unregistered"> + <scoped-constant name="sample.scopable" value="x"/> + </package> +</struts> diff --git a/core/src/test/resources/org/apache/struts2/config/scoped-constants-registered.xml b/core/src/test/resources/org/apache/struts2/config/scoped-constants-registered.xml new file mode 100644 index 000000000..f3e5a5401 --- /dev/null +++ b/core/src/test/resources/org/apache/struts2/config/scoped-constants-registered.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/* + * 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. + */ +--> +<!DOCTYPE struts PUBLIC + "-//Apache Software Foundation//DTD Struts Configuration 7.5//EN" + "struts-7.5.dtd"> +<struts> + <bean type="org.apache.struts2.config.ScopableConstants" name="sample" + class="org.apache.struts2.config.SampleScopableConstants"/> + + <package name="registered" namespace="/registered"> + <scoped-constant name="sample.scopable" value="x"/> + </package> +</struts> diff --git a/core/src/test/resources/org/apache/struts2/config/scoped-constants-two-beans.xml b/core/src/test/resources/org/apache/struts2/config/scoped-constants-two-beans.xml new file mode 100644 index 000000000..d79022cf6 --- /dev/null +++ b/core/src/test/resources/org/apache/struts2/config/scoped-constants-two-beans.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/* + * 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. + */ +--> +<!DOCTYPE struts PUBLIC + "-//Apache Software Foundation//DTD Struts Configuration 7.5//EN" + "struts-7.5.dtd"> +<struts> + <bean type="org.apache.struts2.config.ScopableConstants" name="sample" + class="org.apache.struts2.config.SampleScopableConstants"/> + <bean type="org.apache.struts2.config.ScopableConstants" name="second" + class="org.apache.struts2.config.SecondScopableConstants"/> + + <package name="both" namespace="/both"> + <scoped-constant name="sample.scopable" value="x"/> + <scoped-constant name="second.scopable" value="y"/> + </package> +</struts> diff --git a/core/src/test/resources/org/apache/struts2/config/scoped-constants-unregistered.xml b/core/src/test/resources/org/apache/struts2/config/scoped-constants-unregistered.xml new file mode 100644 index 000000000..0fd512212 --- /dev/null +++ b/core/src/test/resources/org/apache/struts2/config/scoped-constants-unregistered.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/* + * 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. + */ +--> +<!DOCTYPE struts PUBLIC + "-//Apache Software Foundation//DTD Struts Configuration 7.5//EN" + "struts-7.5.dtd"> +<struts> + <bean type="org.apache.struts2.config.ScopableConstants" name="sample" + class="org.apache.struts2.config.SampleScopableConstants"/> + + <package name="unregistered" namespace="/unregistered"> + <scoped-constant name="sample.unknown" value="x"/> + </package> +</struts>
