001/*
002 * Copyright 2002-2017 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.BufferedReader;
020import java.io.ByteArrayOutputStream;
021import java.io.IOException;
022import java.io.InputStreamReader;
023import java.net.URLEncoder;
024import java.util.Arrays;
025import java.util.Enumeration;
026import java.util.Iterator;
027import java.util.List;
028import java.util.Map;
029import javax.servlet.ServletInputStream;
030import javax.servlet.http.HttpServletRequest;
031import javax.servlet.http.HttpServletRequestWrapper;
032
033import org.springframework.http.HttpMethod;
034
035/**
036 * {@link javax.servlet.http.HttpServletRequest} wrapper that caches all content read from
037 * the {@linkplain #getInputStream() input stream} and {@linkplain #getReader() reader},
038 * and allows this content to be retrieved via a {@link #getContentAsByteArray() byte array}.
039 *
040 * <p>Used e.g. by {@link org.springframework.web.filter.AbstractRequestLoggingFilter}.
041 *
042 * @author Juergen Hoeller
043 * @author Brian Clozel
044 * @since 4.1.3
045 * @see ContentCachingResponseWrapper
046 */
047public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {
048
049        private static final String FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
050
051
052        private final ByteArrayOutputStream cachedContent;
053
054        private final Integer contentCacheLimit;
055
056        private ServletInputStream inputStream;
057
058        private BufferedReader reader;
059
060
061        /**
062         * Create a new ContentCachingRequestWrapper for the given servlet request.
063         * @param request the original servlet request
064         */
065        public ContentCachingRequestWrapper(HttpServletRequest request) {
066                super(request);
067                int contentLength = request.getContentLength();
068                this.cachedContent = new ByteArrayOutputStream(contentLength >= 0 ? contentLength : 1024);
069                this.contentCacheLimit = null;
070        }
071
072        /**
073         * Create a new ContentCachingRequestWrapper for the given servlet request.
074         * @param request the original servlet request
075         * @param contentCacheLimit the maximum number of bytes to cache per request
076         * @since 4.3.6
077         * @see #handleContentOverflow(int)
078         */
079        public ContentCachingRequestWrapper(HttpServletRequest request, int contentCacheLimit) {
080                super(request);
081                this.cachedContent = new ByteArrayOutputStream(contentCacheLimit);
082                this.contentCacheLimit = contentCacheLimit;
083        }
084
085
086        @Override
087        public ServletInputStream getInputStream() throws IOException {
088                if (this.inputStream == null) {
089                        this.inputStream = new ContentCachingInputStream(getRequest().getInputStream());
090                }
091                return this.inputStream;
092        }
093
094        @Override
095        public String getCharacterEncoding() {
096                String enc = super.getCharacterEncoding();
097                return (enc != null ? enc : WebUtils.DEFAULT_CHARACTER_ENCODING);
098        }
099
100        @Override
101        public BufferedReader getReader() throws IOException {
102                if (this.reader == null) {
103                        this.reader = new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));
104                }
105                return this.reader;
106        }
107
108        @Override
109        public String getParameter(String name) {
110                if (this.cachedContent.size() == 0 && isFormPost()) {
111                        writeRequestParametersToCachedContent();
112                }
113                return super.getParameter(name);
114        }
115
116        @Override
117        public Map<String, String[]> getParameterMap() {
118                if (this.cachedContent.size() == 0 && isFormPost()) {
119                        writeRequestParametersToCachedContent();
120                }
121                return super.getParameterMap();
122        }
123
124        @Override
125        public Enumeration<String> getParameterNames() {
126                if (this.cachedContent.size() == 0 && isFormPost()) {
127                        writeRequestParametersToCachedContent();
128                }
129                return super.getParameterNames();
130        }
131
132        @Override
133        public String[] getParameterValues(String name) {
134                if (this.cachedContent.size() == 0 && isFormPost()) {
135                        writeRequestParametersToCachedContent();
136                }
137                return super.getParameterValues(name);
138        }
139
140
141        private boolean isFormPost() {
142                String contentType = getContentType();
143                return (contentType != null && contentType.contains(FORM_CONTENT_TYPE) &&
144                                HttpMethod.POST.matches(getMethod()));
145        }
146
147        private void writeRequestParametersToCachedContent() {
148                try {
149                        if (this.cachedContent.size() == 0) {
150                                String requestEncoding = getCharacterEncoding();
151                                Map<String, String[]> form = super.getParameterMap();
152                                for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext(); ) {
153                                        String name = nameIterator.next();
154                                        List<String> values = Arrays.asList(form.get(name));
155                                        for (Iterator<String> valueIterator = values.iterator(); valueIterator.hasNext(); ) {
156                                                String value = valueIterator.next();
157                                                this.cachedContent.write(URLEncoder.encode(name, requestEncoding).getBytes());
158                                                if (value != null) {
159                                                        this.cachedContent.write('=');
160                                                        this.cachedContent.write(URLEncoder.encode(value, requestEncoding).getBytes());
161                                                        if (valueIterator.hasNext()) {
162                                                                this.cachedContent.write('&');
163                                                        }
164                                                }
165                                        }
166                                        if (nameIterator.hasNext()) {
167                                                this.cachedContent.write('&');
168                                        }
169                                }
170                        }
171                }
172                catch (IOException ex) {
173                        throw new IllegalStateException("Failed to write request parameters to cached content", ex);
174                }
175        }
176
177        /**
178         * Return the cached request content as a byte array.
179         * <p>The returned array will never be larger than the content cache limit.
180         * @see #ContentCachingRequestWrapper(HttpServletRequest, int)
181         */
182        public byte[] getContentAsByteArray() {
183                return this.cachedContent.toByteArray();
184        }
185
186        /**
187         * Template method for handling a content overflow: specifically, a request
188         * body being read that exceeds the specified content cache limit.
189         * <p>The default implementation is empty. Subclasses may override this to
190         * throw a payload-too-large exception or the like.
191         * @param contentCacheLimit the maximum number of bytes to cache per request
192         * which has just been exceeded
193         * @since 4.3.6
194         * @see #ContentCachingRequestWrapper(HttpServletRequest, int)
195         */
196        protected void handleContentOverflow(int contentCacheLimit) {
197        }
198
199
200        private class ContentCachingInputStream extends ServletInputStream {
201
202                private final ServletInputStream is;
203
204                private boolean overflow = false;
205
206                public ContentCachingInputStream(ServletInputStream is) {
207                        this.is = is;
208                }
209
210                @Override
211                public int read() throws IOException {
212                        int ch = this.is.read();
213                        if (ch != -1 && !this.overflow) {
214                                if (contentCacheLimit != null && cachedContent.size() == contentCacheLimit) {
215                                        this.overflow = true;
216                                        handleContentOverflow(contentCacheLimit);
217                                }
218                                else {
219                                        cachedContent.write(ch);
220                                }
221                        }
222                        return ch;
223                }
224        }
225
226}