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());
+    }
 }