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}