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