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