You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by am...@apache.org on 2009/04/10 23:07:31 UTC
svn commit: r764056 - in /cxf/trunk/rt/frontend/jaxrs/src:
main/java/org/apache/cxf/jaxrs/model/URITemplate.java
test/java/org/apache/cxf/jaxrs/model/URITemplateTest.java
Author: amichalec
Date: Fri Apr 10 21:07:31 2009
New Revision: 764056
URL: http://svn.apache.org/viewvc?rev=764056&view=rev
Log:
CXF-2014: support for nested curly braces in UriTemplate
Modified:
cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/model/URITemplate.java
cxf/trunk/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/model/URITemplateTest.java
Modified: cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/model/URITemplate.java
URL: http://svn.apache.org/viewvc/cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/model/URITemplate.java?rev=764056&r1=764055&r2=764056&view=diff
==============================================================================
--- cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/model/URITemplate.java (original)
+++ cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/model/URITemplate.java Fri Apr 10 21:07:31 2009
@@ -26,6 +26,7 @@
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
import javax.ws.rs.Path;
import javax.ws.rs.core.MultivaluedMap;
@@ -39,51 +40,43 @@
public static final String TEMPLATE_PARAMETERS = "jaxrs.template.parameters";
public static final String LIMITED_REGEX_SUFFIX = "(/.*)?";
public static final String FINAL_MATCH_GROUP = "FINAL_MATCH_GROUP";
-
- /**
- * The regular expression for matching URI templates and names.
- */
- private static final Pattern TEMPLATE_NAMES_PATTERN = Pattern.compile("\\{(\\w[-\\w\\.]*)(\\:(.+?))?\\}");
-
private static final String DEFAULT_PATH_VARIABLE_REGEX = "([^/]+?)";
private static final String CHARACTERS_TO_ESCAPE = ".";
private final String template;
- private final List<String> templateVariables = new ArrayList<String>();
- private final List<String> customTemplateVariables = new ArrayList<String>();
+ private final List<String> variables = new ArrayList<String>();
+ private final List<String> customVariables = new ArrayList<String>();
private final Pattern templateRegexPattern;
private final String literals;
+ private final List<UriChunk> uriChunks;
public URITemplate(String theTemplate) {
-
- this.template = theTemplate;
-
+ template = theTemplate;
StringBuilder literalChars = new StringBuilder();
StringBuilder patternBuilder = new StringBuilder();
-
- // compute a regular expression from URI template
- Matcher matcher = TEMPLATE_NAMES_PATTERN.matcher(template);
- int i = 0;
- while (matcher.find()) {
- templateVariables.add(matcher.group(1).trim());
-
- String substr = escapeCharacters(template.substring(i, matcher.start()));
- literalChars.append(substr);
- patternBuilder.append(substr);
- i = matcher.end();
- if (matcher.group(2) != null && matcher.group(3) != null) {
- patternBuilder.append('(');
- patternBuilder.append(matcher.group(3).trim());
- patternBuilder.append(')');
- customTemplateVariables.add(matcher.group(1).trim());
- } else {
- patternBuilder.append(DEFAULT_PATH_VARIABLE_REGEX);
+ CurlyBraceTokenizer tok = new CurlyBraceTokenizer(template);
+ uriChunks = new ArrayList<UriChunk>();
+ while (tok.hasNext()) {
+ String templatePart = tok.next();
+ UriChunk chunk = UriChunk.createUriChunk(templatePart);
+ uriChunks.add(chunk);
+ if (chunk instanceof Literal) {
+ String substr = escapeCharacters(chunk.getValue());
+ literalChars.append(substr);
+ patternBuilder.append(substr);
+ } else if (chunk instanceof Variable) {
+ Variable var = (Variable)chunk;
+ variables.add(var.getName());
+ if (var.getPattern() != null) {
+ customVariables.add(var.getName());
+ patternBuilder.append('(');
+ patternBuilder.append(var.getPattern());
+ patternBuilder.append(')');
+ } else {
+ patternBuilder.append(DEFAULT_PATH_VARIABLE_REGEX);
+ }
}
}
- String substr = escapeCharacters(template.substring(i, template.length()));
- literalChars.append(substr);
- patternBuilder.append(substr);
-
literals = literalChars.toString();
int endPos = patternBuilder.length() - 1;
@@ -104,12 +97,23 @@
return template;
}
+ /**
+ * List of all variables in order of appearance in template.
+ *
+ * @return unmodifiable list of variable names w/o patterns, e.g. for "/foo/{v1:\\d}/{v2}" returned list
+ * is ["v1","v2"].
+ */
public List<String> getVariables() {
- return Collections.unmodifiableList(templateVariables);
+ return Collections.unmodifiableList(variables);
}
+ /**
+ * List of variables with patterns (regexps). List is subset of elements from {@link #getVariables()}.
+ *
+ * @return unmodifiable list of variables names w/o patterns.
+ */
public List<String> getCustomVariables() {
- return Collections.unmodifiableList(customTemplateVariables);
+ return Collections.unmodifiableList(customVariables);
}
private static String escapeCharacters(String expression) {
@@ -139,7 +143,8 @@
Matcher m = templateRegexPattern.matcher(uri);
if (!m.matches()) {
if (uri.contains(";")) {
- // we might be trying to match one or few path segments containing matrix
+ // we might be trying to match one or few path segments
+ // containing matrix
// parameters against a clear path segment as in @Path("base").
List<PathSegment> pList = JAXRSUtils.getPathSegments(template, false);
List<PathSegment> uList = JAXRSUtils.getPathSegments(uri, false);
@@ -164,12 +169,13 @@
// Assign the matched template values to template variables
int i = 1;
- for (String name : templateVariables) {
+ for (String name : variables) {
String value = m.group(i++);
templateVariableToValue.add(name, value);
}
- // The right hand side value, might be used to further resolve sub-resources.
+ // The right hand side value, might be used to further resolve
+ // sub-resources.
String finalGroup = m.group(i);
templateVariableToValue.putSingle(FINAL_MATCH_GROUP, finalGroup == null ? "/" : finalGroup);
@@ -196,24 +202,26 @@
if (values == null) {
throw new IllegalArgumentException("values is null");
}
- Matcher m = TEMPLATE_NAMES_PATTERN.matcher(template);
- Iterator<String> valIter = values.iterator();
+ Iterator<String> iter = values.iterator();
StringBuffer sb = new StringBuffer();
- while (m.find() && valIter.hasNext()) {
- String value = valIter.next();
- String varPattern = m.group(2);
- if (varPattern != null) {
- // variable has pattern, matching formats e.g.
- // for "{a:\d\d}" variable value must have two digits etc.
- Pattern p = Pattern.compile(varPattern);
- if (!p.matcher(":" + value).matches()) {
- throw new IllegalArgumentException("Value '" + value + "' does not match variable "
- + m.group());
+ for (UriChunk chunk : uriChunks) {
+ if (chunk instanceof Variable) {
+ Variable var = (Variable)chunk;
+ if (iter.hasNext()) {
+ String value = iter.next();
+ if (!var.matches(value)) {
+ throw new IllegalArgumentException("Value '" + value + "' does not match variable "
+ + var.getName() + " with pattern "
+ + var.getPattern());
+ }
+ sb.append(value);
+ } else {
+ sb.append(var);
}
+ } else {
+ sb.append(chunk);
}
- m.appendReplacement(sb, value);
}
- m.appendTail(sb);
return sb.toString();
}
@@ -228,32 +236,31 @@
*
* @param valuesMap map variables to their values; on each value Object.toString() is called.
* @return template with bound variables.
- * @throws IllegalArgumentException when size of list of values differs from list of variables or list
- * contains nulls.
*/
public String substitute(Map<String, ? extends Object> valuesMap) throws IllegalArgumentException {
if (valuesMap == null) {
throw new IllegalArgumentException("valuesMap is null");
}
- Matcher m = TEMPLATE_NAMES_PATTERN.matcher(template);
StringBuffer sb = new StringBuffer();
- while (m.find()) {
- Object value = valuesMap.get(m.group(1));
- if (value == null) {
- continue;
- }
- String sval = value.toString();
- String varPattern = m.group(2);
- if (varPattern != null) {
- Pattern p = Pattern.compile(varPattern);
- if (!p.matcher(":" + sval).matches()) {
- throw new IllegalArgumentException("Value '" + sval + "' does not match variable "
- + m.group());
+ for (UriChunk chunk : uriChunks) {
+ if (chunk instanceof Variable) {
+ Variable var = (Variable)chunk;
+ Object value = valuesMap.get(var.getName());
+ if (value != null) {
+ String sval = value.toString();
+ if (!var.matches(sval)) {
+ throw new IllegalArgumentException("Value '" + sval + "' does not match variable "
+ + var.getName() + " with pattern "
+ + var.getPattern());
+ }
+ sb.append(value);
+ } else {
+ sb.append(var);
}
+ } else {
+ sb.append(chunk);
}
- m.appendReplacement(sb, sval);
}
- m.appendTail(sb);
return sb.toString();
}
@@ -263,15 +270,16 @@
* @return encoded value
*/
public String encodeLiteralCharacters() {
- StringBuilder sb = new StringBuilder();
- Matcher matcher = TEMPLATE_NAMES_PATTERN.matcher(template);
- int i = 0;
- while (matcher.find()) {
- sb.append(HttpUtils.encodePartiallyEncoded(template.substring(i, matcher.start()), false));
- sb.append('{').append(matcher.group(1)).append('}');
- i = matcher.end();
+ final float ENCODED_RATIO = 1.5f;
+ StringBuffer sb = new StringBuffer((int)(ENCODED_RATIO * template.length()));
+ for (UriChunk chunk : uriChunks) {
+ String val = chunk.getValue();
+ if (chunk instanceof Literal) {
+ sb.append(HttpUtils.encodePartiallyEncoded(val, false));
+ } else {
+ sb.append(val);
+ }
}
- sb.append(HttpUtils.encodePartiallyEncoded(template.substring(i, template.length()), false));
return sb.toString();
}
@@ -297,13 +305,13 @@
return l1.length() < l2.length() ? 1 : -1;
}
- int g1 = t1.templateVariables.size();
- int g2 = t2.templateVariables.size();
+ int g1 = t1.getVariables().size();
+ int g2 = t2.getVariables().size();
// descending order
int result = g1 < g2 ? 1 : g1 > g2 ? -1 : 0;
if (result == 0) {
- int gCustom1 = t1.customTemplateVariables.size();
- int gCustom2 = t2.customTemplateVariables.size();
+ int gCustom1 = t1.getCustomVariables().size();
+ int gCustom2 = t2.getCustomVariables().size();
if (gCustom1 != gCustom2) {
// descending order
return gCustom1 < gCustom2 ? 1 : -1;
@@ -311,4 +319,213 @@
}
return result;
}
+
+ /**
+ * Stringified part of URI. Chunk is not URI segment; chunk can span over multiple URI segments or one URI
+ * segments can have multiple chunks. Chunk is used to decompose URI of {@link URITemplate} into literals
+ * and variables. Example: "foo/bar/{baz}{blah}" is decomposed into chunks: "foo/bar", "{baz}" and
+ * "{blah}".
+ */
+ private abstract static class UriChunk {
+ /**
+ * Creates object form string.
+ *
+ * @param uriChunk stringified uri chunk
+ * @return If param has variable form then {@link Variable} instance is created, otherwise chunk is
+ * treated as {@link Literal}.
+ */
+ public static UriChunk createUriChunk(String uriChunk) {
+ if (uriChunk == null || "".equals(uriChunk)) {
+ throw new IllegalArgumentException("uriChunk is empty");
+ }
+ try {
+ return new Variable(uriChunk);
+ } catch (IllegalArgumentException e) {
+ return new Literal(uriChunk);
+ }
+ }
+
+ public abstract String getValue();
+
+ @Override
+ public String toString() {
+ return getValue();
+ }
+ }
+
+ private static final class Literal extends UriChunk {
+ private String value;
+
+ public Literal(String uriChunk) {
+ if (uriChunk == null || "".equals(uriChunk)) {
+ throw new IllegalArgumentException("uriChunk is empty");
+ }
+ value = uriChunk;
+ }
+
+ @Override
+ public String getValue() {
+ return value;
+ }
+
+ }
+
+ /**
+ * Variable of URITemplate. Variable has either "{varname:pattern}" syntax or "{varname}".
+ */
+ private static final class Variable extends UriChunk {
+ private static final Pattern VARIABLE_PATTERN = Pattern.compile("(\\w[-\\w\\.]*)(\\:(.+))?");
+ private String name;
+ private Pattern pattern;
+
+ /**
+ * Creates variable from stringified part of URI.
+ *
+ * @param uriChunk chunk that depicts variable
+ * @throws IllegalArgumentException when param is null, empty or does not have variable syntax.
+ * @throws PatternSyntaxException when pattern of variable has wrong syntax.
+ */
+ public Variable(String uriChunk) throws IllegalArgumentException, PatternSyntaxException {
+ if (uriChunk == null || "".equals(uriChunk)) {
+ throw new IllegalArgumentException("uriChunk is empty");
+ }
+ if (CurlyBraceTokenizer.insideBraces(uriChunk)) {
+ uriChunk = CurlyBraceTokenizer.stripBraces(uriChunk);
+ Matcher matcher = VARIABLE_PATTERN.matcher(uriChunk);
+ if (matcher.matches()) {
+ name = matcher.group(1).trim();
+ if (matcher.group(2) != null && matcher.group(3) != null) {
+ pattern = Pattern.compile(matcher.group(3).trim());
+ }
+ return;
+ }
+ }
+ throw new IllegalArgumentException("not a variable syntax");
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getPattern() {
+ return pattern != null ? pattern.pattern() : null;
+ }
+
+ /**
+ * Checks whether value matches variable. If variable has pattern its checked against, otherwise true
+ * is returned.
+ *
+ * @param value value of variable
+ * @return true if value is valid for variable, false otherwise.
+ */
+ public boolean matches(String value) {
+ if (pattern == null) {
+ return true;
+ } else {
+ return pattern.matcher(value).matches();
+ }
+ }
+
+ @Override
+ public String getValue() {
+ if (pattern != null) {
+ return "{" + name + ":" + pattern + "}";
+ } else {
+ return "{" + name + "}";
+ }
+ }
+ }
+}
+
+/**
+ * Splits string into parts inside and outside curly braces. Nested curly braces are ignored and treated as
+ * part inside top-level curly braces. Example: string "foo{bar{baz}}blah" is split into three tokens, "foo",
+ * "{bar{baz}}" and "blah". When closed bracket is missing, whole unclosed part is returned as one token,
+ * e.g.: "foo{bar" is split into "foo" and "{bar". When opening bracket is missing, closing bracked is ignored
+ * and taken as part of current token e.g.: "foo{bar}baz}blah" is split into "foo", "{bar}" and "baz}blah".
+ * <p>
+ * This is helper class for {@link URITemplate} that enables recurring literals appearing next to regular
+ * expressions e.g. "/foo/{zipcode:[0-9]{5}}/". Nested expressions with closed sections, like open-closed
+ * brackets causes expression to be out of regular grammar (is context-free grammar) which are not supported
+ * by Java regexp version.
+ *
+ * @author amichalec
+ * @version $Rev$
+ */
+final class CurlyBraceTokenizer {
+
+ private List<String> tokens = new ArrayList<String>();
+ private int tokenIdx;
+
+ public CurlyBraceTokenizer(String string) {
+ boolean outside = true;
+ int level = 0;
+ int lastIdx = 0;
+ int idx;
+ for (idx = 0; idx < string.length(); idx++) {
+ if (string.charAt(idx) == '{') {
+ if (outside) {
+ if (lastIdx < idx) {
+ tokens.add(string.substring(lastIdx, idx));
+ }
+ lastIdx = idx;
+ outside = false;
+ } else {
+ level++;
+ }
+ } else if (string.charAt(idx) == '}' && !outside) {
+ if (level > 0) {
+ level--;
+ } else {
+ if (lastIdx < idx) {
+ tokens.add(string.substring(lastIdx, idx + 1));
+ }
+ lastIdx = idx + 1;
+ outside = true;
+ }
+ }
+ }
+ if (lastIdx < idx) {
+ tokens.add(string.substring(lastIdx, idx));
+ }
+ }
+
+ /**
+ * Token is enclosed by curly braces.
+ *
+ * @param token
+ * text to verify
+ * @return true if enclosed, false otherwise.
+ */
+ public static boolean insideBraces(String token) {
+ return token.charAt(0) == '{' && token.charAt(token.length() - 1) == '}';
+ }
+
+ /**
+ * Strips token from enclosed curly braces. If token is not enclosed method
+ * has no side effect.
+ *
+ * @param token
+ * text to verify
+ * @return text stripped from curly brace begin-end pair.
+ */
+ public static String stripBraces(String token) {
+ if (insideBraces(token)) {
+ return token.substring(1, token.length() - 1);
+ } else {
+ return token;
+ }
+ }
+
+ public boolean hasNext() {
+ return tokens.size() > tokenIdx;
+ }
+
+ public String next() {
+ if (hasNext()) {
+ return tokens.get(tokenIdx++);
+ } else {
+ throw new IllegalStateException("no more elements");
+ }
+ }
}
Modified: cxf/trunk/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/model/URITemplateTest.java
URL: http://svn.apache.org/viewvc/cxf/trunk/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/model/URITemplateTest.java?rev=764056&r1=764055&r2=764056&view=diff
==============================================================================
--- cxf/trunk/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/model/URITemplateTest.java (original)
+++ cxf/trunk/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/model/URITemplateTest.java Fri Apr 10 21:07:31 2009
@@ -26,7 +26,6 @@
import javax.ws.rs.core.MultivaluedMap;
import org.apache.cxf.jaxrs.impl.MetadataMap;
-
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
@@ -305,12 +304,12 @@
List<String> list = Arrays.asList("bar", "baz", "blah");
assertEquals("Wrong substitution", "/foo/bar/baz/blah", ut.substitute(list));
}
-
+
@Test
public void testSubstituteListIncomplete() throws Exception {
- URITemplate ut = new URITemplate("/foo/{a}/{c}/{b}");
+ URITemplate ut = new URITemplate("/foo/{a}/{c}/{b}/{d:\\w}");
List<String> list = Arrays.asList("bar", "baz");
- assertEquals("Wrong substitution", "/foo/bar/baz/{b}", ut.substitute(list));
+ assertEquals("Wrong substitution", "/foo/bar/baz/{b}/{d:\\w}", ut.substitute(list));
}
@Test
@@ -375,4 +374,85 @@
map.put("a", "blah");
assertEquals("Wrong substitution", "/foo/blah", ut.substitute(map));
}
+
+ @Test
+ public void testVariables() {
+ URITemplate ut = new URITemplate("/foo/{a}/bar{c:\\d}{b:\\w}/{e}/{d}");
+ assertEquals(Arrays.asList("a", "c", "b", "e", "d"), ut.getVariables());
+ assertEquals(Arrays.asList("c", "b"), ut.getCustomVariables());
+ }
+
+ @Test
+ public void testTokenizerNoBraces() {
+ CurlyBraceTokenizer tok = new CurlyBraceTokenizer("nobraces");
+ assertEquals("nobraces", tok.next());
+ assertFalse(tok.hasNext());
+ }
+
+ @Test
+ public void testTokenizerNoNesting() {
+ CurlyBraceTokenizer tok = new CurlyBraceTokenizer("foo{bar}baz");
+ assertEquals("foo", tok.next());
+ assertEquals("{bar}", tok.next());
+ assertEquals("baz", tok.next());
+ assertFalse(tok.hasNext());
+ }
+
+ @Test
+ public void testTokenizerNesting() {
+ CurlyBraceTokenizer tok = new CurlyBraceTokenizer("foo{bar{baz}}blah");
+ assertEquals("foo", tok.next());
+ assertEquals("{bar{baz}}", tok.next());
+ assertEquals("blah", tok.next());
+ assertFalse(tok.hasNext());
+ }
+
+ @Test
+ public void testTokenizerNoClosing() {
+ CurlyBraceTokenizer tok = new CurlyBraceTokenizer("foo{bar}baz{blah");
+ assertEquals("foo", tok.next());
+ assertEquals("{bar}", tok.next());
+ assertEquals("baz", tok.next());
+ assertEquals("{blah", tok.next());
+ assertFalse(tok.hasNext());
+ }
+
+ @Test
+ public void testTokenizerNoOpening() {
+ CurlyBraceTokenizer tok = new CurlyBraceTokenizer("foo}bar}baz");
+ assertEquals("foo}bar}baz", tok.next());
+ assertFalse(tok.hasNext());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testUnclosedVariable() {
+ new URITemplate("/foo/{var/bar");
+ }
+
+ @Test
+ public void testUnopenedVariable() {
+ URITemplate ut = new URITemplate("/foo/var}/bar");
+ assertEquals("/foo/var}/bar", ut.getValue());
+ }
+
+ @Test
+ public void testNestedCurlyBraces() {
+ URITemplate ut = new URITemplate("/foo/{hex:[0-9a-fA-F]{2}}");
+ Map<String, String> map = new HashMap<String, String>();
+ map.put("hex", "FF");
+ assertEquals("Wrong substitution", "/foo/FF", ut.substitute(map));
+ }
+
+ @Test
+ public void testEncodeLiteralCharacters() {
+ URITemplate ut = new URITemplate("a {id} b");
+ assertEquals("a%20{id}%20b", ut.encodeLiteralCharacters());
+ }
+
+ @Test
+ public void testEncodeLiteralCharactersNotVariable() {
+ URITemplate ut = new URITemplate("a {digit:[0-9]} b");
+ System.out.println(ut.encodeLiteralCharacters());
+ assertEquals("a%20{digit:[0-9]}%20b", ut.encodeLiteralCharacters());
+ }
}