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.http.server.reactive;
018
019import java.io.UnsupportedEncodingException;
020import java.net.URI;
021import java.net.URLDecoder;
022import java.util.regex.Matcher;
023import java.util.regex.Pattern;
024
025import org.apache.commons.logging.Log;
026
027import org.springframework.http.HttpCookie;
028import org.springframework.http.HttpHeaders;
029import org.springframework.http.HttpLogging;
030import org.springframework.http.server.RequestPath;
031import org.springframework.lang.Nullable;
032import org.springframework.util.CollectionUtils;
033import org.springframework.util.LinkedMultiValueMap;
034import org.springframework.util.MultiValueMap;
035import org.springframework.util.ObjectUtils;
036import org.springframework.util.StringUtils;
037
038/**
039 * Common base class for {@link ServerHttpRequest} implementations.
040 *
041 * @author Rossen Stoyanchev
042 * @since 5.0
043 */
044public abstract class AbstractServerHttpRequest implements ServerHttpRequest {
045
046        private static final Pattern QUERY_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?");
047
048
049        protected final Log logger = HttpLogging.forLogName(getClass());
050
051        private final URI uri;
052
053        private final RequestPath path;
054
055        private final HttpHeaders headers;
056
057        @Nullable
058        private MultiValueMap<String, String> queryParams;
059
060        @Nullable
061        private MultiValueMap<String, HttpCookie> cookies;
062
063        @Nullable
064        private SslInfo sslInfo;
065
066        @Nullable
067        private String id;
068
069        @Nullable
070        private String logPrefix;
071
072
073        /**
074         * Constructor with the URI and headers for the request.
075         * @param uri the URI for the request
076         * @param contextPath the context path for the request
077         * @param headers the headers for the request
078         */
079        public AbstractServerHttpRequest(URI uri, @Nullable String contextPath, HttpHeaders headers) {
080                this.uri = uri;
081                this.path = RequestPath.parse(uri, contextPath);
082                this.headers = HttpHeaders.readOnlyHttpHeaders(headers);
083        }
084
085
086        @Override
087        public String getId() {
088                if (this.id == null) {
089                        this.id = initId();
090                        if (this.id == null) {
091                                this.id = ObjectUtils.getIdentityHexString(this);
092                        }
093                }
094                return this.id;
095        }
096
097        /**
098         * Obtain the request id to use, or {@code null} in which case the Object
099         * identity of this request instance is used.
100         * @since 5.1
101         */
102        @Nullable
103        protected String initId() {
104                return null;
105        }
106
107        @Override
108        public URI getURI() {
109                return this.uri;
110        }
111
112        @Override
113        public RequestPath getPath() {
114                return this.path;
115        }
116
117        @Override
118        public HttpHeaders getHeaders() {
119                return this.headers;
120        }
121
122        @Override
123        public MultiValueMap<String, String> getQueryParams() {
124                if (this.queryParams == null) {
125                        this.queryParams = CollectionUtils.unmodifiableMultiValueMap(initQueryParams());
126                }
127                return this.queryParams;
128        }
129
130        /**
131         * A method for parsing of the query into name-value pairs. The return
132         * value is turned into an immutable map and cached.
133         * <p>Note that this method is invoked lazily on first access to
134         * {@link #getQueryParams()}. The invocation is not synchronized but the
135         * parsing is thread-safe nevertheless.
136         */
137        protected MultiValueMap<String, String> initQueryParams() {
138                MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
139                String query = getURI().getRawQuery();
140                if (query != null) {
141                        Matcher matcher = QUERY_PATTERN.matcher(query);
142                        while (matcher.find()) {
143                                String name = decodeQueryParam(matcher.group(1));
144                                String eq = matcher.group(2);
145                                String value = matcher.group(3);
146                                value = (value != null ? decodeQueryParam(value) : (StringUtils.hasLength(eq) ? "" : null));
147                                queryParams.add(name, value);
148                        }
149                }
150                return queryParams;
151        }
152
153        @SuppressWarnings("deprecation")
154        private String decodeQueryParam(String value) {
155                try {
156                        return URLDecoder.decode(value, "UTF-8");
157                }
158                catch (UnsupportedEncodingException ex) {
159                        if (logger.isWarnEnabled()) {
160                                logger.warn(getLogPrefix() + "Could not decode query value [" + value + "] as 'UTF-8'. " +
161                                                "Falling back on default encoding: " + ex.getMessage());
162                        }
163                        return URLDecoder.decode(value);
164                }
165        }
166
167        @Override
168        public MultiValueMap<String, HttpCookie> getCookies() {
169                if (this.cookies == null) {
170                        this.cookies = CollectionUtils.unmodifiableMultiValueMap(initCookies());
171                }
172                return this.cookies;
173        }
174
175        /**
176         * Obtain the cookies from the underlying "native" request and adapt those to
177         * an {@link HttpCookie} map. The return value is turned into an immutable
178         * map and cached.
179         * <p>Note that this method is invoked lazily on access to
180         * {@link #getCookies()}. Sub-classes should synchronize cookie
181         * initialization if the underlying "native" request does not provide
182         * thread-safe access to cookie data.
183         */
184        protected abstract MultiValueMap<String, HttpCookie> initCookies();
185
186        @Nullable
187        @Override
188        public SslInfo getSslInfo() {
189                if (this.sslInfo == null) {
190                        this.sslInfo = initSslInfo();
191                }
192                return this.sslInfo;
193        }
194
195        /**
196         * Obtain SSL session information from the underlying "native" request.
197         * @return the session information, or {@code null} if none available
198         * @since 5.0.2
199         */
200        @Nullable
201        protected abstract SslInfo initSslInfo();
202
203        /**
204         * Return the underlying server response.
205         * <p><strong>Note:</strong> This is exposed mainly for internal framework
206         * use such as WebSocket upgrades in the spring-webflux module.
207         */
208        public abstract <T> T getNativeRequest();
209
210        /**
211         * For internal use in logging at the HTTP adapter layer.
212         * @since 5.1
213         */
214        String getLogPrefix() {
215                if (this.logPrefix == null) {
216                        this.logPrefix = "[" + getId() + "] ";
217                }
218                return this.logPrefix;
219        }
220
221}