001/* 002 * Copyright 2002-2017 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.io.UnsupportedEncodingException; 021import java.net.URI; 022import java.util.Arrays; 023import java.util.Iterator; 024import java.util.List; 025import java.util.Map; 026import java.util.regex.Matcher; 027import java.util.regex.Pattern; 028 029import org.springframework.util.Assert; 030import org.springframework.util.MultiValueMap; 031 032/** 033 * Represents an immutable collection of URI components, mapping component type to 034 * String values. Contains convenience getters for all components. Effectively similar 035 * to {@link java.net.URI}, but with more powerful encoding options and support for 036 * URI template variables. 037 * 038 * @author Arjen Poutsma 039 * @author Juergen Hoeller 040 * @author Rossen Stoyanchev 041 * @since 3.1 042 * @see UriComponentsBuilder 043 */ 044@SuppressWarnings("serial") 045public abstract class UriComponents implements Serializable { 046 047 private static final String DEFAULT_ENCODING = "UTF-8"; 048 049 /** Captures URI template variable names */ 050 private static final Pattern NAMES_PATTERN = Pattern.compile("\\{([^/]+?)\\}"); 051 052 053 private final String scheme; 054 055 private final String fragment; 056 057 058 protected UriComponents(String scheme, String fragment) { 059 this.scheme = scheme; 060 this.fragment = fragment; 061 } 062 063 064 // Component getters 065 066 /** 067 * Return the scheme. Can be {@code null}. 068 */ 069 public final String getScheme() { 070 return this.scheme; 071 } 072 073 /** 074 * Return the fragment. Can be {@code null}. 075 */ 076 public final String getFragment() { 077 return this.fragment; 078 } 079 080 /** 081 * Return the scheme specific part. Can be {@code null}. 082 */ 083 public abstract String getSchemeSpecificPart(); 084 085 /** 086 * Return the user info. Can be {@code null}. 087 */ 088 public abstract String getUserInfo(); 089 090 /** 091 * Return the host. Can be {@code null}. 092 */ 093 public abstract String getHost(); 094 095 /** 096 * Return the port. {@code -1} if no port has been set. 097 */ 098 public abstract int getPort(); 099 100 /** 101 * Return the path. Can be {@code null}. 102 */ 103 public abstract String getPath(); 104 105 /** 106 * Return the list of path segments. Empty if no path has been set. 107 */ 108 public abstract List<String> getPathSegments(); 109 110 /** 111 * Return the query. Can be {@code null}. 112 */ 113 public abstract String getQuery(); 114 115 /** 116 * Return the map of query parameters. Empty if no query has been set. 117 */ 118 public abstract MultiValueMap<String, String> getQueryParams(); 119 120 121 /** 122 * Encode all URI components using their specific encoding rules, and returns the 123 * result as a new {@code UriComponents} instance. This method uses UTF-8 to encode. 124 * @return the encoded URI components 125 */ 126 public final UriComponents encode() { 127 try { 128 return encode(DEFAULT_ENCODING); 129 } 130 catch (UnsupportedEncodingException ex) { 131 // should not occur 132 throw new IllegalStateException(ex); 133 } 134 } 135 136 /** 137 * Encode all URI components using their specific encoding rules, and 138 * returns the result as a new {@code UriComponents} instance. 139 * @param encoding the encoding of the values contained in this map 140 * @return the encoded URI components 141 * @throws UnsupportedEncodingException if the given encoding is not supported 142 */ 143 public abstract UriComponents encode(String encoding) throws UnsupportedEncodingException; 144 145 /** 146 * Replace all URI template variables with the values from a given map. 147 * <p>The given map keys represent variable names; the corresponding values 148 * represent variable values. The order of variables is not significant. 149 * @param uriVariables the map of URI variables 150 * @return the expanded URI components 151 */ 152 public final UriComponents expand(Map<String, ?> uriVariables) { 153 Assert.notNull(uriVariables, "'uriVariables' must not be null"); 154 return expandInternal(new MapTemplateVariables(uriVariables)); 155 } 156 157 /** 158 * Replace all URI template variables with the values from a given array. 159 * <p>The given array represents variable values. The order of variables is significant. 160 * @param uriVariableValues the URI variable values 161 * @return the expanded URI components 162 */ 163 public final UriComponents expand(Object... uriVariableValues) { 164 Assert.notNull(uriVariableValues, "'uriVariableValues' must not be null"); 165 return expandInternal(new VarArgsTemplateVariables(uriVariableValues)); 166 } 167 168 /** 169 * Replace all URI template variables with the values from the given 170 * {@link UriTemplateVariables}. 171 * @param uriVariables the URI template values 172 * @return the expanded URI components 173 */ 174 public final UriComponents expand(UriTemplateVariables uriVariables) { 175 Assert.notNull(uriVariables, "'uriVariables' must not be null"); 176 return expandInternal(uriVariables); 177 } 178 179 /** 180 * Replace all URI template variables with the values from the given {@link 181 * UriTemplateVariables} 182 * @param uriVariables URI template values 183 * @return the expanded URI components 184 */ 185 abstract UriComponents expandInternal(UriTemplateVariables uriVariables); 186 187 /** 188 * Normalize the path removing sequences like "path/..". 189 * @see org.springframework.util.StringUtils#cleanPath(String) 190 */ 191 public abstract UriComponents normalize(); 192 193 /** 194 * Return a URI String from this {@code UriComponents} instance. 195 */ 196 public abstract String toUriString(); 197 198 /** 199 * Return a {@code URI} from this {@code UriComponents} instance. 200 */ 201 public abstract URI toUri(); 202 203 @Override 204 public final String toString() { 205 return toUriString(); 206 } 207 208 /** 209 * Set all components of the given UriComponentsBuilder. 210 * @since 4.2 211 */ 212 protected abstract void copyToUriComponentsBuilder(UriComponentsBuilder builder); 213 214 215 // Static expansion helpers 216 217 static String expandUriComponent(String source, UriTemplateVariables uriVariables) { 218 if (source == null) { 219 return null; 220 } 221 if (source.indexOf('{') == -1) { 222 return source; 223 } 224 if (source.indexOf(':') != -1) { 225 source = sanitizeSource(source); 226 } 227 Matcher matcher = NAMES_PATTERN.matcher(source); 228 StringBuffer sb = new StringBuffer(); 229 while (matcher.find()) { 230 String match = matcher.group(1); 231 String variableName = getVariableName(match); 232 Object variableValue = uriVariables.getValue(variableName); 233 if (UriTemplateVariables.SKIP_VALUE.equals(variableValue)) { 234 continue; 235 } 236 String variableValueString = getVariableValueAsString(variableValue); 237 String replacement = Matcher.quoteReplacement(variableValueString); 238 matcher.appendReplacement(sb, replacement); 239 } 240 matcher.appendTail(sb); 241 return sb.toString(); 242 } 243 244 /** 245 * Remove nested "{}" such as in URI vars with regular expressions. 246 */ 247 private static String sanitizeSource(String source) { 248 int level = 0; 249 StringBuilder sb = new StringBuilder(); 250 for (char c : source.toCharArray()) { 251 if (c == '{') { 252 level++; 253 } 254 if (c == '}') { 255 level--; 256 } 257 if (level > 1 || (level == 1 && c == '}')) { 258 continue; 259 } 260 sb.append(c); 261 } 262 return sb.toString(); 263 } 264 265 private static String getVariableName(String match) { 266 int colonIdx = match.indexOf(':'); 267 return (colonIdx != -1 ? match.substring(0, colonIdx) : match); 268 } 269 270 private static String getVariableValueAsString(Object variableValue) { 271 return (variableValue != null ? variableValue.toString() : ""); 272 } 273 274 275 /** 276 * Defines the contract for URI Template variables 277 * @see HierarchicalUriComponents#expand 278 */ 279 public interface UriTemplateVariables { 280 281 Object SKIP_VALUE = UriTemplateVariables.class; 282 283 /** 284 * Get the value for the given URI variable name. 285 * If the value is {@code null}, an empty String is expanded. 286 * If the value is {@link #SKIP_VALUE}, the URI variable is not expanded. 287 * @param name the variable name 288 * @return the variable value, possibly {@code null} or {@link #SKIP_VALUE} 289 */ 290 Object getValue(String name); 291 } 292 293 294 /** 295 * URI template variables backed by a map. 296 */ 297 private static class MapTemplateVariables implements UriTemplateVariables { 298 299 private final Map<String, ?> uriVariables; 300 301 public MapTemplateVariables(Map<String, ?> uriVariables) { 302 this.uriVariables = uriVariables; 303 } 304 305 @Override 306 public Object getValue(String name) { 307 if (!this.uriVariables.containsKey(name)) { 308 throw new IllegalArgumentException("Map has no value for '" + name + "'"); 309 } 310 return this.uriVariables.get(name); 311 } 312 } 313 314 315 /** 316 * URI template variables backed by a variable argument array. 317 */ 318 private static class VarArgsTemplateVariables implements UriTemplateVariables { 319 320 private final Iterator<Object> valueIterator; 321 322 public VarArgsTemplateVariables(Object... uriVariableValues) { 323 this.valueIterator = Arrays.asList(uriVariableValues).iterator(); 324 } 325 326 @Override 327 public Object getValue(String name) { 328 if (!this.valueIterator.hasNext()) { 329 throw new IllegalArgumentException("Not enough variable values available to expand '" + name + "'"); 330 } 331 return this.valueIterator.next(); 332 } 333 } 334 335}