This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/struts.git
commit 5f78337213285dd7f708d46ea094ff1448068a97 Author: Lukasz Lenart <[email protected]> AuthorDate: Mon Nov 2 07:40:00 2026 +0100 WW-4164 feat(core): add ActionExtensions list parsing Co-Authored-By: Claude Sonnet 5.5 <[email protected]> --- .../dispatcher/mapper/ActionExtensions.java | 78 ++++++++++++++++++++++ .../dispatcher/mapper/DefaultActionMapper.java | 10 +-- .../dispatcher/mapper/ActionExtensionsTest.java | 57 ++++++++++++++++ 3 files changed, 136 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/org/apache/struts2/dispatcher/mapper/ActionExtensions.java b/core/src/main/java/org/apache/struts2/dispatcher/mapper/ActionExtensions.java new file mode 100644 index 000000000..ec90209fa --- /dev/null +++ b/core/src/main/java/org/apache/struts2/dispatcher/mapper/ActionExtensions.java @@ -0,0 +1,78 @@ +/* + * 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.dispatcher.mapper; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Helpers for the ordered extension lists accepted by {@code struts.action.extension} and the + * per-package / per-action {@code extension} attribute. The first entry is the default and an empty + * entry stands for "no extension". + * + * @since 7.5.0 + */ +public final class ActionExtensions { + + private ActionExtensions() { + } + + /** + * Parses a comma separated extension list; a trailing comma adds an empty entry. + * + * @param value the raw value, may be {@code null} + * @return an unmodifiable list in declared order, or {@code null} when {@code value} is {@code null} + * @since 7.5.0 + */ + public static List<String> parse(String value) { + if (value == null) { + return null; + } + if (value.isEmpty()) { + return List.of(""); + } + List<String> list = new ArrayList<>(List.of(value.split(","))); + if (value.endsWith(",")) { + list.add(""); + } + return Collections.unmodifiableList(list); + } + + /** + * @param allowed the allowed extensions + * @param extension the extension to check, {@code null} counts as an empty one + * @return whether {@code extension} is in {@code allowed} + * @since 7.5.0 + */ + public static boolean allows(List<String> allowed, String extension) { + return allowed.contains(extension == null ? "" : extension); + } + + /** + * @param allowed the allowed extensions, must not be empty + * @param current the preferred extension, {@code null} counts as an empty one + * @return {@code current} when allowed, otherwise the first allowed extension + * @since 7.5.0 + */ + public static String select(List<String> allowed, String current) { + String candidate = current == null ? "" : current; + return allows(allowed, candidate) ? candidate : allowed.get(0); + } +} diff --git a/core/src/main/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java b/core/src/main/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java index 60d31be68..ffdda8bea 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java @@ -34,8 +34,6 @@ import org.apache.struts2.RequestUtils; import org.apache.struts2.StrutsConstants; import org.apache.struts2.util.PrefixTrie; -import java.util.ArrayList; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -247,13 +245,7 @@ public class DefaultActionMapper implements ActionMapper { @Inject(StrutsConstants.STRUTS_ACTION_EXTENSION) public void setExtensions(String extensions) { if (StringUtils.isNotEmpty(extensions)) { - List<String> list = new ArrayList<>(); - String[] tokens = extensions.split(","); - Collections.addAll(list, tokens); - if (extensions.endsWith(",")) { - list.add(""); - } - this.extensions = Collections.unmodifiableList(list); + this.extensions = ActionExtensions.parse(extensions); } else { this.extensions = null; } diff --git a/core/src/test/java/org/apache/struts2/dispatcher/mapper/ActionExtensionsTest.java b/core/src/test/java/org/apache/struts2/dispatcher/mapper/ActionExtensionsTest.java new file mode 100644 index 000000000..3a1f46fe1 --- /dev/null +++ b/core/src/test/java/org/apache/struts2/dispatcher/mapper/ActionExtensionsTest.java @@ -0,0 +1,57 @@ +/* + * 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.dispatcher.mapper; + +import junit.framework.TestCase; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ActionExtensionsTest extends TestCase { + + public void testParseKeepsOrder() { + assertThat(ActionExtensions.parse("html,action")).isEqualTo(List.of("html", "action")); + } + + public void testParseTrailingCommaAddsEmpty() { + assertThat(ActionExtensions.parse("action,,")).isEqualTo(List.of("action", "")); + } + + public void testParseEmptyMeansNoExtensionOnly() { + assertThat(ActionExtensions.parse("")).isEqualTo(List.of("")); + } + + public void testParseNull() { + assertThat(ActionExtensions.parse(null)).isNull(); + } + + public void testAllowsNullAsEmpty() { + assertThat(ActionExtensions.allows(List.of("html", ""), null)).isTrue(); + assertThat(ActionExtensions.allows(List.of("html"), null)).isFalse(); + } + + public void testSelectKeepsAllowedCurrent() { + assertThat(ActionExtensions.select(List.of("action", "html"), "html")).isEqualTo("html"); + } + + public void testSelectFallsBackToFirst() { + assertThat(ActionExtensions.select(List.of("action", ""), "html")).isEqualTo("action"); + } +}
