001/*
002 * Copyright 2002-2019 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.util.Assert;
030
031/**
032 * Representation of a URI template that can be expanded with URI variables via
033 * {@link #expand(Map)}, {@link #expand(Object[])}, or matched to a URL via
034 * {@link #match(String)}. This class is designed to be thread-safe and
035 * reusable, and allows any number of expand or match calls.
036 *
037 * <p><strong>Note:</strong> this class uses {@link UriComponentsBuilder}
038 * internally to expand URI templates, and is merely a shortcut for already
039 * prepared URI templates. For more dynamic preparation and extra flexibility,
040 * e.g. around URI encoding, consider using {@code UriComponentsBuilder} or the
041 * higher level {@link DefaultUriBuilderFactory} which adds several encoding
042 * modes on top of {@code UriComponentsBuilder}. See the
043 * <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-uri-building">reference docs</a>
044 * for further details.
045 *
046 * @author Arjen Poutsma
047 * @author Juergen Hoeller
048 * @author Rossen Stoyanchev
049 * @since 3.0
050 */
051@SuppressWarnings("serial")
052public class UriTemplate implements Serializable {
053
054        private final String uriTemplate;
055
056        private final UriComponents uriComponents;
057
058        private final List<String> variableNames;
059
060        private final Pattern matchPattern;
061
062
063        /**
064         * Construct a new {@code UriTemplate} with the given URI String.
065         * @param uriTemplate the URI template string
066         */
067        public UriTemplate(String uriTemplate) {
068                Assert.hasText(uriTemplate, "'uriTemplate' must not be null");
069                this.uriTemplate = uriTemplate;
070                this.uriComponents = UriComponentsBuilder.fromUriString(uriTemplate).build();
071
072                TemplateInfo info = TemplateInfo.parse(uriTemplate);
073                this.variableNames = Collections.unmodifiableList(info.getVariableNames());
074                this.matchPattern = info.getMatchPattern();
075        }
076
077
078        /**
079         * Return the names of the variables in the template, in order.
080         * @return the template variable names
081         */
082        public List<String> getVariableNames() {
083                return this.variableNames;
084        }
085
086        /**
087         * Given the Map of variables, expands this template into a URI. The Map keys represent variable names,
088         * the Map values variable values. The order of variables is not significant.
089         * <p>Example:
090         * <pre class="code">
091         * UriTemplate template = new UriTemplate("https://example.com/hotels/{hotel}/bookings/{booking}");
092         * Map&lt;String, String&gt; uriVariables = new HashMap&lt;String, String&gt;();
093         * uriVariables.put("booking", "42");
094         * uriVariables.put("hotel", "Rest & Relax");
095         * System.out.println(template.expand(uriVariables));
096         * </pre>
097         * will print: <blockquote>{@code https://example.com/hotels/Rest%20%26%20Relax/bookings/42}</blockquote>
098         * @param uriVariables the map of URI variables
099         * @return the expanded URI
100         * @throws IllegalArgumentException if {@code uriVariables} is {@code null};
101         * or if it does not contain values for all the variable names
102         */
103        public URI expand(Map<String, ?> uriVariables) {
104                UriComponents expandedComponents = this.uriComponents.expand(uriVariables);
105                UriComponents encodedComponents = expandedComponents.encode();
106                return encodedComponents.toUri();
107        }
108
109    /**
110     * Given an array of variables, expand this template into a full URI. The array represent variable values.
111     * The order of variables is significant.
112     * <p>Example:
113     * <pre class="code">
114     * UriTemplate template = new UriTemplate("https://example.com/hotels/{hotel}/bookings/{booking}");
115     * System.out.println(template.expand("Rest & Relax", 42));
116     * </pre>
117     * will print: <blockquote>{@code https://example.com/hotels/Rest%20%26%20Relax/bookings/42}</blockquote>
118     * @param uriVariableValues the array of URI variables
119     * @return the expanded URI
120     * @throws IllegalArgumentException if {@code uriVariables} is {@code null}
121     * or if it does not contain sufficient variables
122     */
123        public URI expand(Object... uriVariableValues) {
124                UriComponents expandedComponents = this.uriComponents.expand(uriVariableValues);
125                UriComponents encodedComponents = expandedComponents.encode();
126                return encodedComponents.toUri();
127        }
128
129        /**
130         * Indicate whether the given URI matches this template.
131         * @param uri the URI to match to
132         * @return {@code true} if it matches; {@code false} otherwise
133         */
134        public boolean matches(String uri) {
135                if (uri == null) {
136                        return false;
137                }
138                Matcher matcher = this.matchPattern.matcher(uri);
139                return matcher.matches();
140        }
141
142        /**
143         * Match the given URI to a map of variable values. Keys in the returned map are variable names,
144         * values are variable values, as occurred in the given URI.
145         * <p>Example:
146         * <pre class="code">
147         * UriTemplate template = new UriTemplate("https://example.com/hotels/{hotel}/bookings/{booking}");
148         * System.out.println(template.match("https://example.com/hotels/1/bookings/42"));
149         * </pre>
150         * will print: <blockquote>{@code {hotel=1, booking=42}}</blockquote>
151         * @param uri the URI to match to
152         * @return a map of variable values
153         */
154        public Map<String, String> match(String uri) {
155                Assert.notNull(uri, "'uri' must not be null");
156                Map<String, String> result = new LinkedHashMap<String, String>(this.variableNames.size());
157                Matcher matcher = this.matchPattern.matcher(uri);
158                if (matcher.find()) {
159                        for (int i = 1; i <= matcher.groupCount(); i++) {
160                                String name = this.variableNames.get(i - 1);
161                                String value = matcher.group(i);
162                                result.put(name, value);
163                        }
164                }
165                return result;
166        }
167
168        @Override
169        public String toString() {
170                return this.uriTemplate;
171        }
172
173
174        /**
175         * Helper to extract variable names and regex for matching to actual URLs.
176         */
177        private static class TemplateInfo {
178
179                private final List<String> variableNames;
180
181                private final Pattern pattern;
182
183                private TemplateInfo(List<String> vars, Pattern pattern) {
184                        this.variableNames = vars;
185                        this.pattern = pattern;
186                }
187
188                public List<String> getVariableNames() {
189                        return this.variableNames;
190                }
191
192                public Pattern getMatchPattern() {
193                        return this.pattern;
194                }
195
196                public static TemplateInfo parse(String uriTemplate) {
197                        int level = 0;
198                        List<String> variableNames = new ArrayList<String>();
199                        StringBuilder pattern = new StringBuilder();
200                        StringBuilder builder = new StringBuilder();
201                        for (int i = 0 ; i < uriTemplate.length(); i++) {
202                                char c = uriTemplate.charAt(i);
203                                if (c == '{') {
204                                        level++;
205                                        if (level == 1) {
206                                                // start of URI variable
207                                                pattern.append(quote(builder));
208                                                builder = new StringBuilder();
209                                                continue;
210                                        }
211                                }
212                                else if (c == '}') {
213                                        level--;
214                                        if (level == 0) {
215                                                // end of URI variable
216                                                String variable = builder.toString();
217                                                int idx = variable.indexOf(':');
218                                                if (idx == -1) {
219                                                        pattern.append("(.*)");
220                                                        variableNames.add(variable);
221                                                }
222                                                else {
223                                                        if (idx + 1 == variable.length()) {
224                                                                throw new IllegalArgumentException(
225                                                                                "No custom regular expression specified after ':' in \"" + variable + "\"");
226                                                        }
227                                                        String regex = variable.substring(idx + 1);
228                                                        pattern.append('(');
229                                                        pattern.append(regex);
230                                                        pattern.append(')');
231                                                        variableNames.add(variable.substring(0, idx));
232                                                }
233                                                builder = new StringBuilder();
234                                                continue;
235                                        }
236                                }
237                                builder.append(c);
238                        }
239                        if (builder.length() > 0) {
240                                pattern.append(quote(builder));
241                        }
242                        return new TemplateInfo(variableNames, Pattern.compile(pattern.toString()));
243                }
244
245                private static String quote(StringBuilder builder) {
246                        return (builder.length() > 0 ? Pattern.quote(builder.toString()) : "");
247                }
248        }
249
250}