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.servlet.support;
018
019import javax.servlet.http.HttpServletRequest;
020
021import org.springframework.lang.Nullable;
022import org.springframework.util.Assert;
023import org.springframework.util.StringUtils;
024import org.springframework.web.context.request.RequestAttributes;
025import org.springframework.web.context.request.RequestContextHolder;
026import org.springframework.web.context.request.ServletRequestAttributes;
027import org.springframework.web.util.UriComponentsBuilder;
028import org.springframework.web.util.UriUtils;
029import org.springframework.web.util.UrlPathHelper;
030
031/**
032 * UriComponentsBuilder with additional static factory methods to create links
033 * based on the current HttpServletRequest.
034 *
035 * <p><strong>Note:</strong> As of 5.1, methods in this class do not extract
036 * {@code "Forwarded"} and {@code "X-Forwarded-*"} headers that specify the
037 * client-originated address. Please, use
038 * {@link org.springframework.web.filter.ForwardedHeaderFilter
039 * ForwardedHeaderFilter}, or similar from the underlying server, to extract
040 * and use such headers, or to discard them.
041 *
042 * @author Rossen Stoyanchev
043 * @since 3.1
044 */
045public class ServletUriComponentsBuilder extends UriComponentsBuilder {
046
047        @Nullable
048        private String originalPath;
049
050
051        /**
052         * Default constructor. Protected to prevent direct instantiation.
053         * @see #fromContextPath(HttpServletRequest)
054         * @see #fromServletMapping(HttpServletRequest)
055         * @see #fromRequest(HttpServletRequest)
056         * @see #fromCurrentContextPath()
057         * @see #fromCurrentServletMapping()
058         * @see #fromCurrentRequest()
059         */
060        protected ServletUriComponentsBuilder() {
061        }
062
063        /**
064         * Create a deep copy of the given ServletUriComponentsBuilder.
065         * @param other the other builder to copy from
066         */
067        protected ServletUriComponentsBuilder(ServletUriComponentsBuilder other) {
068                super(other);
069                this.originalPath = other.originalPath;
070        }
071
072
073        // Factory methods based on an HttpServletRequest
074
075        /**
076         * Prepare a builder from the host, port, scheme, and context path of the
077         * given HttpServletRequest.
078         */
079        public static ServletUriComponentsBuilder fromContextPath(HttpServletRequest request) {
080                ServletUriComponentsBuilder builder = initFromRequest(request);
081                builder.replacePath(request.getContextPath());
082                return builder;
083        }
084
085        /**
086         * Prepare a builder from the host, port, scheme, context path, and
087         * servlet mapping of the given HttpServletRequest.
088         * <p>If the servlet is mapped by name, e.g. {@code "/main/*"}, the path
089         * will end with "/main". If the servlet is mapped otherwise, e.g.
090         * {@code "/"} or {@code "*.do"}, the result will be the same as
091         * if calling {@link #fromContextPath(HttpServletRequest)}.
092         */
093        public static ServletUriComponentsBuilder fromServletMapping(HttpServletRequest request) {
094                ServletUriComponentsBuilder builder = fromContextPath(request);
095                if (StringUtils.hasText(UrlPathHelper.defaultInstance.getPathWithinServletMapping(request))) {
096                        builder.path(request.getServletPath());
097                }
098                return builder;
099        }
100
101        /**
102         * Prepare a builder from the host, port, scheme, and path (but not the query)
103         * of the HttpServletRequest.
104         */
105        public static ServletUriComponentsBuilder fromRequestUri(HttpServletRequest request) {
106                ServletUriComponentsBuilder builder = initFromRequest(request);
107                builder.initPath(request.getRequestURI());
108                return builder;
109        }
110
111        /**
112         * Prepare a builder by copying the scheme, host, port, path, and
113         * query string of an HttpServletRequest.
114         */
115        public static ServletUriComponentsBuilder fromRequest(HttpServletRequest request) {
116                ServletUriComponentsBuilder builder = initFromRequest(request);
117                builder.initPath(request.getRequestURI());
118                builder.query(request.getQueryString());
119                return builder;
120        }
121
122        /**
123         * Initialize a builder with a scheme, host,and port (but not path and query).
124         */
125        private static ServletUriComponentsBuilder initFromRequest(HttpServletRequest request) {
126                String scheme = request.getScheme();
127                String host = request.getServerName();
128                int port = request.getServerPort();
129
130                ServletUriComponentsBuilder builder = new ServletUriComponentsBuilder();
131                builder.scheme(scheme);
132                builder.host(host);
133                if (("http".equals(scheme) && port != 80) || ("https".equals(scheme) && port != 443)) {
134                        builder.port(port);
135                }
136                return builder;
137        }
138
139
140        // Alternative methods relying on RequestContextHolder to find the request
141
142        /**
143         * Same as {@link #fromContextPath(HttpServletRequest)} except the
144         * request is obtained through {@link RequestContextHolder}.
145         */
146        public static ServletUriComponentsBuilder fromCurrentContextPath() {
147                return fromContextPath(getCurrentRequest());
148        }
149
150        /**
151         * Same as {@link #fromServletMapping(HttpServletRequest)} except the
152         * request is obtained through {@link RequestContextHolder}.
153         */
154        public static ServletUriComponentsBuilder fromCurrentServletMapping() {
155                return fromServletMapping(getCurrentRequest());
156        }
157
158        /**
159         * Same as {@link #fromRequestUri(HttpServletRequest)} except the
160         * request is obtained through {@link RequestContextHolder}.
161         */
162        public static ServletUriComponentsBuilder fromCurrentRequestUri() {
163                return fromRequestUri(getCurrentRequest());
164        }
165
166        /**
167         * Same as {@link #fromRequest(HttpServletRequest)} except the
168         * request is obtained through {@link RequestContextHolder}.
169         */
170        public static ServletUriComponentsBuilder fromCurrentRequest() {
171                return fromRequest(getCurrentRequest());
172        }
173
174        /**
175         * Obtain current request through {@link RequestContextHolder}.
176         */
177        protected static HttpServletRequest getCurrentRequest() {
178                RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
179                Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
180                return ((ServletRequestAttributes) attrs).getRequest();
181        }
182
183
184        private void initPath(String path) {
185                this.originalPath = path;
186                replacePath(path);
187        }
188
189        /**
190         * Remove any path extension from the {@link HttpServletRequest#getRequestURI()
191         * requestURI}. This method must be invoked before any calls to {@link #path(String)}
192         * or {@link #pathSegment(String...)}.
193         * <pre>
194         * GET http://www.foo.example/rest/books/6.json
195         *
196         * ServletUriComponentsBuilder builder = ServletUriComponentsBuilder.fromRequestUri(this.request);
197         * String ext = builder.removePathExtension();
198         * String uri = builder.path("/pages/1.{ext}").buildAndExpand(ext).toUriString();
199         * assertEquals("http://www.foo.example/rest/books/6/pages/1.json", result);
200         * </pre>
201         * @return the removed path extension for possible re-use, or {@code null}
202         * @since 4.0
203         */
204        @Nullable
205        public String removePathExtension() {
206                String extension = null;
207                if (this.originalPath != null) {
208                        extension = UriUtils.extractFileExtension(this.originalPath);
209                        if (StringUtils.hasLength(extension)) {
210                                int end = this.originalPath.length() - (extension.length() + 1);
211                                replacePath(this.originalPath.substring(0, end));
212                        }
213                        this.originalPath = null;
214                }
215                return extension;
216        }
217
218        @Override
219        public ServletUriComponentsBuilder cloneBuilder() {
220                return new ServletUriComponentsBuilder(this);
221        }
222
223}