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