Skip to content

Commit ab07919

Browse files
committed
[Fix #991] Changing URLPatternMatcher implementation
1 parent 556bab3 commit ab07919

File tree

2 files changed

+154
-90
lines changed

2 files changed

+154
-90
lines changed
Lines changed: 139 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,45 @@
11
/*
22
Original Copyright Headers
3-
This file has been modified, but copied from
4-
https://github.com/RestExpress/RestExpress/blob/master/core/src/main/java/org/restexpress/url/UrlPattern.java
3+
This file include excerpt copied from
4+
https://github.com/wilkincheung/URI-Template-Pattern-Matcher/blob/master/src/main/java/com/prodigi/service/UriTemplateValidator.java
55
*/
66
/**
7-
* Copyright 2010, Strategic Gains, Inc.
8-
* <p>
9-
* Licensed under the Apache License, Version 2.0 (the "License");
10-
* you may not use this file except in compliance with the License.
11-
* You may obtain a copy of the License at
12-
* <p>
13-
* http://www.apache.org/licenses/LICENSE-2.0
14-
* <p>
15-
* Unless required by applicable law or agreed to in writing, software
16-
* distributed under the License is distributed on an "AS IS" BASIS,
17-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18-
* See the License for the specific language governing permissions and
19-
* limitations under the License.
20-
*/
21-
package io.quarkiverse.openapi.generator.providers;
22-
23-
import java.util.regex.Pattern;
24-
25-
/**
26-
* PathPatternMatcher leverages Regex Pattern to represent a parameterized URL. Parameters within the URL are
27-
* denoted by curly braces '{}' with the parameter name contained within (e.g. '{userid}').
28-
* <p>
29-
* <p/>
30-
* Parameter names must be formed of word characters (e.g. A-Z, a-z, 0-9, '_').
31-
* <p/>
32-
* An optional format parameter following a dot ('.') may be added to the end. While it could be named any valid parameter name,
33-
* RestExpress offers special handling (e.g. within the Request, etc.) if it's named 'format'.
34-
* <p/>
35-
* Note that the format specifier allows only word characters and percent-encoded characters.
36-
* <p>
37-
* <p/>
38-
* URL Pattern examples:
39-
* <ul>
40-
* <li>/api/search.{format}</li>
41-
* <li>/api/search/users/{userid}.{format}</li>
42-
* <li>/api/{version}/search/users/{userid}</li>
43-
* </ul>
44-
* <p>
45-
* RestExpress parses URI paths which is described in the URI Generic Syntax IETF RFC 3986 specification,
46-
* section 3.3 (http://tools.ietf.org/html/rfc3986#section-3.3). RestExpress parses paths into segments
47-
* separated by slashes ("/"), the segments of which are composed of unreserved, percent encoded,
48-
* sub-delimiters, colon (":") or ampersand ("@"), each of which are defined below (from the spec):
49-
* <p/>
50-
* pct-encoded = "%" HEXDIG HEXDIG
51-
* <p/>
52-
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"<br/>
53-
* reserved = gen-delims / sub-delims<br/>
54-
* gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"</br>
55-
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" *
56-
* <p/>
57-
* In other words, RestExpress accepts path segments containing: [A-Z] [a-z] [0-9] % - . _ ~ ! $ & ' ( ) * + , ; = : @
58-
* <p/>
59-
* RestExpress also accepts square brackets ('[' and ']'), but this is deprecated and not recommended.
60-
*
61-
* @author toddf
62-
* @see <a href="Uniform Resource Identifier (URI): Generic Syntax">http://www.ietf.org/rfc/rfc3986.txt</a>
63-
* @since Apr 28, 2010
64-
*/
65-
public class UrlPatternMatcher {
66-
// Finds parameters in the URL pattern string.
67-
private static final String URL_PARAM_REGEX = "\\{(\\S*?)\\}";
7+
The MIT License (MIT)
8+
Copyright (c) 2015 Wilkin Cheung
689
69-
// Replaces parameter names in the URL pattern string to match parameters in URLs.
70-
private static final String URL_PARAM_MATCH_REGEX = "\\([%\\\\w-.\\\\~!\\$&'\\\\(\\\\)\\\\*\\\\+,;=:\\\\[\\\\]@]+?\\)";
10+
Permission is hereby granted, free of charge, to any person obtaining a copy
11+
of this software and associated documentation files (the "Software"), to deal
12+
in the Software without restriction, including without limitation the rights
13+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14+
copies of the Software, and to permit persons to whom the Software is
15+
furnished to do so, subject to the following conditions:
7116
72-
// Finds the 'format' portion of the URL pattern string.
73-
private static final String URL_FORMAT_REGEX = "(?:\\.\\{format\\})$";
17+
The above copyright notice and this permission notice shall be included in all
18+
copies or substantial portions of the Software.
7419
75-
// Replaces the format parameter name in the URL pattern string to match the format specifier in URLs. Appended to the end of the regex string
76-
// when a URL pattern contains a format parameter.
77-
private static final String URL_FORMAT_MATCH_REGEX = "(?:\\\\.\\([\\\\w%]+?\\))?";
20+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26+
SOFTWARE.
27+
*/
28+
package io.quarkiverse.openapi.generator.providers;
7829

79-
// Finds the query string portion within a URL. Appended to the end of the built-up regex string.
80-
private static final String URL_QUERY_STRING_REGEX = "(?:\\?.*?)?$";
30+
import java.util.ArrayList;
31+
import java.util.LinkedHashMap;
32+
import java.util.List;
33+
import java.util.Map;
34+
import java.util.regex.Matcher;
35+
import java.util.regex.Pattern;
8136

82-
/**
83-
* The URL pattern describing the URL layout and any parameters.
84-
*/
85-
private final String urlPattern;
37+
public class UrlPatternMatcher {
8638

87-
/**
88-
* A compiled regex created from the urlPattern, above.
89-
*/
90-
private Pattern compiledUrl;
39+
private UriTemplate uriTemplate;
9140

92-
/**
93-
* @param pattern
94-
*/
9541
public UrlPatternMatcher(String pattern) {
96-
this.urlPattern = pattern;
97-
String parsedPattern = this.urlPattern.replaceFirst(URL_FORMAT_REGEX, URL_FORMAT_MATCH_REGEX);
98-
parsedPattern = parsedPattern.replaceAll(URL_PARAM_REGEX, URL_PARAM_MATCH_REGEX);
99-
this.compiledUrl = Pattern.compile(parsedPattern + URL_QUERY_STRING_REGEX);
42+
this.uriTemplate = new UriTemplate(pattern);
10043
}
10144

10245
/**
@@ -107,6 +50,113 @@ public UrlPatternMatcher(String pattern) {
10750
* @return true if the given URL matches the underlying pattern. Otherwise false.
10851
*/
10952
public boolean matches(String url) {
110-
return compiledUrl.matcher(url).matches();
53+
return uriTemplate.matches(url);
54+
}
55+
56+
private static class UriTemplate {
57+
58+
// For each pattern {keyName} replaces it with (.*)
59+
private static final Pattern LEVEL_ONE_PATTERN = Pattern
60+
.compile("\\{([^/]+?)\\}");
61+
// Replaces each {keyName} with (.*)
62+
private static final String REPLACES_WITH = "(.*)";
63+
64+
/**
65+
* Ordered keyNames
66+
*/
67+
private final List<String> keys;
68+
69+
/**
70+
* Pattern
71+
*/
72+
private final Pattern pattern;
73+
74+
/**
75+
* UriTemplate for internal parsing to regular expression
76+
*
77+
* @param uriTemplate uriTemplate to be parsed
78+
*/
79+
public UriTemplate(String uriTemplate) {
80+
81+
StringBuilder patternBuilder = new StringBuilder();
82+
83+
keys = new ArrayList<String>();
84+
Matcher m = LEVEL_ONE_PATTERN.matcher(uriTemplate);
85+
int start;
86+
int end = 0;
87+
88+
// In each loop, find next pattern in URI that is "{keyName}"
89+
// If found, then add "keyName" to keyNames, and append the substring to
90+
// patternBuilder.
91+
while (m.find()) {
92+
93+
// move start pointer to last match
94+
start = m.start();
95+
96+
// Mark the pattern as escaped
97+
String escaped = Pattern.quote(uriTemplate.substring(end, start));
98+
99+
patternBuilder.append(escaped);
100+
101+
patternBuilder.append(REPLACES_WITH);
102+
103+
// save the previously matched sequence (that is, keyName)
104+
// group(1) means the substring within (.*)
105+
keys.add(m.group(1));
106+
107+
// move end pointer to the end of matched string
108+
end = m.end();
109+
}
110+
111+
// Mark the pattern as escaped
112+
patternBuilder.append(Pattern.quote(uriTemplate.substring(end,
113+
uriTemplate
114+
.length())));
115+
this.pattern = Pattern.compile(patternBuilder.toString());
116+
117+
}
118+
119+
/**
120+
* Match the given URI to a map of key values. Keys in the returned map are
121+
* key names, values are key values, as occurred in the given URI.
122+
*
123+
* For example:
124+
* <code>
125+
* UriTemplate t = new UriTemplate("/human/v1/{typeA}/en-US/{typeB}/id/{typeC}.json");
126+
* t.match("http://api.prodigisoftware.com/human/v1/rec/en-US/movies/id/123.json");
127+
* </code>
128+
* ...would return: {typeA=rec, typeB=movies, typeC=123}
129+
*
130+
* If not a match, then returning <code>map</code> will have size 0.
131+
*
132+
* @param uri to try to match to
133+
* @return Map of matching key name (Map key), key value (Map value)
134+
*/
135+
public Map<String, String> match(String uri) {
136+
// LinkedHashMap to maintain key insertion order
137+
Map<String, String> result = new LinkedHashMap<>(keys.size());
138+
139+
Matcher matcher = pattern.matcher(uri);
140+
141+
// find next part in uri that matches the pattern
142+
if (matcher.find()) {
143+
for (int i = 0; i < matcher.groupCount(); i++) {
144+
String name = keys.get(i);
145+
String value = matcher.group(i + 1);
146+
result.put(name, value);
147+
}
148+
}
149+
return result;
150+
}
151+
152+
/**
153+
* See if uri matches the pattern.
154+
*
155+
* @param uri String for matching
156+
* @return true if there is one or more matches; false otherwise
157+
*/
158+
public boolean matches(String uri) {
159+
return match(uri).size() > 0;
160+
}
111161
}
112-
}
162+
}

client/runtime/src/test/java/io/quarkiverse/openapi/generator/providers/UrlPatternMatcherTest.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ void verifyPathsMatch(final String pathPattern, final String requestPath) {
1616
Assertions.assertTrue(pattern.matches(requestPath));
1717
}
1818

19+
@ParameterizedTest
20+
@MethodSource("providePathsThatNotMatch")
21+
void verifyPathsNotMatch(final String pathPattern, final String requestPath) {
22+
UrlPatternMatcher pattern = new UrlPatternMatcher(pathPattern);
23+
Assertions.assertFalse(pattern.matches(requestPath));
24+
}
25+
1926
private static Stream<Arguments> providePathsThatMatch() {
2027
return Stream.of(
2128
Arguments.of("/pets/{id}", "/pets/1"),
@@ -32,7 +39,14 @@ private static Stream<Arguments> providePathsThatMatch() {
3239
Arguments.of("/{id}/{foo}/{id2}", "/1/2/3?q=1&q2=2"),
3340
Arguments.of("/{id}/{foo}/{id2}", "/1/2/3"),
3441
Arguments.of("/v2/pets/{id}", "/v2/pets/1"),
35-
Arguments.of("/pets/{pet-id}/types/{type-id}", "/pets/1/types/2"));
42+
Arguments.of("/pets/{pet-id}/types/{type-id}", "/pets/1/types/2"),
43+
Arguments.of("/repos/{ref}", "/repos/prefixed/cool.sw"));
44+
}
45+
46+
private static Stream<Arguments> providePathsThatNotMatch() {
47+
return Stream.of(
48+
Arguments.of("/pets/{id}", "/pes/1"),
49+
Arguments.of("/{id}/pepe", "/1/2/pep"));
3650
}
3751

3852
}

0 commit comments

Comments
 (0)