001/*
002 * Copyright 2002-2018 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;
018
019import java.io.ByteArrayInputStream;
020import java.io.ByteArrayOutputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.OutputStreamWriter;
024import java.io.Writer;
025import java.net.InetSocketAddress;
026import java.net.URI;
027import java.net.URISyntaxException;
028import java.net.URLEncoder;
029import java.nio.charset.Charset;
030import java.security.Principal;
031import java.util.Arrays;
032import java.util.Enumeration;
033import java.util.Iterator;
034import java.util.List;
035import java.util.Map;
036import javax.servlet.http.HttpServletRequest;
037
038import org.springframework.http.HttpHeaders;
039import org.springframework.http.HttpMethod;
040import org.springframework.http.InvalidMediaTypeException;
041import org.springframework.http.MediaType;
042import org.springframework.util.Assert;
043import org.springframework.util.LinkedCaseInsensitiveMap;
044import org.springframework.util.StringUtils;
045
046/**
047 * {@link ServerHttpRequest} implementation that is based on a {@link HttpServletRequest}.
048 *
049 * @author Arjen Poutsma
050 * @author Rossen Stoyanchev
051 * @since 3.0
052 */
053public class ServletServerHttpRequest implements ServerHttpRequest {
054
055        protected static final String FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
056
057        protected static final String FORM_CHARSET = "UTF-8";
058
059
060        private final HttpServletRequest servletRequest;
061
062        private URI uri;
063
064        private HttpHeaders headers;
065
066        private ServerHttpAsyncRequestControl asyncRequestControl;
067
068
069        /**
070         * Construct a new instance of the ServletServerHttpRequest based on the given {@link HttpServletRequest}.
071         * @param servletRequest the servlet request
072         */
073        public ServletServerHttpRequest(HttpServletRequest servletRequest) {
074                Assert.notNull(servletRequest, "HttpServletRequest must not be null");
075                this.servletRequest = servletRequest;
076        }
077
078
079        /**
080         * Returns the {@code HttpServletRequest} this object is based on.
081         */
082        public HttpServletRequest getServletRequest() {
083                return this.servletRequest;
084        }
085
086        @Override
087        public HttpMethod getMethod() {
088                return HttpMethod.resolve(this.servletRequest.getMethod());
089        }
090
091        @Override
092        public URI getURI() {
093                if (this.uri == null) {
094                        String urlString = null;
095                        boolean hasQuery = false;
096                        try {
097                                StringBuffer url = this.servletRequest.getRequestURL();
098                                String query = this.servletRequest.getQueryString();
099                                hasQuery = StringUtils.hasText(query);
100                                if (hasQuery) {
101                                        url.append('?').append(query);
102                                }
103                                urlString = url.toString();
104                                this.uri = new URI(urlString);
105                        }
106                        catch (URISyntaxException ex) {
107                                if (!hasQuery) {
108                                        throw new IllegalStateException(
109                                                        "Could not resolve HttpServletRequest as URI: " + urlString, ex);
110                                }
111                                // Maybe a malformed query string... try plain request URL
112                                try {
113                                        urlString = this.servletRequest.getRequestURL().toString();
114                                        this.uri = new URI(urlString);
115                                }
116                                catch (URISyntaxException ex2) {
117                                        throw new IllegalStateException(
118                                                        "Could not resolve HttpServletRequest as URI: " + urlString, ex2);
119                                }
120                        }
121                }
122                return this.uri;
123        }
124
125        @Override
126        public HttpHeaders getHeaders() {
127                if (this.headers == null) {
128                        this.headers = new HttpHeaders();
129
130                        for (Enumeration<?> names = this.servletRequest.getHeaderNames(); names.hasMoreElements();) {
131                                String headerName = (String) names.nextElement();
132                                for (Enumeration<?> headerValues = this.servletRequest.getHeaders(headerName);
133                                                headerValues.hasMoreElements();) {
134                                        String headerValue = (String) headerValues.nextElement();
135                                        this.headers.add(headerName, headerValue);
136                                }
137                        }
138
139                        // HttpServletRequest exposes some headers as properties:
140                        // we should include those if not already present
141                        try {
142                                MediaType contentType = this.headers.getContentType();
143                                if (contentType == null) {
144                                        String requestContentType = this.servletRequest.getContentType();
145                                        if (StringUtils.hasLength(requestContentType)) {
146                                                contentType = MediaType.parseMediaType(requestContentType);
147                                                this.headers.setContentType(contentType);
148                                        }
149                                }
150                                if (contentType != null && contentType.getCharset() == null) {
151                                        String requestEncoding = this.servletRequest.getCharacterEncoding();
152                                        if (StringUtils.hasLength(requestEncoding)) {
153                                                Charset charSet = Charset.forName(requestEncoding);
154                                                Map<String, String> params = new LinkedCaseInsensitiveMap<String>();
155                                                params.putAll(contentType.getParameters());
156                                                params.put("charset", charSet.toString());
157                                                MediaType mediaType = new MediaType(contentType.getType(), contentType.getSubtype(), params);
158                                                this.headers.setContentType(mediaType);
159                                        }
160                                }
161                        }
162                        catch (InvalidMediaTypeException ex) {
163                                // Ignore: simply not exposing an invalid content type in HttpHeaders...
164                        }
165
166                        if (this.headers.getContentLength() < 0) {
167                                int requestContentLength = this.servletRequest.getContentLength();
168                                if (requestContentLength != -1) {
169                                        this.headers.setContentLength(requestContentLength);
170                                }
171                        }
172                }
173
174                return this.headers;
175        }
176
177        @Override
178        public Principal getPrincipal() {
179                return this.servletRequest.getUserPrincipal();
180        }
181
182        @Override
183        public InetSocketAddress getLocalAddress() {
184                return new InetSocketAddress(this.servletRequest.getLocalName(), this.servletRequest.getLocalPort());
185        }
186
187        @Override
188        public InetSocketAddress getRemoteAddress() {
189                return new InetSocketAddress(this.servletRequest.getRemoteHost(), this.servletRequest.getRemotePort());
190        }
191
192        @Override
193        public InputStream getBody() throws IOException {
194                if (isFormPost(this.servletRequest)) {
195                        return getBodyFromServletRequestParameters(this.servletRequest);
196                }
197                else {
198                        return this.servletRequest.getInputStream();
199                }
200        }
201
202        @Override
203        public ServerHttpAsyncRequestControl getAsyncRequestControl(ServerHttpResponse response) {
204                if (this.asyncRequestControl == null) {
205                        if (!ServletServerHttpResponse.class.isInstance(response)) {
206                                throw new IllegalArgumentException(
207                                                "Response must be a ServletServerHttpResponse: " + response.getClass());
208                        }
209                        ServletServerHttpResponse servletServerResponse = (ServletServerHttpResponse) response;
210                        this.asyncRequestControl = new ServletServerHttpAsyncRequestControl(this, servletServerResponse);
211                }
212                return this.asyncRequestControl;
213        }
214
215
216        private static boolean isFormPost(HttpServletRequest request) {
217                String contentType = request.getContentType();
218                return (contentType != null && contentType.contains(FORM_CONTENT_TYPE) &&
219                                HttpMethod.POST.matches(request.getMethod()));
220        }
221
222        /**
223         * Use {@link javax.servlet.ServletRequest#getParameterMap()} to reconstruct the
224         * body of a form 'POST' providing a predictable outcome as opposed to reading
225         * from the body, which can fail if any other code has used the ServletRequest
226         * to access a parameter, thus causing the input stream to be "consumed".
227         */
228        private static InputStream getBodyFromServletRequestParameters(HttpServletRequest request) throws IOException {
229                ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);
230                Writer writer = new OutputStreamWriter(bos, FORM_CHARSET);
231
232                Map<String, String[]> form = request.getParameterMap();
233                for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext();) {
234                        String name = nameIterator.next();
235                        List<String> values = Arrays.asList(form.get(name));
236                        for (Iterator<String> valueIterator = values.iterator(); valueIterator.hasNext();) {
237                                String value = valueIterator.next();
238                                writer.write(URLEncoder.encode(name, FORM_CHARSET));
239                                if (value != null) {
240                                        writer.write('=');
241                                        writer.write(URLEncoder.encode(value, FORM_CHARSET));
242                                        if (valueIterator.hasNext()) {
243                                                writer.write('&');
244                                        }
245                                }
246                        }
247                        if (nameIterator.hasNext()) {
248                                writer.append('&');
249                        }
250                }
251                writer.flush();
252
253                return new ByteArrayInputStream(bos.toByteArray());
254        }
255
256}