Skip to content

Commit 1b4f941

Browse files
authored
RegexPathSpec documentation and MatchedPath improvements (#8163)
* More documentation Signed-off-by: Joakim Erdfelt <[email protected]>
1 parent 1f902f6 commit 1b4f941

File tree

3 files changed

+249
-101
lines changed

3 files changed

+249
-101
lines changed

jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/RegexPathSpec.java

+178-84
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,77 @@
2121
import org.slf4j.Logger;
2222
import org.slf4j.LoggerFactory;
2323

24+
/**
25+
* <p>
26+
* RegexPathSpec is a PathSpec implementation for a {@link PathMappings} instance.
27+
* </p>
28+
*
29+
* <p>
30+
* Supports the standard Java regex found in {@link java.util.regex.Pattern}.
31+
* </p>
32+
*
33+
* <p>
34+
* Supports {@link PathSpecGroup} for {@link PathSpecGroup#EXACT}, {@link PathSpecGroup#PREFIX_GLOB}, {@link PathSpecGroup#MIDDLE_GLOB}, and {@link PathSpecGroup#SUFFIX_GLOB}.
35+
* This is done by evaluating the signature or the provided regex pattern for what is a literal vs a glob (of any kind).
36+
* </p>
37+
*
38+
* <ul>
39+
* <li>Only literals, it's a {@link PathSpecGroup#EXACT}.</li>
40+
* <li>Starts with literals, ends with globs, it's a {@link PathSpecGroup#PREFIX_GLOB}</li>
41+
* <li>Starts with glob, has at least 1 literal, then any thing else, it's a {@link PathSpecGroup#SUFFIX_GLOB}</li>
42+
* <li>All other signatures are a {@link PathSpecGroup#MIDDLE_GLOB}</li>
43+
* </ul>
44+
*
45+
* <p>
46+
* The use of regex capture groups, regex character classes, regex quantifiers, and regex special contructs
47+
* will be identified as a glob (for signature determination), all other characters are identified
48+
* as literal. The regex {@code ^} beginning of line, and regex {@code $} end of line are ignored.
49+
* </p>
50+
*
51+
* <p>
52+
* <b>Support for {@link MatchedPath} and PathMatch vs PathInfo</b>
53+
* </p>
54+
*
55+
* <p>
56+
* There's a few steps in evaluating the matched input path for determining where the split
57+
* in the input path should occur for {@link MatchedPath#getPathMatch()} and {@link MatchedPath#getPathInfo()}.
58+
* </p>
59+
*
60+
* <ol>
61+
* <li>
62+
* If there are no regex capturing groups,
63+
* the entire path is returned in {@link MatchedPath#getPathMatch()},
64+
* and a null returned for {@link MatchedPath#getPathInfo()}
65+
* </li>
66+
* <li>
67+
* If both the named regex capturing groups {@code name} and {@code info} are present, then
68+
* the {@code name} group is returned in {@link MatchedPath#getPathMatch()} and the
69+
* {@code info} group is returned in {@link MatchedPath#getPathInfo()}
70+
* </li>
71+
* <li>
72+
* If there is only 1 regex capturing group
73+
* <ul>
74+
* <li>
75+
* If the named regex capturing group {@code name} is present, the
76+
* input path up to the end of the capturing group is returned
77+
* in {@link MatchedPath#getPathMatch()} and any following characters (or null)
78+
* are returned in {@link MatchedPath#getPathInfo()}
79+
* </li>
80+
* <li>
81+
* other wise the input path up to the start of the capturing group is returned
82+
* in {@link MatchedPath#getPathMatch()} and any following characters (or null)
83+
* are returned in {@link MatchedPath#getPathInfo()}
84+
* </li>
85+
* </ul>
86+
* If the split on pathMatch ends with {@code /} AND the pathInfo doesn't start with {@code /}
87+
* then the slash is moved from pathMatch to pathInfo.
88+
* </li>
89+
* <li>
90+
* All other RegexPathSpec signatures will return the entire path
91+
* in {@link MatchedPath#getPathMatch()}, and a null returned for {@link MatchedPath#getPathInfo()}
92+
* </li>
93+
* </ol>
94+
*/
2495
public class RegexPathSpec extends AbstractPathSpec
2596
{
2697
private static final Logger LOG = LoggerFactory.getLogger(UriTemplatePathSpec.class);
@@ -54,8 +125,9 @@ public RegexPathSpec(String regex)
54125
declaration = regex;
55126
int specLength = declaration.length();
56127
// build up a simple signature we can use to identify the grouping
57-
boolean inTextList = false;
128+
boolean inCharacterClass = false;
58129
boolean inQuantifier = false;
130+
boolean inCaptureGroup = false;
59131
StringBuilder signature = new StringBuilder();
60132

61133
int pathDepth = 0;
@@ -68,8 +140,6 @@ public RegexPathSpec(String regex)
68140
case '^': // ignore anchors
69141
case '$': // ignore anchors
70142
case '\'': // ignore escaping
71-
case '(': // ignore grouping
72-
case ')': // ignore grouping
73143
break;
74144
case '+': // single char quantifier
75145
case '?': // single char quantifier
@@ -78,25 +148,32 @@ public RegexPathSpec(String regex)
78148
case '.': // any char token
79149
signature.append('g'); // glob
80150
break;
81-
case '{':
151+
case '(': // in regex capture group
152+
inCaptureGroup = true;
153+
break;
154+
case ')':
155+
inCaptureGroup = false;
156+
signature.append('g');
157+
break;
158+
case '{': // in regex quantifier
82159
inQuantifier = true;
83160
break;
84161
case '}':
85162
inQuantifier = false;
86163
break;
87-
case '[':
88-
inTextList = true;
164+
case '[': // in regex character class
165+
inCharacterClass = true;
89166
break;
90167
case ']':
91-
inTextList = false;
168+
inCharacterClass = false;
92169
signature.append('g'); // glob
93170
break;
94171
case '/':
95-
if (!inTextList && !inQuantifier)
172+
if (!inCharacterClass && !inQuantifier && !inCaptureGroup)
96173
pathDepth++;
97174
break;
98175
default:
99-
if (!inTextList && !inQuantifier && Character.isLetterOrDigit(c))
176+
if (!inCharacterClass && !inQuantifier && !inCaptureGroup && Character.isLetterOrDigit(c))
100177
{
101178
if (last == '\\') // escaped
102179
{
@@ -135,9 +212,9 @@ public RegexPathSpec(String regex)
135212
String sig = signature.toString();
136213

137214
PathSpecGroup group;
138-
if (Pattern.matches("^l*$", sig))
215+
if (Pattern.matches("^l+$", sig))
139216
group = PathSpecGroup.EXACT;
140-
else if (Pattern.matches("^l*g+", sig))
217+
else if (Pattern.matches("^l+g+", sig))
141218
group = PathSpecGroup.PREFIX_GLOB;
142219
else if (Pattern.matches("^g+l+.*", sig))
143220
group = PathSpecGroup.SUFFIX_GLOB;
@@ -193,44 +270,19 @@ public int getPathDepth()
193270
@Override
194271
public String getPathInfo(String path)
195272
{
196-
// Path Info only valid for PREFIX_GLOB types
197-
if (_group == PathSpecGroup.PREFIX_GLOB)
198-
{
199-
Matcher matcher = getMatcher(path);
200-
if (matcher.matches())
201-
{
202-
if (matcher.groupCount() >= 1)
203-
{
204-
String pathInfo = matcher.group(1);
205-
if ("".equals(pathInfo))
206-
return "/";
207-
else
208-
return pathInfo;
209-
}
210-
}
211-
}
212-
return null;
273+
MatchedPath matched = matched(path);
274+
if (matched == null)
275+
return null;
276+
return matched.getPathInfo();
213277
}
214278

215279
@Override
216280
public String getPathMatch(String path)
217281
{
218-
Matcher matcher = getMatcher(path);
219-
if (matcher.matches())
220-
{
221-
if (_group == PathSpecGroup.PREFIX_GLOB && matcher.groupCount() >= 1)
222-
{
223-
int idx = matcher.start(1);
224-
if (idx > 0)
225-
{
226-
if (path.charAt(idx - 1) == '/')
227-
idx--;
228-
return path.substring(0, idx);
229-
}
230-
}
231-
return path;
232-
}
233-
return null;
282+
MatchedPath matched = matched(path);
283+
if (matched == null)
284+
return "";
285+
return matched.getPathMatch();
234286
}
235287

236288
@Override
@@ -277,74 +329,117 @@ private class RegexMatchedPath implements MatchedPath
277329
{
278330
private final RegexPathSpec pathSpec;
279331
private final String path;
280-
private final Matcher matcher;
332+
private String pathMatch;
333+
private String pathInfo;
281334

282335
public RegexMatchedPath(RegexPathSpec regexPathSpec, String path, Matcher matcher)
283336
{
284337
this.pathSpec = regexPathSpec;
285338
this.path = path;
286-
this.matcher = matcher;
339+
340+
calcPathMatchInfo(matcher);
287341
}
288342

289-
@Override
290-
public String getPathMatch()
343+
private void calcPathMatchInfo(Matcher matcher)
291344
{
292-
try
293-
{
294-
String p = matcher.group("name");
295-
if (p != null)
296-
{
297-
return p;
298-
}
299-
}
300-
catch (IllegalArgumentException ignore)
345+
int groupCount = matcher.groupCount();
346+
347+
348+
if (groupCount == 0)
301349
{
302-
// ignore if group name not found.
350+
pathMatch = path;
351+
pathInfo = null;
352+
return;
303353
}
304354

305-
if (pathSpec.getGroup() == PathSpecGroup.PREFIX_GLOB && matcher.groupCount() >= 1)
355+
if (groupCount == 1)
306356
{
357+
// we know we are splitting
358+
int idxNameEnd = endOf(matcher, "name");
359+
if (idxNameEnd >= 0)
360+
{
361+
pathMatch = path.substring(0, idxNameEnd);
362+
pathInfo = path.substring(idxNameEnd);
363+
364+
// If split on pathMatch ends with '/'
365+
// AND pathInfo doesn't have one, move the slash to pathInfo only move 1 level
366+
if (pathMatch.length() > 0 && pathMatch.charAt(pathMatch.length() - 1) == '/' &&
367+
!pathInfo.startsWith("/"))
368+
{
369+
pathMatch = pathMatch.substring(0, pathMatch.length() - 1);
370+
pathInfo = '/' + pathInfo;
371+
}
372+
return;
373+
}
374+
375+
// Use start of anonymous group
307376
int idx = matcher.start(1);
308-
if (idx > 0)
377+
if (idx >= 0)
309378
{
310-
if (this.path.charAt(idx - 1) == '/')
311-
idx--;
312-
return this.path.substring(0, idx);
379+
pathMatch = path.substring(0, idx);
380+
pathInfo = path.substring(idx);
381+
382+
if (pathMatch.length() > 0 && pathMatch.charAt(pathMatch.length() - 1) == '/' &&
383+
!pathInfo.startsWith("/"))
384+
{
385+
pathMatch = pathMatch.substring(0, pathMatch.length() - 1);
386+
pathInfo = '/' + pathInfo;
387+
}
388+
return;
313389
}
314390
}
315391

316-
// default is the full path
317-
return this.path;
392+
// Reach here we have 2+ groups
393+
394+
String gName = valueOf(matcher, "name");
395+
String gInfo = valueOf(matcher, "info");
396+
397+
// if both named groups exist
398+
if (gName != null && gInfo != null)
399+
{
400+
pathMatch = gName;
401+
pathInfo = gInfo;
402+
return;
403+
}
404+
405+
pathMatch = path;
406+
pathInfo = null;
318407
}
319408

320-
@Override
321-
public String getPathInfo()
409+
private String valueOf(Matcher matcher, String groupName)
322410
{
323411
try
324412
{
325-
String p = matcher.group("info");
326-
if (p != null)
327-
{
328-
return p;
329-
}
413+
return matcher.group(groupName);
330414
}
331-
catch (IllegalArgumentException ignore)
415+
catch (IllegalArgumentException notFound)
332416
{
333-
// ignore if group info not found.
417+
return null;
334418
}
419+
}
335420

336-
// Path Info only valid for PREFIX_GLOB
337-
if (pathSpec.getGroup() == PathSpecGroup.PREFIX_GLOB && matcher.groupCount() >= 1)
421+
private int endOf(Matcher matcher, String groupName)
422+
{
423+
try
424+
{
425+
return matcher.end(groupName);
426+
}
427+
catch (IllegalArgumentException notFound)
338428
{
339-
String pathInfo = matcher.group(1);
340-
if ("".equals(pathInfo))
341-
return "/";
342-
else
343-
return pathInfo;
429+
return -2;
344430
}
431+
}
345432

346-
// default is null
347-
return null;
433+
@Override
434+
public String getPathMatch()
435+
{
436+
return this.pathMatch;
437+
}
438+
439+
@Override
440+
public String getPathInfo()
441+
{
442+
return this.pathInfo;
348443
}
349444

350445
@Override
@@ -353,7 +448,6 @@ public String toString()
353448
return getClass().getSimpleName() + "[" +
354449
"pathSpec=" + pathSpec +
355450
", path=\"" + path + "\"" +
356-
", matcher=" + matcher +
357451
']';
358452
}
359453
}

0 commit comments

Comments
 (0)