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}