2121import org .slf4j .Logger ;
2222import 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+ */
2495public 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