001/*
002 * Copyright 2002-2016 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.UnsupportedEncodingException;
020import java.net.URI;
021import java.net.URISyntaxException;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025
026/**
027 * Default implementation of {@link UriTemplateHandler} based on the use of
028 * {@link UriComponentsBuilder} for expanding and encoding variables.
029 *
030 * <p>There are also several properties to customize how URI template handling
031 * is performed, including a {@link #setBaseUrl baseUrl} to be used as a prefix
032 * for all URI templates and a couple of encoding related options &mdash;
033 * {@link #setParsePath parsePath} and {@link #setStrictEncoding strictEncoding}
034 * respectively.
035 *
036 * @author Rossen Stoyanchev
037 * @since 4.2
038 */
039public class DefaultUriTemplateHandler extends AbstractUriTemplateHandler {
040
041        private boolean parsePath;
042
043        private boolean strictEncoding;
044
045
046        /**
047         * Whether to parse the path of a URI template string into path segments.
048         * <p>If set to {@code true} the URI template path is immediately decomposed
049         * into path segments any URI variables expanded into it are then subject to
050         * path segment encoding rules. In effect URI variables in the path have any
051         * "/" characters percent encoded.
052         * <p>By default this is set to {@code false} in which case the path is kept
053         * as a full path and expanded URI variables will preserve "/" characters.
054         * @param parsePath whether to parse the path into path segments
055         */
056        public void setParsePath(boolean parsePath) {
057                this.parsePath = parsePath;
058        }
059
060        /**
061         * Whether the handler is configured to parse the path into path segments.
062         */
063        public boolean shouldParsePath() {
064                return this.parsePath;
065        }
066
067        /**
068         * Whether to encode characters outside the unreserved set as defined in
069         * <a href="https://tools.ietf.org/html/rfc3986#section-2">RFC 3986 Section 2</a>.
070         * This ensures a URI variable value will not contain any characters with a
071         * reserved purpose.
072         * <p>By default this is set to {@code false} in which case only characters
073         * illegal for the given URI component are encoded. For example when expanding
074         * a URI variable into a path segment the "/" character is illegal and
075         * encoded. The ";" character however is legal and not encoded even though
076         * it has a reserved purpose.
077         * <p><strong>Note:</strong> this property supersedes the need to also set
078         * the {@link #setParsePath parsePath} property.
079         * @param strictEncoding whether to perform strict encoding
080         * @since 4.3
081         */
082        public void setStrictEncoding(boolean strictEncoding) {
083                this.strictEncoding = strictEncoding;
084        }
085
086        /**
087         * Whether to strictly encode any character outside the unreserved set.
088         */
089        public boolean isStrictEncoding() {
090                return this.strictEncoding;
091        }
092
093
094        @Override
095        protected URI expandInternal(String uriTemplate, Map<String, ?> uriVariables) {
096                UriComponentsBuilder uriComponentsBuilder = initUriComponentsBuilder(uriTemplate);
097                UriComponents uriComponents = expandAndEncode(uriComponentsBuilder, uriVariables);
098                return createUri(uriComponents);
099        }
100
101        @Override
102        protected URI expandInternal(String uriTemplate, Object... uriVariables) {
103                UriComponentsBuilder uriComponentsBuilder = initUriComponentsBuilder(uriTemplate);
104                UriComponents uriComponents = expandAndEncode(uriComponentsBuilder, uriVariables);
105                return createUri(uriComponents);
106        }
107
108        /**
109         * Create a {@code UriComponentsBuilder} from the URI template string.
110         * This implementation also breaks up the path into path segments depending
111         * on whether {@link #setParsePath parsePath} is enabled.
112         */
113        protected UriComponentsBuilder initUriComponentsBuilder(String uriTemplate) {
114                UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(uriTemplate);
115                if (shouldParsePath() && !isStrictEncoding()) {
116                        List<String> pathSegments = builder.build().getPathSegments();
117                        builder.replacePath(null);
118                        for (String pathSegment : pathSegments) {
119                                builder.pathSegment(pathSegment);
120                        }
121                }
122                return builder;
123        }
124
125        protected UriComponents expandAndEncode(UriComponentsBuilder builder, Map<String, ?> uriVariables) {
126                if (!isStrictEncoding()) {
127                        return builder.buildAndExpand(uriVariables).encode();
128                }
129                else {
130                        Map<String, Object> encodedUriVars = new HashMap<String, Object>(uriVariables.size());
131                        for (Map.Entry<String, ?> entry : uriVariables.entrySet()) {
132                                encodedUriVars.put(entry.getKey(), applyStrictEncoding(entry.getValue()));
133                        }
134                        return builder.buildAndExpand(encodedUriVars);
135                }
136        }
137
138        protected UriComponents expandAndEncode(UriComponentsBuilder builder, Object[] uriVariables) {
139                if (!isStrictEncoding()) {
140                        return builder.buildAndExpand(uriVariables).encode();
141                }
142                else {
143                        Object[] encodedUriVars = new Object[uriVariables.length];
144                        for (int i = 0; i < uriVariables.length; i++) {
145                                encodedUriVars[i] = applyStrictEncoding(uriVariables[i]);
146                        }
147                        return builder.buildAndExpand(encodedUriVars);
148                }
149        }
150
151        private String applyStrictEncoding(Object value) {
152                String stringValue = (value != null ? value.toString() : "");
153                try {
154                        return UriUtils.encode(stringValue, "UTF-8");
155                }
156                catch (UnsupportedEncodingException ex) {
157                        // Should never happen
158                        throw new IllegalStateException("Failed to encode URI variable", ex);
159                }
160        }
161
162        private URI createUri(UriComponents uriComponents) {
163                try {
164                        // Avoid further encoding (in the case of strictEncoding=true)
165                        return new URI(uriComponents.toUriString());
166                }
167                catch (URISyntaxException ex) {
168                        throw new IllegalStateException("Could not create URI object: " + ex.getMessage(), ex);
169                }
170        }
171
172}