001/* 002 * Copyright 2002-2019 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.util; 018 019import java.io.Serializable; 020import java.net.URI; 021import java.util.ArrayList; 022import java.util.Collections; 023import java.util.LinkedHashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.regex.Matcher; 027import java.util.regex.Pattern; 028 029import org.springframework.util.Assert; 030 031/** 032 * Representation of a URI template that can be expanded with URI variables via 033 * {@link #expand(Map)}, {@link #expand(Object[])}, or matched to a URL via 034 * {@link #match(String)}. This class is designed to be thread-safe and 035 * reusable, and allows any number of expand or match calls. 036 * 037 * <p><strong>Note:</strong> this class uses {@link UriComponentsBuilder} 038 * internally to expand URI templates, and is merely a shortcut for already 039 * prepared URI templates. For more dynamic preparation and extra flexibility, 040 * e.g. around URI encoding, consider using {@code UriComponentsBuilder} or the 041 * higher level {@link DefaultUriBuilderFactory} which adds several encoding 042 * modes on top of {@code UriComponentsBuilder}. See the 043 * <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-uri-building">reference docs</a> 044 * for further details. 045 * 046 * @author Arjen Poutsma 047 * @author Juergen Hoeller 048 * @author Rossen Stoyanchev 049 * @since 3.0 050 */ 051@SuppressWarnings("serial") 052public class UriTemplate implements Serializable { 053 054 private final String uriTemplate; 055 056 private final UriComponents uriComponents; 057 058 private final List<String> variableNames; 059 060 private final Pattern matchPattern; 061 062 063 /** 064 * Construct a new {@code UriTemplate} with the given URI String. 065 * @param uriTemplate the URI template string 066 */ 067 public UriTemplate(String uriTemplate) { 068 Assert.hasText(uriTemplate, "'uriTemplate' must not be null"); 069 this.uriTemplate = uriTemplate; 070 this.uriComponents = UriComponentsBuilder.fromUriString(uriTemplate).build(); 071 072 TemplateInfo info = TemplateInfo.parse(uriTemplate); 073 this.variableNames = Collections.unmodifiableList(info.getVariableNames()); 074 this.matchPattern = info.getMatchPattern(); 075 } 076 077 078 /** 079 * Return the names of the variables in the template, in order. 080 * @return the template variable names 081 */ 082 public List<String> getVariableNames() { 083 return this.variableNames; 084 } 085 086 /** 087 * Given the Map of variables, expands this template into a URI. The Map keys represent variable names, 088 * the Map values variable values. The order of variables is not significant. 089 * <p>Example: 090 * <pre class="code"> 091 * UriTemplate template = new UriTemplate("https://example.com/hotels/{hotel}/bookings/{booking}"); 092 * Map<String, String> uriVariables = new HashMap<String, String>(); 093 * uriVariables.put("booking", "42"); 094 * uriVariables.put("hotel", "Rest & Relax"); 095 * System.out.println(template.expand(uriVariables)); 096 * </pre> 097 * will print: <blockquote>{@code https://example.com/hotels/Rest%20%26%20Relax/bookings/42}</blockquote> 098 * @param uriVariables the map of URI variables 099 * @return the expanded URI 100 * @throws IllegalArgumentException if {@code uriVariables} is {@code null}; 101 * or if it does not contain values for all the variable names 102 */ 103 public URI expand(Map<String, ?> uriVariables) { 104 UriComponents expandedComponents = this.uriComponents.expand(uriVariables); 105 UriComponents encodedComponents = expandedComponents.encode(); 106 return encodedComponents.toUri(); 107 } 108 109 /** 110 * Given an array of variables, expand this template into a full URI. The array represent variable values. 111 * The order of variables is significant. 112 * <p>Example: 113 * <pre class="code"> 114 * UriTemplate template = new UriTemplate("https://example.com/hotels/{hotel}/bookings/{booking}"); 115 * System.out.println(template.expand("Rest & Relax", 42)); 116 * </pre> 117 * will print: <blockquote>{@code https://example.com/hotels/Rest%20%26%20Relax/bookings/42}</blockquote> 118 * @param uriVariableValues the array of URI variables 119 * @return the expanded URI 120 * @throws IllegalArgumentException if {@code uriVariables} is {@code null} 121 * or if it does not contain sufficient variables 122 */ 123 public URI expand(Object... uriVariableValues) { 124 UriComponents expandedComponents = this.uriComponents.expand(uriVariableValues); 125 UriComponents encodedComponents = expandedComponents.encode(); 126 return encodedComponents.toUri(); 127 } 128 129 /** 130 * Indicate whether the given URI matches this template. 131 * @param uri the URI to match to 132 * @return {@code true} if it matches; {@code false} otherwise 133 */ 134 public boolean matches(String uri) { 135 if (uri == null) { 136 return false; 137 } 138 Matcher matcher = this.matchPattern.matcher(uri); 139 return matcher.matches(); 140 } 141 142 /** 143 * Match the given URI to a map of variable values. Keys in the returned map are variable names, 144 * values are variable values, as occurred in the given URI. 145 * <p>Example: 146 * <pre class="code"> 147 * UriTemplate template = new UriTemplate("https://example.com/hotels/{hotel}/bookings/{booking}"); 148 * System.out.println(template.match("https://example.com/hotels/1/bookings/42")); 149 * </pre> 150 * will print: <blockquote>{@code {hotel=1, booking=42}}</blockquote> 151 * @param uri the URI to match to 152 * @return a map of variable values 153 */ 154 public Map<String, String> match(String uri) { 155 Assert.notNull(uri, "'uri' must not be null"); 156 Map<String, String> result = new LinkedHashMap<String, String>(this.variableNames.size()); 157 Matcher matcher = this.matchPattern.matcher(uri); 158 if (matcher.find()) { 159 for (int i = 1; i <= matcher.groupCount(); i++) { 160 String name = this.variableNames.get(i - 1); 161 String value = matcher.group(i); 162 result.put(name, value); 163 } 164 } 165 return result; 166 } 167 168 @Override 169 public String toString() { 170 return this.uriTemplate; 171 } 172 173 174 /** 175 * Helper to extract variable names and regex for matching to actual URLs. 176 */ 177 private static class TemplateInfo { 178 179 private final List<String> variableNames; 180 181 private final Pattern pattern; 182 183 private TemplateInfo(List<String> vars, Pattern pattern) { 184 this.variableNames = vars; 185 this.pattern = pattern; 186 } 187 188 public List<String> getVariableNames() { 189 return this.variableNames; 190 } 191 192 public Pattern getMatchPattern() { 193 return this.pattern; 194 } 195 196 public static TemplateInfo parse(String uriTemplate) { 197 int level = 0; 198 List<String> variableNames = new ArrayList<String>(); 199 StringBuilder pattern = new StringBuilder(); 200 StringBuilder builder = new StringBuilder(); 201 for (int i = 0 ; i < uriTemplate.length(); i++) { 202 char c = uriTemplate.charAt(i); 203 if (c == '{') { 204 level++; 205 if (level == 1) { 206 // start of URI variable 207 pattern.append(quote(builder)); 208 builder = new StringBuilder(); 209 continue; 210 } 211 } 212 else if (c == '}') { 213 level--; 214 if (level == 0) { 215 // end of URI variable 216 String variable = builder.toString(); 217 int idx = variable.indexOf(':'); 218 if (idx == -1) { 219 pattern.append("(.*)"); 220 variableNames.add(variable); 221 } 222 else { 223 if (idx + 1 == variable.length()) { 224 throw new IllegalArgumentException( 225 "No custom regular expression specified after ':' in \"" + variable + "\""); 226 } 227 String regex = variable.substring(idx + 1); 228 pattern.append('('); 229 pattern.append(regex); 230 pattern.append(')'); 231 variableNames.add(variable.substring(0, idx)); 232 } 233 builder = new StringBuilder(); 234 continue; 235 } 236 } 237 builder.append(c); 238 } 239 if (builder.length() > 0) { 240 pattern.append(quote(builder)); 241 } 242 return new TemplateInfo(variableNames, Pattern.compile(pattern.toString())); 243 } 244 245 private static String quote(StringBuilder builder) { 246 return (builder.length() > 0 ? Pattern.quote(builder.toString()) : ""); 247 } 248 } 249 250}