1 /*
2  * Copyright 2002-2011 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package org.springframework.remoting.httpinvoker;
18 
19 import java.io.ByteArrayOutputStream;
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.net.HttpURLConnection;
23 import java.net.URL;
24 import java.net.URLConnection;
25 import java.util.zip.GZIPInputStream;
26 
27 import org.springframework.context.i18n.LocaleContext;
28 import org.springframework.context.i18n.LocaleContextHolder;
29 import org.springframework.remoting.support.RemoteInvocationResult;
30 import org.springframework.util.StringUtils;
31 
32 /**
33  * HttpInvokerRequestExecutor implementation that uses standard J2SE facilities
34  * to execute POST requests, without support for HTTP authentication or
35  * advanced configuration options.
36  *
37  * <p>Designed for easy subclassing, customizing specific template methods.
38  * However, consider CommonsHttpInvokerRequestExecutor for more sophisticated
39  * needs: The J2SE HttpURLConnection is rather limited in its capabilities.
40  *
41  * @author Juergen Hoeller
42  * @since 1.1
43  * @see CommonsHttpInvokerRequestExecutor
44  * @see java.net.HttpURLConnection
45  */
46 public class SimpleHttpInvokerRequestExecutor extends AbstractHttpInvokerRequestExecutor {
47 
48 	private int connectTimeout = -1;
49 
50 	private int readTimeout = -1;
51 
52 
53 	/**
54 	 * Set the underlying URLConnection's connect timeout (in milliseconds).
55 	 * A timeout value of 0 specifies an infinite timeout.
56 	 * <p>Default is the system's default timeout.
57 	 * @see URLConnection#setConnectTimeout(int)
58 	 */
setConnectTimeout(int connectTimeout)59 	public void setConnectTimeout(int connectTimeout) {
60 		this.connectTimeout = connectTimeout;
61 	}
62 
63 	/**
64 	 * Set the underlying URLConnection's read timeout (in milliseconds).
65 	 * A timeout value of 0 specifies an infinite timeout.
66 	 * <p>Default is the system's default timeout.
67 	 * @see URLConnection#setReadTimeout(int)
68 	 */
setReadTimeout(int readTimeout)69 	public void setReadTimeout(int readTimeout) {
70 		this.readTimeout = readTimeout;
71 	}
72 
73 
74 	/**
75 	 * Execute the given request through a standard J2SE HttpURLConnection.
76 	 * <p>This method implements the basic processing workflow:
77 	 * The actual work happens in this class's template methods.
78 	 * @see #openConnection
79 	 * @see #prepareConnection
80 	 * @see #writeRequestBody
81 	 * @see #validateResponse
82 	 * @see #readResponseBody
83 	 */
84 	@Override
doExecuteRequest( HttpInvokerClientConfiguration config, ByteArrayOutputStream baos)85 	protected RemoteInvocationResult doExecuteRequest(
86 			HttpInvokerClientConfiguration config, ByteArrayOutputStream baos)
87 			throws IOException, ClassNotFoundException {
88 
89 		HttpURLConnection con = openConnection(config);
90 		prepareConnection(con, baos.size());
91 		writeRequestBody(config, con, baos);
92 		validateResponse(config, con);
93 		InputStream responseBody = readResponseBody(config, con);
94 
95 		return readRemoteInvocationResult(responseBody, config.getCodebaseUrl());
96 	}
97 
98 	/**
99 	 * Open an 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 	 */
openConnection(HttpInvokerClientConfiguration config)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("Service URL [" + config.getServiceUrl() + "] is not an HTTP URL");
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 	 */
prepareConnection(HttpURLConnection connection, int contentLength)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 		connection.setDoOutput(true);
133 		connection.setRequestMethod(HTTP_METHOD_POST);
134 		connection.setRequestProperty(HTTP_HEADER_CONTENT_TYPE, getContentType());
135 		connection.setRequestProperty(HTTP_HEADER_CONTENT_LENGTH, Integer.toString(contentLength));
136 		LocaleContext locale = LocaleContextHolder.getLocaleContext();
137 		if (locale != null) {
138 			connection.setRequestProperty(HTTP_HEADER_ACCEPT_LANGUAGE, StringUtils.toLanguageTag(locale.getLocale()));
139 		}
140 		if (isAcceptGzipEncoding()) {
141 			connection.setRequestProperty(HTTP_HEADER_ACCEPT_ENCODING, ENCODING_GZIP);
142 		}
143 	}
144 
145 	/**
146 	 * Set the given serialized remote invocation as request body.
147 	 * <p>The default implementation simply write the serialized invocation to the
148 	 * HttpURLConnection's OutputStream. This can be overridden, for example, to write
149 	 * a specific encoding and potentially set appropriate HTTP request headers.
150 	 * @param config the HTTP invoker configuration that specifies the target service
151 	 * @param con the HttpURLConnection to write the request body to
152 	 * @param baos the ByteArrayOutputStream that contains the serialized
153 	 * RemoteInvocation object
154 	 * @throws IOException if thrown by I/O methods
155 	 * @see java.net.HttpURLConnection#getOutputStream()
156 	 * @see java.net.HttpURLConnection#setRequestProperty
157 	 */
writeRequestBody( HttpInvokerClientConfiguration config, HttpURLConnection con, ByteArrayOutputStream baos)158 	protected void writeRequestBody(
159 			HttpInvokerClientConfiguration config, HttpURLConnection con, ByteArrayOutputStream baos)
160 			throws IOException {
161 
162 		baos.writeTo(con.getOutputStream());
163 	}
164 
165 	/**
166 	 * Validate the given response as contained in the HttpURLConnection object,
167 	 * throwing an exception if it does not correspond to a successful HTTP response.
168 	 * <p>Default implementation rejects any HTTP status code beyond 2xx, to avoid
169 	 * parsing the response body and trying to deserialize from a corrupted stream.
170 	 * @param config the HTTP invoker configuration that specifies the target service
171 	 * @param con the HttpURLConnection to validate
172 	 * @throws IOException if validation failed
173 	 * @see java.net.HttpURLConnection#getResponseCode()
174 	 */
validateResponse(HttpInvokerClientConfiguration config, HttpURLConnection con)175 	protected void validateResponse(HttpInvokerClientConfiguration config, HttpURLConnection con)
176 			throws IOException {
177 
178 		if (con.getResponseCode() >= 300) {
179 			throw new IOException(
180 					"Did not receive successful HTTP response: status code = " + con.getResponseCode() +
181 					", status message = [" + con.getResponseMessage() + "]");
182 		}
183 	}
184 
185 	/**
186 	 * Extract the response body from the given executed remote invocation
187 	 * request.
188 	 * <p>The default implementation simply reads the serialized invocation
189 	 * from the HttpURLConnection's InputStream. If the response is recognized
190 	 * as GZIP response, the InputStream will get wrapped in a GZIPInputStream.
191 	 * @param config the HTTP invoker configuration that specifies the target service
192 	 * @param con the HttpURLConnection to read the response body from
193 	 * @return an InputStream for the response body
194 	 * @throws IOException if thrown by I/O methods
195 	 * @see #isGzipResponse
196 	 * @see java.util.zip.GZIPInputStream
197 	 * @see java.net.HttpURLConnection#getInputStream()
198 	 * @see java.net.HttpURLConnection#getHeaderField(int)
199 	 * @see java.net.HttpURLConnection#getHeaderFieldKey(int)
200 	 */
readResponseBody(HttpInvokerClientConfiguration config, HttpURLConnection con)201 	protected InputStream readResponseBody(HttpInvokerClientConfiguration config, HttpURLConnection con)
202 			throws IOException {
203 
204 		if (isGzipResponse(con)) {
205 			// GZIP response found - need to unzip.
206 			return new GZIPInputStream(con.getInputStream());
207 		}
208 		else {
209 			// Plain response found.
210 			return con.getInputStream();
211 		}
212 	}
213 
214 	/**
215 	 * Determine whether the given response is a GZIP response.
216 	 * <p>Default implementation checks whether the HTTP "Content-Encoding"
217 	 * header contains "gzip" (in any casing).
218 	 * @param con the HttpURLConnection to check
219 	 */
isGzipResponse(HttpURLConnection con)220 	protected boolean isGzipResponse(HttpURLConnection con) {
221 		String encodingHeader = con.getHeaderField(HTTP_HEADER_CONTENT_ENCODING);
222 		return (encodingHeader != null && encodingHeader.toLowerCase().contains(ENCODING_GZIP));
223 	}
224 
225 }
226