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.nio.charset.Charset; 022import java.nio.charset.StandardCharsets; 023import java.util.Arrays; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Map; 027import java.util.function.UnaryOperator; 028import java.util.regex.Matcher; 029import java.util.regex.Pattern; 030 031import org.springframework.lang.Nullable; 032import org.springframework.util.Assert; 033import org.springframework.util.MultiValueMap; 034 035/** 036 * Represents an immutable collection of URI components, mapping component type to 037 * String values. Contains convenience getters for all components. Effectively similar 038 * to {@link java.net.URI}, but with more powerful encoding options and support for 039 * URI template variables. 040 * 041 * @author Arjen Poutsma 042 * @author Juergen Hoeller 043 * @author Rossen Stoyanchev 044 * @since 3.1 045 * @see UriComponentsBuilder 046 */ 047@SuppressWarnings("serial") 048public abstract class UriComponents implements Serializable { 049 050 /** Captures URI template variable names. */ 051 private static final Pattern NAMES_PATTERN = Pattern.compile("\\{([^/]+?)}"); 052 053 054 @Nullable 055 private final String scheme; 056 057 @Nullable 058 private final String fragment; 059 060 061 protected UriComponents(@Nullable String scheme, @Nullable String fragment) { 062 this.scheme = scheme; 063 this.fragment = fragment; 064 } 065 066 067 // Component getters 068 069 /** 070 * Return the scheme. Can be {@code null}. 071 */ 072 @Nullable 073 public final String getScheme() { 074 return this.scheme; 075 } 076 077 /** 078 * Return the fragment. Can be {@code null}. 079 */ 080 @Nullable 081 public final String getFragment() { 082 return this.fragment; 083 } 084 085 /** 086 * Return the scheme specific part. Can be {@code null}. 087 */ 088 @Nullable 089 public abstract String getSchemeSpecificPart(); 090 091 /** 092 * Return the user info. Can be {@code null}. 093 */ 094 @Nullable 095 public abstract String getUserInfo(); 096 097 /** 098 * Return the host. Can be {@code null}. 099 */ 100 @Nullable 101 public abstract String getHost(); 102 103 /** 104 * Return the port. {@code -1} if no port has been set. 105 */ 106 public abstract int getPort(); 107 108 /** 109 * Return the path. Can be {@code null}. 110 */ 111 @Nullable 112 public abstract String getPath(); 113 114 /** 115 * Return the list of path segments. Empty if no path has been set. 116 */ 117 public abstract List<String> getPathSegments(); 118 119 /** 120 * Return the query. Can be {@code null}. 121 */ 122 @Nullable 123 public abstract String getQuery(); 124 125 /** 126 * Return the map of query parameters. Empty if no query has been set. 127 */ 128 public abstract MultiValueMap<String, String> getQueryParams(); 129 130 131 /** 132 * Invoke this <em>after</em> expanding URI variables to encode the 133 * resulting URI component values. 134 * <p>In comparison to {@link UriComponentsBuilder#encode()}, this method 135 * <em>only</em> replaces non-ASCII and illegal (within a given URI 136 * component type) characters, but not characters with reserved meaning. 137 * For most cases, {@link UriComponentsBuilder#encode()} is more likely 138 * to give the expected result. 139 * @see UriComponentsBuilder#encode() 140 */ 141 public final UriComponents encode() { 142 return encode(StandardCharsets.UTF_8); 143 } 144 145 /** 146 * A variant of {@link #encode()} with a charset other than "UTF-8". 147 * @param charset the charset to use for encoding 148 * @see UriComponentsBuilder#encode(Charset) 149 */ 150 public abstract UriComponents encode(Charset charset); 151 152 /** 153 * Replace all URI template variables with the values from a given map. 154 * <p>The given map keys represent variable names; the corresponding values 155 * represent variable values. The order of variables is not significant. 156 * @param uriVariables the map of URI variables 157 * @return the expanded URI components 158 */ 159 public final UriComponents expand(Map<String, ?> uriVariables) { 160 Assert.notNull(uriVariables, "'uriVariables' must not be null"); 161 return expandInternal(new MapTemplateVariables(uriVariables)); 162 } 163 164 /** 165 * Replace all URI template variables with the values from a given array. 166 * <p>The given array represents variable values. The order of variables is significant. 167 * @param uriVariableValues the URI variable values 168 * @return the expanded URI components 169 */ 170 public final UriComponents expand(Object... uriVariableValues) { 171 Assert.notNull(uriVariableValues, "'uriVariableValues' must not be null"); 172 return expandInternal(new VarArgsTemplateVariables(uriVariableValues)); 173 } 174 175 /** 176 * Replace all URI template variables with the values from the given 177 * {@link UriTemplateVariables}. 178 * @param uriVariables the URI template values 179 * @return the expanded URI components 180 */ 181 public final UriComponents expand(UriTemplateVariables uriVariables) { 182 Assert.notNull(uriVariables, "'uriVariables' must not be null"); 183 return expandInternal(uriVariables); 184 } 185 186 /** 187 * Replace all URI template variables with the values from the given {@link 188 * UriTemplateVariables}. 189 * @param uriVariables the URI template values 190 * @return the expanded URI components 191 */ 192 abstract UriComponents expandInternal(UriTemplateVariables uriVariables); 193 194 /** 195 * Normalize the path removing sequences like "path/..". Note that 196 * normalization is applied to the full path, and not to individual path 197 * segments. 198 * @see org.springframework.util.StringUtils#cleanPath(String) 199 */ 200 public abstract UriComponents normalize(); 201 202 /** 203 * Concatenate all URI components to return the fully formed URI String. 204 * <p>This method does nothing more than a simple concatenation based on 205 * current values. That means it could produce different results if invoked 206 * before vs after methods that can change individual values such as 207 * {@code encode}, {@code expand}, or {@code normalize}. 208 */ 209 public abstract String toUriString(); 210 211 /** 212 * Create a {@link URI} from this instance as follows: 213 * <p>If the current instance is {@link #encode() encoded}, form the full 214 * URI String via {@link #toUriString()}, and then pass it to the single 215 * argument {@link URI} constructor which preserves percent encoding. 216 * <p>If not yet encoded, pass individual URI component values to the 217 * multi-argument {@link URI} constructor which quotes illegal characters 218 * that cannot appear in their respective URI component. 219 */ 220 public abstract URI toUri(); 221 222 /** 223 * A simple pass-through to {@link #toUriString()}. 224 */ 225 @Override 226 public final String toString() { 227 return toUriString(); 228 } 229 230 /** 231 * Set all components of the given UriComponentsBuilder. 232 * @since 4.2 233 */ 234 protected abstract void copyToUriComponentsBuilder(UriComponentsBuilder builder); 235 236 237 // Static expansion helpers 238 239 @Nullable 240 static String expandUriComponent(@Nullable String source, UriTemplateVariables uriVariables) { 241 return expandUriComponent(source, uriVariables, null); 242 } 243 244 @Nullable 245 static String expandUriComponent(@Nullable String source, UriTemplateVariables uriVariables, 246 @Nullable UnaryOperator<String> encoder) { 247 248 if (source == null) { 249 return null; 250 } 251 if (source.indexOf('{') == -1) { 252 return source; 253 } 254 if (source.indexOf(':') != -1) { 255 source = sanitizeSource(source); 256 } 257 Matcher matcher = NAMES_PATTERN.matcher(source); 258 StringBuffer sb = new StringBuffer(); 259 while (matcher.find()) { 260 String match = matcher.group(1); 261 String varName = getVariableName(match); 262 Object varValue = uriVariables.getValue(varName); 263 if (UriTemplateVariables.SKIP_VALUE.equals(varValue)) { 264 continue; 265 } 266 String formatted = getVariableValueAsString(varValue); 267 formatted = encoder != null ? encoder.apply(formatted) : Matcher.quoteReplacement(formatted); 268 matcher.appendReplacement(sb, formatted); 269 } 270 matcher.appendTail(sb); 271 return sb.toString(); 272 } 273 274 /** 275 * Remove nested "{}" such as in URI vars with regular expressions. 276 */ 277 private static String sanitizeSource(String source) { 278 int level = 0; 279 StringBuilder sb = new StringBuilder(); 280 for (char c : source.toCharArray()) { 281 if (c == '{') { 282 level++; 283 } 284 if (c == '}') { 285 level--; 286 } 287 if (level > 1 || (level == 1 && c == '}')) { 288 continue; 289 } 290 sb.append(c); 291 } 292 return sb.toString(); 293 } 294 295 private static String getVariableName(String match) { 296 int colonIdx = match.indexOf(':'); 297 return (colonIdx != -1 ? match.substring(0, colonIdx) : match); 298 } 299 300 private static String getVariableValueAsString(@Nullable Object variableValue) { 301 return (variableValue != null ? variableValue.toString() : ""); 302 } 303 304 305 /** 306 * Defines the contract for URI Template variables. 307 * @see HierarchicalUriComponents#expand 308 */ 309 public interface UriTemplateVariables { 310 311 /** 312 * Constant for a value that indicates a URI variable name should be 313 * ignored and left as is. This is useful for partial expanding of some 314 * but not all URI variables. 315 */ 316 Object SKIP_VALUE = UriTemplateVariables.class; 317 318 /** 319 * Get the value for the given URI variable name. 320 * If the value is {@code null}, an empty String is expanded. 321 * If the value is {@link #SKIP_VALUE}, the URI variable is not expanded. 322 * @param name the variable name 323 * @return the variable value, possibly {@code null} or {@link #SKIP_VALUE} 324 */ 325 @Nullable 326 Object getValue(@Nullable String name); 327 } 328 329 330 /** 331 * URI template variables backed by a map. 332 */ 333 private static class MapTemplateVariables implements UriTemplateVariables { 334 335 private final Map<String, ?> uriVariables; 336 337 public MapTemplateVariables(Map<String, ?> uriVariables) { 338 this.uriVariables = uriVariables; 339 } 340 341 @Override 342 @Nullable 343 public Object getValue(@Nullable String name) { 344 if (!this.uriVariables.containsKey(name)) { 345 throw new IllegalArgumentException("Map has no value for '" + name + "'"); 346 } 347 return this.uriVariables.get(name); 348 } 349 } 350 351 352 /** 353 * URI template variables backed by a variable argument array. 354 */ 355 private static class VarArgsTemplateVariables implements UriTemplateVariables { 356 357 private final Iterator<Object> valueIterator; 358 359 public VarArgsTemplateVariables(Object... uriVariableValues) { 360 this.valueIterator = Arrays.asList(uriVariableValues).iterator(); 361 } 362 363 @Override 364 @Nullable 365 public Object getValue(@Nullable String name) { 366 if (!this.valueIterator.hasNext()) { 367 throw new IllegalArgumentException("Not enough variable values available to expand '" + name + "'"); 368 } 369 return this.valueIterator.next(); 370 } 371 } 372 373}