2 // ========================================================================
3 // Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4 // ------------------------------------------------------------------------
5 // All rights reserved. This program and the accompanying materials
6 // are made available under the terms of the Eclipse Public License v1.0
7 // and Apache License v2.0 which accompanies this distribution.
9 // The Eclipse Public License is available at
10 // http://www.eclipse.org/legal/epl-v10.html
12 // The Apache License v2.0 is available at
13 // http://www.opensource.org/licenses/apache2.0.php
15 // You may elect to redistribute this code under either of these licenses.
16 // ========================================================================
19 package org.eclipse.jetty.http.pathmap;
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.HashSet;
25 import java.util.List;
27 import java.util.Objects;
29 import java.util.regex.Matcher;
30 import java.util.regex.Pattern;
32 import org.eclipse.jetty.util.TypeUtil;
33 import org.eclipse.jetty.util.log.Log;
34 import org.eclipse.jetty.util.log.Logger;
37 * PathSpec for URI Template based declarations
39 * @see <a href="https://tools.ietf.org/html/rfc6570">URI Templates (Level 1)</a>
41 public class UriTemplatePathSpec extends RegexPathSpec
43 private static final Logger LOG = Log.getLogger(UriTemplatePathSpec.class);
45 private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{(.*)\\}");
46 /** Reserved Symbols in URI Template variable */
47 private static final String VARIABLE_RESERVED = ":/?#[]@" + // gen-delims
48 "!$&'()*+,;="; // sub-delims
49 /** Allowed Symbols in a URI Template variable */
50 private static final String VARIABLE_SYMBOLS="-._";
51 private static final Set<String> FORBIDDEN_SEGMENTS;
55 FORBIDDEN_SEGMENTS = new HashSet<>();
56 FORBIDDEN_SEGMENTS.add("/./");
57 FORBIDDEN_SEGMENTS.add("/../");
58 FORBIDDEN_SEGMENTS.add("//");
61 private String variables[];
63 public UriTemplatePathSpec(String rawSpec)
66 Objects.requireNonNull(rawSpec,"Path Param Spec cannot be null");
68 if ("".equals(rawSpec) || "/".equals(rawSpec))
71 super.pattern = Pattern.compile("^/$");
74 this.variables = new String[0];
75 this.group = PathSpecGroup.EXACT;
79 if (rawSpec.charAt(0) != '/')
81 // path specs must start with '/'
82 StringBuilder err = new StringBuilder();
83 err.append("Syntax Error: path spec \"");
85 err.append("\" must start with '/'");
86 throw new IllegalArgumentException(err.toString());
89 for (String forbidden : FORBIDDEN_SEGMENTS)
91 if (rawSpec.contains(forbidden))
93 StringBuilder err = new StringBuilder();
94 err.append("Syntax Error: segment ");
95 err.append(forbidden);
96 err.append(" is forbidden in path spec: ");
98 throw new IllegalArgumentException(err.toString());
102 this.pathSpec = rawSpec;
104 StringBuilder regex = new StringBuilder();
107 List<String> varNames = new ArrayList<>();
108 // split up into path segments (ignoring the first slash that will always be empty)
109 String segments[] = rawSpec.substring(1).split("/");
110 char segmentSignature[] = new char[segments.length];
111 this.pathDepth = segments.length;
112 for (int i = 0; i < segments.length; i++)
114 String segment = segments[i];
115 Matcher mat = VARIABLE_PATTERN.matcher(segment);
119 // entire path segment is a variable.
120 String variable = mat.group(1);
121 if (varNames.contains(variable))
123 // duplicate variable names
124 StringBuilder err = new StringBuilder();
125 err.append("Syntax Error: variable ");
126 err.append(variable);
127 err.append(" is duplicated in path spec: ");
129 throw new IllegalArgumentException(err.toString());
132 assertIsValidVariableLiteral(variable);
134 segmentSignature[i] = 'v'; // variable
135 // valid variable name
136 varNames.add(variable);
138 regex.append("/([^/]+)");
140 else if (mat.find(0))
142 // variable exists as partial segment
143 StringBuilder err = new StringBuilder();
144 err.append("Syntax Error: variable ");
145 err.append(mat.group());
146 err.append(" must exist as entire path segment: ");
148 throw new IllegalArgumentException(err.toString());
150 else if ((segment.indexOf('{') >= 0) || (segment.indexOf('}') >= 0))
152 // variable is split with a path separator
153 StringBuilder err = new StringBuilder();
154 err.append("Syntax Error: invalid path segment /");
156 err.append("/ variable declaration incomplete: ");
158 throw new IllegalArgumentException(err.toString());
160 else if (segment.indexOf('*') >= 0)
163 StringBuilder err = new StringBuilder();
164 err.append("Syntax Error: path segment /");
166 err.append("/ contains a wildcard symbol (not supported by this uri-template implementation): ");
168 throw new IllegalArgumentException(err.toString());
172 // valid path segment
173 segmentSignature[i] = 'e'; // exact
176 // escape regex special characters
177 for (char c : segment.toCharArray())
179 if ((c == '.') || (c == '[') || (c == ']') || (c == '\\'))
188 // Handle trailing slash (which is not picked up during split)
189 if(rawSpec.charAt(rawSpec.length()-1) == '/')
196 this.pattern = Pattern.compile(regex.toString());
198 int varcount = varNames.size();
199 this.variables = varNames.toArray(new String[varcount]);
201 // Convert signature to group
202 String sig = String.valueOf(segmentSignature);
204 if (Pattern.matches("^e*$",sig))
206 this.group = PathSpecGroup.EXACT;
208 else if (Pattern.matches("^e*v+",sig))
210 this.group = PathSpecGroup.PREFIX_GLOB;
212 else if (Pattern.matches("^v+e+",sig))
214 this.group = PathSpecGroup.SUFFIX_GLOB;
218 this.group = PathSpecGroup.MIDDLE_GLOB;
223 * Validate variable literal name, per RFC6570, Section 2.1 Literals
225 * @param pathParamSpec
227 private void assertIsValidVariableLiteral(String variable)
229 int len = variable.length();
233 boolean valid = (len > 0); // must not be zero length
235 while (valid && i < len)
237 codepoint = variable.codePointAt(i);
238 i += Character.charCount(codepoint);
240 // basic letters, digits, or symbols
241 if (isValidBasicLiteralCodepoint(codepoint))
246 // The ucschar and iprivate pieces
247 if (Character.isSupplementaryCodePoint(codepoint))
253 if (codepoint == '%')
257 // invalid percent encoding, missing extra 2 chars
261 codepoint = TypeUtil.convertHexDigit(variable.codePointAt(i++)) << 4;
262 codepoint |= TypeUtil.convertHexDigit(variable.codePointAt(i++));
264 // validate basic literal
265 if (isValidBasicLiteralCodepoint(codepoint))
276 // invalid variable name
277 StringBuilder err = new StringBuilder();
278 err.append("Syntax Error: variable {");
279 err.append(variable);
280 err.append("} an invalid variable name: ");
281 err.append(pathSpec);
282 throw new IllegalArgumentException(err.toString());
286 private boolean isValidBasicLiteralCodepoint(int codepoint)
288 // basic letters or digits
289 if((codepoint >= 'a' && codepoint <= 'z') ||
290 (codepoint >= 'A' && codepoint <= 'Z') ||
291 (codepoint >= '0' && codepoint <= '9'))
296 // basic allowed symbols
297 if(VARIABLE_SYMBOLS.indexOf(codepoint) >= 0)
299 return true; // valid simple value
302 // basic reserved symbols
303 if(VARIABLE_RESERVED.indexOf(codepoint) >= 0)
305 LOG.warn("Detected URI Template reserved symbol [{}] in path spec \"{}\"",(char)codepoint,pathSpec);
306 return false; // valid simple value
312 public Map<String, String> getPathParams(String path)
314 Matcher matcher = getMatcher(path);
315 if (matcher.matches())
317 if (group == PathSpecGroup.EXACT)
319 return Collections.emptyMap();
321 Map<String, String> ret = new HashMap<>();
322 int groupCount = matcher.groupCount();
323 for (int i = 1; i <= groupCount; i++)
325 ret.put(this.variables[i - 1],matcher.group(i));
332 public int getVariableCount()
334 return variables.length;
337 public String[] getVariables()
339 return this.variables;