PathPattern.java
package sprout.mvc.mapping;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PathPattern implements Comparable<PathPattern> {
private final String originalPattern;
private final Pattern regex;
private final List<String> varNames;
private final List<Integer> varGroups;
private final int staticLen;
private final int singleStarCount;
private final int doubleStarCount;
private static final Pattern VAR_TOKEN = Pattern.compile("\\{([^/:}]+)(?::([^}]+))?}");
public PathPattern(String pattern) {
this.originalPattern = Objects.requireNonNull(pattern, "Pattern must not be null");
var re = new StringBuilder("^");
var names = new ArrayList<String>();
var groups = new ArrayList<Integer>();
// Local counters for parsing
int staticCharCount = 0;
int singleStars = 0;
int doubleStars = 0;
int groupIndex = 0;
final Matcher varMatcher = VAR_TOKEN.matcher(pattern);
int i = 0;
while (i < pattern.length()) {
char ch = pattern.charAt(i);
switch (ch) {
case '*':
if (i + 1 < pattern.length() && pattern.charAt(i + 1) == '*') {
re.append("(.+?)"); // Non-greedy match for '**'
doubleStars++;
i += 2;
} else {
re.append("([^/]+)"); // Match for '*'
singleStars++;
i++;
}
groupIndex++;
break;
case '?':
re.append("[^/]"); // Match any character except '/'
i++;
break;
case '{':
if (!varMatcher.region(i, pattern.length()).lookingAt()) {
throw new IllegalArgumentException("Invalid variable syntax at index " + i + " in pattern: " + pattern);
}
String varName = varMatcher.group(1);
String customRegex = varMatcher.group(2);
String expression = (customRegex != null) ? customRegex : "[^/]+";
re.append("(").append(expression).append(")");
names.add(varName);
groupIndex++;
groups.add(groupIndex);
i = varMatcher.end();
break;
default:
// Any other character is treated as a static part of the path.
re.append(Pattern.quote(String.valueOf(ch)));
staticCharCount++;
i++;
break;
}
}
re.append("$");
// Assign to final fields to ensure immutability
this.regex = Pattern.compile(re.toString());
this.varNames = Collections.unmodifiableList(names);
this.varGroups = Collections.unmodifiableList(groups);
this.staticLen = staticCharCount;
this.singleStarCount = singleStars;
this.doubleStarCount = doubleStars;
}
public boolean matches(String path) {
return this.regex.matcher(path).matches();
}
public Map<String, String> extractPathVariables(String path) {
Matcher m = this.regex.matcher(path);
if (!m.matches()) {
return Map.of();
}
Map<String, String> vars = new HashMap<>();
for (int idx = 0; idx < this.varNames.size(); idx++) {
vars.put(this.varNames.get(idx), m.group(this.varGroups.get(idx)));
}
return vars;
}
@Override
public int compareTo(PathPattern other) {
int c = Integer.compare(this.doubleStarCount, other.doubleStarCount);
if (c != 0) return c;
c = Integer.compare(this.singleStarCount, other.singleStarCount);
if (c != 0) return c;
c = Integer.compare(this.varNames.size(), other.varNames.size());
if (c != 0) return c;
c = Integer.compare(other.staticLen, this.staticLen); // Longer static part is more specific
if (c != 0) return c;
// Final tie-breaker for stable sorting
return this.originalPattern.compareTo(other.originalPattern);
}
public int getVariableCount() {
return this.varNames.size();
}
public String getOriginalPattern() {
return this.originalPattern;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PathPattern that = (PathPattern) o;
return this.originalPattern.equals(that.originalPattern);
}
@Override
public int hashCode() {
return Objects.hash(this.originalPattern);
}
@Override
public String toString() {
return this.originalPattern;
}
}