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.remoting.httpinvoker;
018
019import java.io.ByteArrayOutputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.net.HttpURLConnection;
023import java.net.URL;
024import java.net.URLConnection;
025import java.util.Locale;
026import java.util.zip.GZIPInputStream;
027
028import org.springframework.context.i18n.LocaleContext;
029import org.springframework.context.i18n.LocaleContextHolder;
030import org.springframework.remoting.support.RemoteInvocationResult;
031import org.springframework.util.StringUtils;
032
033/**
034 * {@link org.springframework.remoting.httpinvoker.HttpInvokerRequestExecutor} implementation
035 * that uses standard Java facilities to execute POST requests, without support for HTTP
036 * authentication or advanced configuration options.
037 *
038 * <p>Designed for easy subclassing, customizing specific template methods. However,
039 * consider {@code HttpComponentsHttpInvokerRequestExecutor} for more sophisticated needs:
040 * The standard {@link HttpURLConnection} class is rather limited in its capabilities.
041 *
042 * @author Juergen Hoeller
043 * @since 1.1
044 * @see java.net.HttpURLConnection
045 */
046public class SimpleHttpInvokerRequestExecutor extends AbstractHttpInvokerRequestExecutor {
047
048        private int connectTimeout = -1;
049
050        private int readTimeout = -1;
051
052
053        /**
054         * Set the underlying URLConnection's connect timeout (in milliseconds).
055         * A timeout value of 0 specifies an infinite timeout.
056         * <p>Default is the system's default timeout.
057         * @see URLConnection#setConnectTimeout(int)
058         */
059        public void setConnectTimeout(int connectTimeout) {
060                this.connectTimeout = connectTimeout;
061        }
062
063        /**
064         * Set the underlying URLConnection's read timeout (in milliseconds).
065         * A timeout value of 0 specifies an infinite timeout.
066         * <p>Default is the system's default timeout.
067         * @see URLConnection#setReadTimeout(int)
068         */
069        public void setReadTimeout(int readTimeout) {
070                this.readTimeout = readTimeout;
071        }
072
073
074        /**
075         * Execute the given request through a standard {@link HttpURLConnection}.
076         * <p>This method implements the basic processing workflow:
077         * The actual work happens in this class's template methods.
078         * @see #openConnection
079         * @see #prepareConnection
080         * @see #writeRequestBody
081         * @see #validateResponse
082         * @see #readResponseBody
083         */
084        @Override
085        protected RemoteInvocationResult doExecuteRequest(
086                        HttpInvokerClientConfiguration config, ByteArrayOutputStream baos)
087                        throws IOException, ClassNotFoundException {
088
089                HttpURLConnection con = openConnection(config);
090                prepareConnection(con, baos.size());
091                writeRequestBody(config, con, baos);
092                validateResponse(config, con);
093                InputStream responseBody = readResponseBody(config, con);
094
095                return readRemoteInvocationResult(responseBody, config.getCodebaseUrl());
096        }
097
098        /**
099         * Open an {@link HttpURLConnection} for the given remote invocation request.
100         * @param config the HTTP invoker configuration that specifies the
101         * target service
102         * @return the HttpURLConnection for the given request
103         * @throws IOException if thrown by I/O methods
104         * @see java.net.URL#openConnection()
105         */
106        protected HttpURLConnection openConnection(HttpInvokerClientConfiguration config) throws IOException {
107                URLConnection con = new URL(config.getServiceUrl()).openConnection();
108                if (!(con instanceof HttpURLConnection)) {
109                        throw new IOException(
110                                        "Service URL [" + config.getServiceUrl() + "] does not resolve to an HTTP connection");
111                }
112                return (HttpURLConnection) con;
113        }
114
115        /**
116         * Prepare the given HTTP connection.
117         * <p>The default implementation specifies POST as method,
118         * "application/x-java-serialized-object" as "Content-Type" header,
119         * and the given content length as "Content-Length" header.
120         * @param connection the HTTP connection to prepare
121         * @param contentLength the length of the content to send
122         * @throws IOException if thrown by HttpURLConnection methods
123         * @see java.net.HttpURLConnection#setRequestMethod
124         * @see java.net.HttpURLConnection#setRequestProperty
125         */
126        protected void prepareConnection(HttpURLConnection connection, int contentLength) throws IOException {
127                if (this.connectTimeout >= 0) {
128                        connection.setConnectTimeout(this.connectTimeout);
129                }
130                if (this.readTimeout >= 0) {
131                        connection.setReadTimeout(this.readTimeout);
132                }
133
134                connection.setDoOutput(true);
135                connection.setRequestMethod(HTTP_METHOD_POST);
136                connection.setRequestProperty(HTTP_HEADER_CONTENT_TYPE, getContentType());
137                connection.setRequestProperty(HTTP_HEADER_CONTENT_LENGTH, Integer.toString(contentLength));
138
139                LocaleContext localeContext = LocaleContextHolder.getLocaleContext();
140                if (localeContext != null) {
141                        Locale locale = localeContext.getLocale();
142                        if (locale != null) {
143                                connection.setRequestProperty(HTTP_HEADER_ACCEPT_LANGUAGE, StringUtils.toLanguageTag(locale));
144                        }
145                }
146
147                if (isAcceptGzipEncoding()) {
148                        connection.setRequestProperty(HTTP_HEADER_ACCEPT_ENCODING, ENCODING_GZIP);
149                }
150        }
151
152        /**
153         * Set the given serialized remote invocation as request body.
154         * <p>The default implementation simply write the serialized invocation to the
155         * HttpURLConnection's OutputStream. This can be overridden, for example, to write
156         * a specific encoding and potentially set appropriate HTTP request headers.
157         * @param config the HTTP invoker configuration that specifies the target service
158         * @param con the HttpURLConnection to write the request body to
159         * @param baos the ByteArrayOutputStream that contains the serialized
160         * RemoteInvocation object
161         * @throws IOException if thrown by I/O methods
162         * @see java.net.HttpURLConnection#getOutputStream()
163         * @see java.net.HttpURLConnection#setRequestProperty
164         */
165        protected void writeRequestBody(
166                        HttpInvokerClientConfiguration config, HttpURLConnection con, ByteArrayOutputStream baos)
167                        throws IOException {
168
169                baos.writeTo(con.getOutputStream());
170        }
171
172        /**
173         * Validate the given response as contained in the {@link HttpURLConnection} object,
174         * throwing an exception if it does not correspond to a successful HTTP response.
175         * <p>Default implementation rejects any HTTP status code beyond 2xx, to avoid
176         * parsing the response body and trying to deserialize from a corrupted stream.
177         * @param config the HTTP invoker configuration that specifies the target service
178         * @param con the HttpURLConnection to validate
179         * @throws IOException if validation failed
180         * @see java.net.HttpURLConnection#getResponseCode()
181         */
182        protected void validateResponse(HttpInvokerClientConfiguration config, HttpURLConnection con)
183                        throws IOException {
184
185                if (con.getResponseCode() >= 300) {
186                        throw new IOException(
187                                        "Did not receive successful HTTP response: status code = " + con.getResponseCode() +
188                                        ", status message = [" + con.getResponseMessage() + "]");
189                }
190        }
191
192        /**
193         * Extract the response body from the given executed remote invocation
194         * request.
195         * <p>The default implementation simply reads the serialized invocation
196         * from the HttpURLConnection's InputStream. If the response is recognized
197         * as GZIP response, the InputStream will get wrapped in a GZIPInputStream.
198         * @param config the HTTP invoker configuration that specifies the target service
199         * @param con the HttpURLConnection to read the response body from
200         * @return an InputStream for the response body
201         * @throws IOException if thrown by I/O methods
202         * @see #isGzipResponse
203         * @see java.util.zip.GZIPInputStream
204         * @see java.net.HttpURLConnection#getInputStream()
205         * @see java.net.HttpURLConnection#getHeaderField(int)
206         * @see java.net.HttpURLConnection#getHeaderFieldKey(int)
207         */
208        protected InputStream readResponseBody(HttpInvokerClientConfiguration config, HttpURLConnection con)
209                        throws IOException {
210
211                if (isGzipResponse(con)) {
212                        // GZIP response found - need to unzip.
213                        return new GZIPInputStream(con.getInputStream());
214                }
215                else {
216                        // Plain response found.
217                        return con.getInputStream();
218                }
219        }
220
221        /**
222         * Determine whether the given response is a GZIP response.
223         * <p>Default implementation checks whether the HTTP "Content-Encoding"
224         * header contains "gzip" (in any casing).
225         * @param con the HttpURLConnection to check
226         */
227        protected boolean isGzipResponse(HttpURLConnection con) {
228                String encodingHeader = con.getHeaderField(HTTP_HEADER_CONTENT_ENCODING);
229                return (encodingHeader != null && encodingHeader.toLowerCase().contains(ENCODING_GZIP));
230        }
231
232}