You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@struts.apache.org by lu...@apache.org on 2026/07/26 06:26:21 UTC
(struts) 03/08: WW-3784 feat(convention): add action-name specificity comparator
This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch WW-3784-annotated-wildcard-specificity-ordering
in repository https://gitbox.apache.org/repos/asf/struts.git
commit 429d49509b70811c21214fbc6ee5af641ba9af73
Author: Lukasz Lenart <lu...@apache.org>
AuthorDate: Sun Jul 26 05:42:00 2026 +0200
WW-3784 feat(convention): add action-name specificity comparator
Co-Authored-By: Claude Opus 4.8 <no...@anthropic.com>
---
.../ActionNameSpecificityComparator.java | 98 ++++++++++++++++++++++
.../ActionNameSpecificityComparatorTest.java | 53 ++++++++++++
2 files changed, 151 insertions(+)
diff --git a/plugins/convention/src/main/java/org/apache/struts2/convention/ActionNameSpecificityComparator.java b/plugins/convention/src/main/java/org/apache/struts2/convention/ActionNameSpecificityComparator.java
new file mode 100644
index 000000000..a0455dced
--- /dev/null
+++ b/plugins/convention/src/main/java/org/apache/struts2/convention/ActionNameSpecificityComparator.java
@@ -0,0 +1,98 @@
+/*
+ * 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.convention;
+
+import java.util.Comparator;
+
+/**
+ * Orders wildcard action-name patterns most-specific-first so that, under the framework's
+ * first-match-wins matching, a specific pattern (e.g. {@code some/usefull/*}) is evaluated
+ * before a general one (e.g. {@code some/*}).
+ *
+ * <p>Ordering keys, applied in order:</p>
+ * <ol>
+ * <li>fewer wildcard tokens first (a {@code *}/{@code **} run, or a <code>{var}</code> group);</li>
+ * <li>more literal characters first;</li>
+ * <li>fewer path-spanning {@code **} tokens first;</li>
+ * <li>natural (alphabetical) order of the pattern, for deterministic tie-breaking.</li>
+ * </ol>
+ *
+ * <p>Matcher-agnostic: it recognises both {@code *}/{@code **} (WildcardHelper) and
+ * <code>{var}</code> (NamedVariablePatternMatcher) wildcards.</p>
+ *
+ * @since 7.3.0 (WW-3784)
+ */
+public class ActionNameSpecificityComparator implements Comparator<String> {
+
+ @Override
+ public int compare(String a, String b) {
+ Counts ca = count(a);
+ Counts cb = count(b);
+
+ int byWildcards = Integer.compare(ca.wildcards, cb.wildcards);
+ if (byWildcards != 0) {
+ return byWildcards;
+ }
+ int byLiterals = Integer.compare(cb.literals, ca.literals); // more literals first
+ if (byLiterals != 0) {
+ return byLiterals;
+ }
+ int byPathWildcards = Integer.compare(ca.pathWildcards, cb.pathWildcards);
+ if (byPathWildcards != 0) {
+ return byPathWildcards;
+ }
+ return a.compareTo(b);
+ }
+
+ private Counts count(String pattern) {
+ int wildcards = 0;
+ int pathWildcards = 0;
+ int literals = 0;
+ int i = 0;
+ int len = pattern.length();
+ while (i < len) {
+ char c = pattern.charAt(i);
+ if (c == '*') {
+ int start = i;
+ while (i < len && pattern.charAt(i) == '*') {
+ i++;
+ }
+ wildcards++;
+ if (i - start >= 2) {
+ pathWildcards++;
+ }
+ } else if (c == '{') {
+ int close = pattern.indexOf('}', i);
+ if (close < 0) {
+ literals += len - i; // malformed: treat the remainder as literal
+ break;
+ }
+ wildcards++;
+ i = close + 1;
+ } else {
+ literals++;
+ i++;
+ }
+ }
+ return new Counts(wildcards, pathWildcards, literals);
+ }
+
+ private record Counts(int wildcards, int pathWildcards, int literals) {
+ }
+}
diff --git a/plugins/convention/src/test/java/org/apache/struts2/convention/ActionNameSpecificityComparatorTest.java b/plugins/convention/src/test/java/org/apache/struts2/convention/ActionNameSpecificityComparatorTest.java
new file mode 100644
index 000000000..5722a4ae3
--- /dev/null
+++ b/plugins/convention/src/test/java/org/apache/struts2/convention/ActionNameSpecificityComparatorTest.java
@@ -0,0 +1,53 @@
+package org.apache.struts2.convention;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class ActionNameSpecificityComparatorTest {
+
+ private final ActionNameSpecificityComparator comparator = new ActionNameSpecificityComparator();
+
+ @Test
+ public void moreLiteralPrefixIsMoreSpecific_ticketCase() {
+ // equal wildcard count (1 each); "some/usefull/*" has more literal chars -> more specific
+ assertTrue(comparator.compare("some/usefull/*", "some/*") < 0);
+ }
+
+ @Test
+ public void fewerWildcardsIsMoreSpecific() {
+ assertTrue(comparator.compare("a/*", "a/*/*") < 0);
+ }
+
+ @Test
+ public void singleStarBeatsPathStarAtEqualLiterals() {
+ // both "a/" literal (2 chars), one wildcard each; "a/*" (file) beats "a/**" (path)
+ assertTrue(comparator.compare("a/*", "a/**") < 0);
+ }
+
+ @Test
+ public void namedVariablesCountAsWildcards() {
+ assertTrue(comparator.compare("some/usefull/{id}", "some/{id}") < 0);
+ }
+
+ @Test
+ public void literalRanksBeforeAnyWildcard() {
+ assertTrue(comparator.compare("some/list", "some/*") < 0);
+ }
+
+ @Test
+ public void sortIsDeterministicRegardlessOfInputOrder() {
+ List<String> expected = Arrays.asList("some/usefull/*", "some/*", "*");
+ List<String> shuffled = new ArrayList<>(expected);
+ Collections.shuffle(shuffled, new Random(42));
+ shuffled.sort(comparator);
+ assertEquals(expected, shuffled);
+ }
+}