Added: ode/branches/APACHE_ODE_1.1/utils/src/main/java/org/apache/ode/utils/URITemplate.java URL: http://svn.apache.org/viewvc/ode/branches/APACHE_ODE_1.1/utils/src/main/java/org/apache/ode/utils/URITemplate.java?rev=658934&view=auto ============================================================================== --- ode/branches/APACHE_ODE_1.1/utils/src/main/java/org/apache/ode/utils/URITemplate.java (added) +++ ode/branches/APACHE_ODE_1.1/utils/src/main/java/org/apache/ode/utils/URITemplate.java Wed May 21 16:40:35 2008 @@ -0,0 +1,323 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ode.utils; + +import org.apache.commons.httpclient.URIException; +import org.apache.commons.httpclient.util.URIUtil; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A partial implementation of URI Template expansion + * as specified by the <a href="http://bitworking.org/projects/URI-Templates/spec/draft-gregorio-uritemplate-03.html">URI template specification</a>. + * <p/><strong>Limitations</strong> + * <br/>The only operation implemented so far is <a href="http://bitworking.org/projects/URI-Templates/spec/draft-gregorio-uritemplate-03.html#var">Var substitution</a>. If an expansion template for another operation (join, neg, opt, etc) is found, + * an [EMAIL PROTECTED] UnsupportedOperationException} is thrown. + * <p/> + * <p/> + * <p/><strong>Escaping Considerations</strong> + * <br/>Replacement and default values are escaped. All characters except unreserved (as defined by <a href="http://tools.ietf.org/html/rfc2396#appendix-A">rfc2396</a>) are escaped. + * <br/> unreserved = alphanum | mark + * <br/> mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")" + * <p/> + * <a href="http://tools.ietf.org/html/rfc2396">Rfc2396</a> is used to be compliant with [EMAIL PROTECTED] java.net.URI java.net.URI}. + * <p/> + * <p/><strong>Examples:</strong> + * <br/> + * Given the following template variable names and values: + * <ul> + * <li>foo = tag</li> + * <li>bar = java</li> + * <li>name = null</li> + * <li>date = 2008/05/09</li> + * </ul> + * <p/>The following URI Templates will be expanded as shown: + * <br/>http://example.com/{foo}/{bar}.{format=xml} + * <br/>http://example.com/tag/java.xml + * <br/> + * <br/>http://example.com/tag/java.{format} + * <br/>http://example.com/tag/java. + * <br/> + * <br/>http://example.com/{foo}/{name} + * <br/>http://example.com/tag/ + * <br/> + * <br/>http://example.com/{foo}/{name=james} + * <br/>http://example.com/tag/james + * <br/> + * <br/>http://example.org/{date} + * <br/>http://example.org/2008%2F05%2F09 + * <br/> + * <br/>http://example.org/{-join|&|foo,bar,xyzzy,baz}/{date} + * <br/>--> UnsupportedOperationException + * + * @author <a href="mailto:[EMAIL PROTECTED]">Alexis Midon</a> + * @see #varSubstitution(String, Object[], java.util.Map) + */ + +public class URITemplate { + + private static final Log log = LogFactory.getLog(URITemplate.class); + + + public static final String EXPANSION_REGEX = "\\{[^\\}]+\\}"; + // compiled pattern of the regex + private static final Pattern PATTERN = Pattern.compile(EXPANSION_REGEX); + + /** + * Implements the function describes in <a href="http://bitworking.org/projects/URI-Templates/spec/draft-gregorio-uritemplate-03.html#appendix_a">the spec</a> + * + * @param expansion, an expansion template (with the surrounding braces) + * @return an array of object containing the operation name, the operation argument, a map of <var, default value (null if none)> + */ + public static Object[] parseExpansion(String expansion) { + // remove surrounding braces if any + if (expansion.matches(EXPANSION_REGEX)) { + expansion = expansion.substring(1, expansion.length() - 1); + } + String[] r; + if (expansion.contains("|")) { + // (op, arg, vars) + r = expansion.split("\\|", -1); + // remove the leading '-' of the operation + r[0] = r[0].substring(1); + } else { + r = new String[]{null, null, expansion}; + } + + // parse the vars + Map vars = new HashMap(); + String[] var = r[2].split(","); + for (String s : var) { + if (s.contains("=")) { + String[] a = s.split("="); + vars.put(a[0], a[1]); + } else { + vars.put(s, null); + } + } + // op, arg, vars + return new Object[]{r[0], r[1], vars}; + } + + /** + * Simply build a map from nameValuePairs and pass it to [EMAIL PROTECTED] #expand(String, java.util.Map)} + * + * @param nameValuePairs an array containing of name, value, name, value, and so on. Null values are allowed. + * @see # expand (String, java.util.Map) + */ + public static String expand(String uriTemplate, String... nameValuePairs) throws URIException, UnsupportedOperationException { + return expand(uriTemplate, toMap(nameValuePairs)); + } + + /** + * A partial implementation of URI Template expansion + * as specified by the <a href="http://bitworking.org/projects/URI-Templates/spec/draft-gregorio-uritemplate-03.html">URI template specification</a>. + * <p/> + * The only operation implemented as of today is "Var Substitution". If an expansion template for another operation (join, neg, opt, etc) is found, + * an [EMAIL PROTECTED] UnsupportedOperationException} will be thrown. + * <p/> + * See [EMAIL PROTECTED] #varSubstitution(String, Object[], java.util.Map)} + * + * @param uriTemplate the URI template + * @param nameValuePairs a Map of <name, value>. Null values are allowed. + * @return a copy of uri template in which substitutions have been made (if possible) + * @throws URIException if the default protocol charset is not supported + * @throws UnsupportedOperationException if the operation is not supported. Currently only var substitution is supported. + * @see #varSubstitution(String, Object[], java.util.Map) + */ + public static String expand(String uriTemplate, Map<String, String> nameValuePairs) throws URIException, UnsupportedOperationException { + return expand(uriTemplate, nameValuePairs, false); + } + + /** + * Same as [EMAIL PROTECTED] #expand(String, java.util.Map)} but preserve an expansion template if the corresponding variable + * is not defined in the [EMAIL PROTECTED] nameValuePairs} map (i.e. map.contains(var)==false). + * <br/>Meaning that a template may be returned. + * <br/> If a default value exists for the undefined value, it will be used to replace the expansion pattern. + * <p/> + * <strong>Beware that this behavior deviates from the URI Template specification.</strong> + * <p/> + * For instance: + * <br/>Given the following template variable names and values: + * <ul> + * <li>bar = java</li> + * <li>foo undefined + * </ul> + * <p/>The following expansion templates will be expanded as shown if [EMAIL PROTECTED] preserveUndefinedVar} is true: + * <br/>http://example.com/{bar} + * <br/>http://example.com/java + * <br/> + * <br/>{foo=a_default_value} + * <br/>a_default_value + * <br/> + * <br/>http://example.com/{bar}/{foo} + * <br/>http://example.com/java/{foo} + * + * @see #expand(String, java.util.Map) + */ + public static String expandLazily(String uriTemplate, Map<String, String> nameValuePairs) throws URIException, UnsupportedOperationException { + return expand(uriTemplate, nameValuePairs, true); + } + + /** + * @see #expandLazily(String, java.util.Map) + */ + public static String expandLazily(String uriTemplate, String... nameValuePairs) throws URIException { + return expandLazily(uriTemplate, toMap(nameValuePairs)); + } + + + /** + * @see #varSubstitution(String, Object[], java.util.Map, boolean) + * @see #expandLazily(String, String[]) + */ + private static String expand(String uriTemplate, Map<String, String> nameValuePairs, boolean preserveUndefinedVar) throws URIException, UnsupportedOperationException { + Matcher m = PATTERN.matcher(uriTemplate); + // Strings are immutable in java + // so let's use a buffer, and append all substrings between 2 matches and the replacement value for each match + StringBuilder sb = new StringBuilder(uriTemplate.length()); + int prevEnd = 0; + while (m.find()) { + // append the string between two matches + sb.append(uriTemplate.substring(prevEnd, m.start())); + prevEnd = m.end(); + + // expansion pattern with braces + String expansionPattern = uriTemplate.substring(m.start(), m.end()); + Object[] expansionInfo = parseExpansion(expansionPattern); + String operationName = (String) expansionInfo[0]; + // here we have to know which operation apply + if (operationName != null) { + final String msg = "Operation not supported [" + operationName + "]. This expansion pattern [" + expansionPattern + "] is not valid."; + if (log.isWarnEnabled()) log.warn(msg); + throw new UnsupportedOperationException(msg); + } else { + // here we care only for var substitution, i.e expansion patterns with no operation name + sb.append(varSubstitution(expansionPattern, expansionInfo, nameValuePairs, preserveUndefinedVar)); + } + + } + if (sb.length() == 0) { + // return the template itself if no match (String are immutable in java, no need to clone the template) + return uriTemplate; + } else { + // don't forget the remaining part + sb.append(uriTemplate.substring(prevEnd, uriTemplate.length())); + return sb.toString(); + } + } + + /** + * An implementation of var substitution as defined by the + * <a href="http://bitworking.org/projects/URI-Templates/spec/draft-gregorio-uritemplate-03.html#var">URI template specification</a>. + * <p/> + * If for a given variable, the variable is in the name/value map but the associated value is null. The variable will be replaced with an empty string or with the default value if any. + * + * @param expansionPattern an expansion pattern (not a uri template) e.g. "{foo}" + * @param expansionInfo the result of [EMAIL PROTECTED] #parseExpansion(String)} for the given expansion pattern + * @param nameValuePairs the Map<String, String> of names and associated values. May containt null values. + * @return the expanded string, properly escaped. + * @throws URIException if an encoding exception occured + * @see org.apache.commons.httpclient.util.URIUtil#encodeWithinQuery(String) + * @see java.net.URI + */ + public static String varSubstitution(String expansionPattern, Object[] expansionInfo, Map<String, String> nameValuePairs) throws URIException { + return varSubstitution(expansionPattern, expansionInfo, nameValuePairs, false); + } + + /** + * Same as [EMAIL PROTECTED] #varSubstitution(String, Object[], java.util.Map)} but the [EMAIL PROTECTED] preserveUndefinedVar} boolean + * argument (if [EMAIL PROTECTED] true}) allows to preserve an expansion template if the corresponding variable is not defined in the [EMAIL PROTECTED] nameValuePairs} map (i.e. map.contains(var)==false). + * <br/> If a default value exists for the undefined value, it will be used to replace the expansion pattern. + * <p/> + * <strong>Beware that this behavior deviates from the URI Template specification.</strong> + * <p/> + * For instance: + * <br/>Given the following template variable names and values: + * <ul> + * <li>bar = java</li> + * <li>foo undefined + * </ul> + * <p/>The following expansion templates will be expanded as shown if [EMAIL PROTECTED] preserveUndefinedVar} is true: + * <br/>{bar} + * <br/>java + * <br/> + * <br/>{foo=a_default_value} + * <br/>a_default_value + * <br/> + * <br/>{foo} + * <br/>{foo} + */ + public static String varSubstitution(String expansionPattern, Object[] expansionInfo, Map<String, String> nameValuePairs, boolean preserveUndefinedVar) throws URIException { + Map vars = (Map) expansionInfo[2]; + // only one var per pattern + Map.Entry e = (Map.Entry) vars.entrySet().iterator().next(); + String var = (String) e.getKey(); + String defaultValue = (String) e.getValue(); + boolean hasDefaultValue = defaultValue != null; + // this boolean indicates if the var is mentionned in the map, not that the associated value is not null. + boolean varDefined = nameValuePairs.containsKey(var); + String providedValue = nameValuePairs.get(var); + String res; + boolean escapingNeeded = true; + if (varDefined) { + if (providedValue == null && !hasDefaultValue) { + res = ""; + } else { + res = providedValue != null ? providedValue : defaultValue; + } + } else { + // If the variable is undefined and no default value is given then substitute with the empty string, + // except if preserveUndefinedVar is true + + if (hasDefaultValue) { + res = defaultValue; + } else { + if (preserveUndefinedVar) { + res = expansionPattern; + escapingNeeded = false; + } else { + res = ""; + } + } + } + // We assume that the replacement value is for the query part of the URI. + // Actually the query allows less character than the path part. $%&+,:@ + // (acording to RFC2396 + return escapingNeeded ? URIUtil.encodeWithinQuery(res) : res; + } + + + private static Map<String, String> toMap(String... nameValuePairs) { + if (nameValuePairs.length % 2 != 0) { + throw new IllegalArgumentException("An even number of elements is expected."); + } + Map<String, String> m = new HashMap<String, String>(); + for (int i = 0; i < nameValuePairs.length; i = i + 2) { + m.put(nameValuePairs[i], nameValuePairs[i + 1]); + } + return m; + } +}
Added: ode/branches/APACHE_ODE_1.1/utils/src/test/java/org/apache/ode/utils/URITemplateTest.java URL: http://svn.apache.org/viewvc/ode/branches/APACHE_ODE_1.1/utils/src/test/java/org/apache/ode/utils/URITemplateTest.java?rev=658934&view=auto ============================================================================== --- ode/branches/APACHE_ODE_1.1/utils/src/test/java/org/apache/ode/utils/URITemplateTest.java (added) +++ ode/branches/APACHE_ODE_1.1/utils/src/test/java/org/apache/ode/utils/URITemplateTest.java Wed May 21 16:40:35 2008 @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ode.utils; + +import junit.framework.TestCase; + +import java.util.*; + +import org.apache.commons.httpclient.URIException; + +/** + * @author <a href="mailto:[EMAIL PROTECTED]">Alexis Midon</a> + */ +public class URITemplateTest extends TestCase { + private static final String EXCEPTION_EXPECTED = "ExceptionExpected"; + + protected void setUp() throws Exception { + super.setUp(); + } + + public void testParseExpansion(){ + Object[] expansionPatterns = new Object[]{ + "{my_var}", new Object[]{null, null, new HashMap(){{put("my_var",null);}}}, + "{my_var=my_default}", new Object[]{null, null, new HashMap(){{put("my_var","my_default");}}}, + "{-suffix|/|foo}", new Object[]{"suffix", "/", new HashMap(){{put("foo",null);}}}, + "{-opt|[EMAIL PROTECTED]|foo}", new Object[]{"opt", "[EMAIL PROTECTED]", new HashMap(){{put("foo",null);}}} + }; + + for (int i = 0; i < expansionPatterns.length; i=i+2) { + String patternInfo = (String) expansionPatterns[i]; + Object[] computedResult = URITemplate.parseExpansion(patternInfo); + Object[] expectedResult = (Object[]) expansionPatterns[i + 1]; + assertEquals("Unexpected operation", expectedResult[0], computedResult[0]); + assertEquals("Unexpected argument", expectedResult[1], computedResult[1]); + Map expectedVarMap = (Map) expectedResult[2]; + Map computedVarMap = (Map) computedResult[2]; + assertEquals("Var map do not have the number of elements", expectedVarMap.size(), computedVarMap.size()); + for (Iterator it = expectedVarMap.entrySet().iterator(); it.hasNext();) { + Map.Entry e = (Map.Entry) it.next(); + assertEquals("Different Value!", e.getValue(), computedVarMap.get(e.getKey())); + } + } + } + + + public void testExpand() throws Exception { + // template, input name/value array, expected result + Object[] templates = new Object[]{ + "{a}", new String[]{"a", "hello"}, "hello" + ,"{a}", new String[]{"var_not_in_template", "hello"}, "{a}" + ,"{a=3}", new String[]{"var_not_in_template", "hello"}, "3" // with a default value + ,"hello {name}!", new String[]{"name", null}, "hello !" // null value + ,"hello {name=darling}!", new String[]{"name", null}, "hello darling!" // null value and a default + ,"hello {name=darling}!", new String[]{"name", "brother"}, "hello brother!" + ,"hello {name=darling}! what's {this}?", new String[]{"name", "brother", "this", "this"}, "hello brother! what's this?" + ,"hello {name=darling}! what's {this}?", new String[]{"name", "brother", "this", "wrong"}, "hello brother! what's wrong?" + ,"hello {name=darling}! what's {this}?", new String[]{"name", "brother", "this", null}, "hello brother! what's ?" + ,"hello {name=darling}! what's {this=up}?", new String[]{"name", "brother", "this", null}, "hello brother! what's up?" + ,"hello {name=darling}! what's {this=up}?", new String[]{"name", "brother"}, "hello brother! what's up?" + ,"hello {name}! what's {this}?", new String[]{"var_not_in_template", "foo"}, "hello ! what's ?" + ,"hello{name}what's{this}", new String[]{"name", " brother! ", "this", " this?"}, "hello%20brother!%20what's%20this%3F" // test encoding + ,"hello{name= brother! }what's{this}", new String[]{"this", " this?"}, "hello%20brother!%20what's%20this%3F" // test encoding + default value + ,"hello {name=darling}! what's {this}?", new String[]{"name", "brother", "this", "{wrong}"}, "hello brother! what's %7Bwrong%7D?" + ,"hello%20brother!%20what's{this}", new String[]{"this", " this?"}, "hello%20brother!%20what's%20this%3F" // test template of template + ,"{this}", new String[]{"this", ";/?:@&=+,$"}, "%3B%2F%3F%3A%40%26%3D%2B%2C%24" // reserved characters within a query + ,"{this}", new String[]{"this", "somereserved%;/?:@&=+,$allunreserved-_.!~*'()"}, "somereserved%25%3B%2F%3F%3A%40%26%3D%2B%2C%24allunreserved-_.!~*'()" // reserved characters within a query + // the followings are included in the javadoc as examples + , "http://example.com/{foo}/{bar}.{format=xml}", new String[]{"foo", "tag", "bar", "java", "name", null, "date", "2008/05/09"},"http://example.com/tag/java.xml" // undefined var with a default value + , "http://example.com/tag/java.{format}", new String[]{"foo", "tag", "bar", "java", "name", null, "date", "2008/05/09"},"http://example.com/tag/java." // undefined and no default + , "http://example.com/{foo}/{name}", new String[]{"foo", "tag", "bar", "java", "name", null, "date", "2008/05/09"},"http://example.com/tag/" + , "http://example.com/{foo}/{name=james}", new String[]{"foo", "tag", "bar", "java", "name", null, "date", "2008/05/09"},"http://example.com/tag/james" + , "http://example.org/{date}", new String[]{"foo", "tag", "bar", "java", "name", null, "date", "2008/05/09"}, "http://example.org/2008%2F05%2F09" + , "http://example.org/{-join|&|foo,bar,xyzzy,baz}/{date}", new String[]{"foo", "tag", "bar", "java", "name", null, "date", "2008/05/09"}, EXCEPTION_EXPECTED + }; + for (int i = 0; i < templates.length; i = i + 3) { + String template = (String) templates[i]; + String[] pairs = (String[]) templates[i + 1]; + String expected = (String) templates[i + 2]; + String computed = null; + try { + computed = URITemplate.expand(template, pairs); + if (EXCEPTION_EXPECTED.equals(expected)) { + fail("An exception was supposed to be thrown!"); + } else { + assertEquals("Test #" + ((i / 3) + 1) + ": Result does not match expectation.", expected, computed); + } + } catch (Exception e) { + if (!EXCEPTION_EXPECTED.equals(expected)) { + // this exception was NOT expected! + throw e; + } + } + } + } + + public void testExpandLazily() throws Exception { + // same but with some undefined vars + // template, input name/value array, expected result + Object[] templates = new Object[]{ + "http://example.com/{foo}/{bar}.{format}", new String[]{"foo", "tag", "bar", "java", "name", null, "date", "2008/05/09"},"http://example.com/tag/java.{format}" // undefined var with no default value + , "http://example.com/{foo}/{name}", new String[]{"bar", "java", "date", "2008/05/09"},"http://example.com/{foo}/{name}" + , "http://example.com/{foo}/{name=james}", new String[]{"bar", "java", "name", null, "date", "2008/05/09"},"http://example.com/{foo}/james" + }; + for (int i = 0; i < templates.length; i = i + 3) { + String template = (String) templates[i]; + String[] pairs = (String[]) templates[i + 1]; + String expected = (String) templates[i + 2]; + String computed = null; + try { + computed = URITemplate.expandLazily(template, pairs); + if (EXCEPTION_EXPECTED.equals(expected)) { + fail("An exception was supposed to be thrown!"); + } else { + assertEquals("Test #" + ((i / 3) + 1) + ": Result does not match expectation.", expected, computed); + } + } catch (Exception e) { + if (!EXCEPTION_EXPECTED.equals(expected)) { + // this exception was NOT expected! + throw e; + } + } + } + } +}
