From 86497cb52a2db2bc2eabe66ccb752fd15d3c6d86 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Sun, 22 Feb 2009 14:54:09 +0000 Subject: [PATCH] Added UriTemplate class git-svn-id: https://src.springframework.org/svn/spring-framework/trunk@669 50f2f4bb-b051-0410-bef5-90022cba6387 --- .../springframework/web/util/UriTemplate.java | 242 ++++++++++++++++++ .../web/util/UriTemplateTests.java | 114 +++++++++ 2 files changed, 356 insertions(+) create mode 100644 org.springframework.web/src/main/java/org/springframework/web/util/UriTemplate.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/util/UriTemplateTests.java diff --git a/org.springframework.web/src/main/java/org/springframework/web/util/UriTemplate.java b/org.springframework.web/src/main/java/org/springframework/web/util/UriTemplate.java new file mode 100644 index 00000000000..1e280a993e8 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/util/UriTemplate.java @@ -0,0 +1,242 @@ +/* + * Copyright 2002-2009 the original author or authors. + * + * Licensed 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.springframework.web.util; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.util.Assert; + +/** + * Represents a URI template. An URI template is a URI-like string that contained variables marked of in braces + * ({, }), which can be expanded to produce a URI. + *

+ * See {@link #expand(Map)}, {@link #expand(String[])}, and {@link #match(String)} for example usages. + * + * @author Arjen Poutsma + * @see URI Templates + */ +public final class UriTemplate { + + /** + * Captures URI template variable names. + */ + private static final Pattern NAMES_PATTERN = Pattern.compile("\\{([^/]+?)\\}"); + + /** + * Replaces template variables in the URI template. + */ + private static final String VALUE_REGEX = "(.*)"; + + private final List variableNames; + + private final Pattern matchPattern; + + private final String uriTemplate; + + /** + * Constructs a new {@link UriTemplate} with the given string. + * + * @param uriTemplate the uri template string + */ + public UriTemplate(String uriTemplate) { + Parser parser = new Parser(uriTemplate); + this.uriTemplate = uriTemplate; + this.variableNames = parser.getVariableNames(); + this.matchPattern = parser.getMatchPattern(); + } + + /** + * Returns the names of the variables in the template, in order. + * + * @return the template variable names + */ + public List getVariableNames() { + return variableNames; + } + + /** + * Given the map of variables, expands this template into a URI string. The map keys represent variable names, the + * map values variable values. The order of variables is not significant. + *

+ * Example: + *

+	 * UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}");
+	 * Map<String, String> uriVariables = new HashMap<String, String>();
+	 * uriVariables.put("booking", "42");
+	 * uriVariables.put("hotel", "1");
+	 * System.out.println(template.expand(uriVariables));
+	 * 
+ * will print:
http://example.com/hotels/1/bookings/42
+ * + * @param uriVariables the map of uri variables + * @return the expanded uri + * @throws IllegalArgumentException if uriVariables is null; or if it does not contain + * values for all the variable names + */ + public URI expand(Map uriVariables) { + Assert.notNull(uriVariables, "'uriVariables' must not be null"); + String[] values = new String[variableNames.size()]; + for (int i = 0; i < variableNames.size(); i++) { + String name = variableNames.get(i); + Assert.isTrue(uriVariables.containsKey(name), "'uriVariables' has no value for [" + name + "]"); + values[i] = uriVariables.get(name); + } + return expand(values); + } + + /** + * Given an array of variables, expands this template into a URI string. The array represent variable values. The + * order of variables is significant. + *

+ * Example: + *

+	 * UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}");
+	 * System.out.println(template.expand("1", "42));
+	 * 
+ * will print:
http://example.com/hotels/1/bookings/42
+ * + * @param uriVariableValues the array of uri variables + * @return the expanded uri + * @throws IllegalArgumentException if uriVariables is null; or if it does not contain + * sufficient variables + */ + public URI expand(String... uriVariableValues) { + Assert.notNull(uriVariableValues, "'uriVariableValues' must not be null"); + if (uriVariableValues.length != variableNames.size()) { + throw new IllegalArgumentException( + "Invalid amount of variables values in [" + uriTemplate + "]: expected " + variableNames.size() + + "; got " + uriVariableValues.length); + } + Matcher m = NAMES_PATTERN.matcher(uriTemplate); + StringBuffer buffer = new StringBuffer(); + int i = 0; + while (m.find()) { + String uriVariable = uriVariableValues[i++]; + m.appendReplacement(buffer, uriVariable); + } + m.appendTail(buffer); + return URI.create(buffer.toString()); + } + + /** + * Indicates whether the given URI matches this template. + * + * @param uri the URI to match to + * @return true if it matches; false otherwise + */ + public boolean matches(String uri) { + if (uri == null) { + return false; + } + Matcher m = matchPattern.matcher(uri); + return m.matches(); + } + + /** + * Matches the given URI to a map of variable values. Keys in the returned map are variable names, values are + * variable values, as occurred in the given URI. + *

+ * Example: + *

+	 * UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}");
+	 * System.out.println(template.match("http://example.com/hotels/1/bookings/42"));
+	 * 
+ * will print:
{hotel=1, booking=42}
+ * + * @param uri the URI to match to + * @return a map of variable values + */ + public Map match(String uri) { + Assert.notNull(uri, "'uri' must not be null"); + Map result = new LinkedHashMap(variableNames.size()); + Matcher m = matchPattern.matcher(uri); + if (m.find()) { + for (int i = 1; i <= m.groupCount(); i++) { + String name = variableNames.get(i - 1); + String value = m.group(i); + result.put(name, value); + } + } + return result; + } + + @Override + public String toString() { + return uriTemplate; + } + + /** + * Static inner class to parse uri template strings into a matching regular expression. + */ + private static class Parser { + + private List variableNames = new LinkedList(); + + private StringBuilder patternBuilder = new StringBuilder(); + + private Parser(String uriTemplate) { + Assert.hasText(uriTemplate, "'uriTemplate' must not be null"); + Matcher m = NAMES_PATTERN.matcher(uriTemplate); + int end = 0; + while (m.find()) { + patternBuilder.append(encodeAndQuote(uriTemplate, end, m.start())); + patternBuilder.append(VALUE_REGEX); + variableNames.add(m.group(1)); + end = m.end(); + } + patternBuilder.append(encodeAndQuote(uriTemplate, end, uriTemplate.length())); + + int lastIdx = patternBuilder.length() - 1; + if (lastIdx >= 0 && patternBuilder.charAt(lastIdx) == '/') { + patternBuilder.deleteCharAt(lastIdx); + } + } + + private String encodeAndQuote(String fullPath, int start, int end) { + if (start == end) { + return ""; + } + String result = fullPath.substring(start, end); + try { + URI uri = new URI(null, null, result, null); + result = uri.toASCIIString(); + } + catch (URISyntaxException e) { + throw new IllegalArgumentException("Could not create URI from [" + fullPath + "]"); + } + return Pattern.quote(result); + } + + private List getVariableNames() { + return Collections.unmodifiableList(variableNames); + } + + private Pattern getMatchPattern() { + return Pattern.compile(patternBuilder.toString()); + } + + } + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/util/UriTemplateTests.java b/org.springframework.web/src/test/java/org/springframework/web/util/UriTemplateTests.java new file mode 100644 index 00000000000..a8bae28e419 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/util/UriTemplateTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2009 the original author or authors. + * + * Licensed 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.springframework.web.util; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; + +/** + * @author Arjen Poutsma + */ +public class UriTemplateTests { + + private UriTemplate template; + + @Before + public void create() { + template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); + } + + @Test + public void getVariableNames() throws Exception { + List variableNames = template.getVariableNames(); + assertEquals("Invalid variable names", Arrays.asList("hotel", "booking"), variableNames); + } + + @Test + public void expandVarArgs() throws Exception { + URI result = template.expand("1", "42"); + assertEquals("Invalid expanded template", new URI("http://example.com/hotels/1/bookings/42"), result); + } + + @Test(expected = IllegalArgumentException.class) + public void expandVarArgsInvalidAmountVariables() throws Exception { + template.expand("1", "42", "100"); + } + + @Test + public void expandMapDuplicateVariables() throws Exception { + template = new UriTemplate("/order/{c}/{c}/{c}"); + assertEquals("Invalid variable names", Arrays.asList("c", "c", "c"), template.getVariableNames()); + URI result = template.expand(Collections.singletonMap("c", "cheeseburger")); + assertEquals("Invalid expanded template", new URI("/order/cheeseburger/cheeseburger/cheeseburger"), result); + } + + @Test + public void expandMap() throws Exception { + Map uriVariables = new HashMap(2); + uriVariables.put("booking", "42"); + uriVariables.put("hotel", "1"); + URI result = template.expand(uriVariables); + assertEquals("Invalid expanded template", new URI("http://example.com/hotels/1/bookings/42"), result); + } + + @Test(expected = IllegalArgumentException.class) + public void expandMapInvalidAmountVariables() throws Exception { + template.expand(Collections.singletonMap("hotel", "1")); + } + + @Test(expected = IllegalArgumentException.class) + public void expandMapUnboundVariables() throws Exception { + Map uriVariables = new HashMap(2); + uriVariables.put("booking", "42"); + uriVariables.put("bar", "1"); + template.expand(uriVariables); + } + + @Test + public void matches() throws Exception { + assertTrue("UriTemplate does not match", template.matches("http://example.com/hotels/1/bookings/42")); + assertFalse("UriTemplate matches", template.matches("http://example.com/hotels/bookings")); + assertFalse("UriTemplate matches", template.matches("")); + assertFalse("UriTemplate matches", template.matches(null)); + } + + @Test + public void match() throws Exception { + Map expected = new HashMap(2); + expected.put("booking", "42"); + expected.put("hotel", "1"); + + Map result = template.match("http://example.com/hotels/1/bookings/42"); + assertEquals("Invalid match", expected, result); + } + + @Test + public void matchDuplicate() throws Exception { + template = new UriTemplate("/order/{c}/{c}/{c}"); + Map result = template.match("/order/cheeseburger/cheeseburger/cheeseburger"); + Map expected = Collections.singletonMap("c", "cheeseburger"); + assertEquals("Invalid match", expected, result); + } +} \ No newline at end of file