mirror of
https://github.com/apache/struts.git
synced 2026-08-05 14:47:09 +00:00
WW-3784 Order annotated wildcard actions most-specific-first (#1813)
* WW-3784 docs: design for specificity-ordered wildcard matching in annotated actions Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * WW-3784 docs: implementation plan for annotated wildcard specificity ordering Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * WW-3784 feat(convention): add action-name specificity comparator Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * WW-3784 fix(convention): add Apache License header to test file Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * WW-3784 feat(core): add PackageConfig.Builder.reorderActionConfigs Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * WW-3784 docs: add javadoc for PackageConfig.Builder.reorderActionConfigs Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * WW-3784 feat(convention): order annotated wildcard actions most-specific-first Sorts each convention-built package's action configs by pattern specificity so a specific pattern (some/usefull/*) is matched before a general one (some/*), regardless of class-scan order. Also makes convention action ordering deterministic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * WW-3784 docs: correct wildcard cross-segment claims and note comparator limitations The spec incorrectly stated that WildcardHelper's single `*` is greedy and crosses `/`, and that `some/*` shadows `some/usefull/*`. Verified against WildcardHelper.java and NamedVariablePatternMatcher.java: only `**` crosses `/`, so those two patterns are actually disjoint (different segment counts) and never compete for the same request. Correct the Problem narrative, ticket example, and matcher bullets to state this accurately, and document two known limitations of the specificity comparator (raw wildcard-token-count key can misrank `**` ahead of narrower multi-token patterns; parent-package actions bypass sorting). Also add a test asserting the natural-order alphabetical tiebreak key. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * WW-3784 test(convention): prove specificity ordering fixes wildcard shadowing end-to-end Adds an end-to-end routing test driving the production reorder (PackageConfig.Builder.reorderActionConfigs + ActionNameSpecificityComparator) through the real ActionConfigMatcher/WildcardHelper. some/** and some/usefull/* genuinely overlap for some/usefull/sleeping (** crosses '/'), so the test asserts the general pattern shadows the specific one when registered first, and that specificity ordering makes the specific action reachable again. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import org.apache.struts2.util.location.Location;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -517,6 +518,26 @@ public class PackageConfig extends Located implements Comparable<PackageConfig>,
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-inserts this package's action configs into a new insertion-ordered map,
|
||||
* ordered by the supplied comparator over the action-name keys. Must be called
|
||||
* before {@link #build()}.
|
||||
*
|
||||
* @param byActionName comparator over action names determining match precedence
|
||||
* @return this builder
|
||||
* @since 7.3.0 (WW-3784)
|
||||
*/
|
||||
public Builder reorderActionConfigs(Comparator<String> byActionName) {
|
||||
List<Map.Entry<String, ActionConfig>> entries = new ArrayList<>(target.actionConfigs.entrySet());
|
||||
entries.sort(Map.Entry.comparingByKey(byActionName));
|
||||
Map<String, ActionConfig> reordered = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, ActionConfig> entry : entries) {
|
||||
reordered.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
target.actionConfigs = reordered;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addParents(List<PackageConfig> parents) {
|
||||
for (PackageConfig config : parents) {
|
||||
addParent(config);
|
||||
|
||||
+44
@@ -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.entities;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class PackageConfigBuilderReorderTest extends TestCase {
|
||||
|
||||
public void testReorderActionConfigsAppliesComparator() {
|
||||
PackageConfig.Builder builder = new PackageConfig.Builder("test");
|
||||
builder.addActionConfig("some/*", action("some/*"));
|
||||
builder.addActionConfig("some/usefull/*", action("some/usefull/*"));
|
||||
|
||||
// reverse-alphabetical proves the map is genuinely reordered, not left as-inserted
|
||||
builder.reorderActionConfigs(Comparator.reverseOrder());
|
||||
|
||||
List<String> keys = new ArrayList<>(builder.build().getActionConfigs().keySet());
|
||||
assertEquals(List.of("some/usefull/*", "some/*"), keys);
|
||||
}
|
||||
|
||||
private ActionConfig action(String name) {
|
||||
return new ActionConfig.Builder("test", name, "com.example.Action").build();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user