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&lt;String, String&gt; uriVariables = new HashMap&lt;String, String&gt;();
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}