001: /*
002: * Copyright 2002-2007 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: * http://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:
017: package org.springframework.remoting.httpinvoker;
018:
019: import java.io.ByteArrayOutputStream;
020: import java.io.IOException;
021: import java.io.InputStream;
022: import java.util.zip.GZIPInputStream;
023:
024: import org.apache.commons.httpclient.Header;
025: import org.apache.commons.httpclient.HttpClient;
026: import org.apache.commons.httpclient.HttpException;
027: import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
028: import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
029: import org.apache.commons.httpclient.methods.PostMethod;
030:
031: import org.springframework.remoting.support.RemoteInvocationResult;
032:
033: /**
034: * {@link HttpInvokerRequestExecutor} implementation that uses
035: * <a href="http://jakarta.apache.org/commons/httpclient">Jakarta Commons HttpClient</a>
036: * to execute POST requests. Requires Commons HttpClient 3.0 or higher.
037: *
038: * <p>Allows to use a pre-configured {@link org.apache.commons.httpclient.HttpClient}
039: * instance, potentially with authentication, HTTP connection pooling, etc.
040: * Also designed for easy subclassing, providing specific template methods.
041: *
042: * @author Juergen Hoeller
043: * @author Mark Fisher
044: * @since 1.1
045: * @see SimpleHttpInvokerRequestExecutor
046: */
047: public class CommonsHttpInvokerRequestExecutor extends
048: AbstractHttpInvokerRequestExecutor {
049:
050: /**
051: * Default timeout value if no HttpClient is explicitly provided.
052: */
053: private static final int DEFAULT_READ_TIMEOUT_MILLISECONDS = (60 * 1000);
054:
055: private HttpClient httpClient;
056:
057: /**
058: * Create a new CommonsHttpInvokerRequestExecutor with a default
059: * HttpClient that uses a default MultiThreadedHttpConnectionManager.
060: * Sets the socket read timeout to {@link #DEFAULT_READ_TIMEOUT_MILLISECONDS}.
061: * @see org.apache.commons.httpclient.HttpClient
062: * @see org.apache.commons.httpclient.MultiThreadedHttpConnectionManager
063: */
064: public CommonsHttpInvokerRequestExecutor() {
065: this .httpClient = new HttpClient(
066: new MultiThreadedHttpConnectionManager());
067: this .setReadTimeout(DEFAULT_READ_TIMEOUT_MILLISECONDS);
068: }
069:
070: /**
071: * Create a new CommonsHttpInvokerRequestExecutor with the given
072: * HttpClient instance. The socket read timeout of the provided
073: * HttpClient will not be changed.
074: * @param httpClient the HttpClient instance to use for this request executor
075: */
076: public CommonsHttpInvokerRequestExecutor(HttpClient httpClient) {
077: this .httpClient = httpClient;
078: }
079:
080: /**
081: * Set the HttpClient instance to use for this request executor.
082: */
083: public void setHttpClient(HttpClient httpClient) {
084: this .httpClient = httpClient;
085: }
086:
087: /**
088: * Return the HttpClient instance that this request executor uses.
089: */
090: public HttpClient getHttpClient() {
091: return this .httpClient;
092: }
093:
094: /**
095: * Set the socket read timeout for the underlying HttpClient. A value
096: * of 0 means <emphasis>never</emphasis> timeout.
097: * @param timeout the timeout value in milliseconds
098: * @see org.apache.commons.httpclient.params.HttpConnectionManagerParams#setSoTimeout(int)
099: * @see #DEFAULT_READ_TIMEOUT_MILLISECONDS
100: */
101: public void setReadTimeout(int timeout) {
102: if (timeout < 0) {
103: throw new IllegalArgumentException(
104: "timeout must be a non-negative value");
105: }
106: this .httpClient.getHttpConnectionManager().getParams()
107: .setSoTimeout(timeout);
108: }
109:
110: /**
111: * Execute the given request through Commons HttpClient.
112: * <p>This method implements the basic processing workflow:
113: * The actual work happens in this class's template methods.
114: * @see #createPostMethod
115: * @see #setRequestBody
116: * @see #executePostMethod
117: * @see #validateResponse
118: * @see #getResponseBody
119: */
120: protected RemoteInvocationResult doExecuteRequest(
121: HttpInvokerClientConfiguration config,
122: ByteArrayOutputStream baos) throws IOException,
123: ClassNotFoundException {
124:
125: PostMethod postMethod = createPostMethod(config);
126: try {
127: setRequestBody(config, postMethod, baos);
128: executePostMethod(config, getHttpClient(), postMethod);
129: validateResponse(config, postMethod);
130: InputStream responseBody = getResponseBody(config,
131: postMethod);
132: return readRemoteInvocationResult(responseBody, config
133: .getCodebaseUrl());
134: } finally {
135: // Need to explicitly release because it might be pooled.
136: postMethod.releaseConnection();
137: }
138: }
139:
140: /**
141: * Create a PostMethod for the given configuration.
142: * <p>The default implementation creates a standard PostMethod with
143: * "application/x-java-serialized-object" as "Content-Type" header.
144: * @param config the HTTP invoker configuration that specifies the
145: * target service
146: * @return the PostMethod instance
147: * @throws IOException if thrown by I/O methods
148: */
149: protected PostMethod createPostMethod(
150: HttpInvokerClientConfiguration config) throws IOException {
151: PostMethod postMethod = new PostMethod(config.getServiceUrl());
152: if (isAcceptGzipEncoding()) {
153: postMethod.addRequestHeader(HTTP_HEADER_ACCEPT_ENCODING,
154: ENCODING_GZIP);
155: }
156: return postMethod;
157: }
158:
159: /**
160: * Set the given serialized remote invocation as request body.
161: * <p>The default implementation simply sets the serialized invocation
162: * as the PostMethod's request body. This can be overridden, for example,
163: * to write a specific encoding and potentially set appropriate HTTP
164: * request headers.
165: * @param config the HTTP invoker configuration that specifies the target service
166: * @param postMethod the PostMethod to set the request body on
167: * @param baos the ByteArrayOutputStream that contains the serialized
168: * RemoteInvocation object
169: * @throws IOException if thrown by I/O methods
170: * @see org.apache.commons.httpclient.methods.PostMethod#setRequestBody(java.io.InputStream)
171: * @see org.apache.commons.httpclient.methods.PostMethod#setRequestEntity
172: * @see org.apache.commons.httpclient.methods.InputStreamRequestEntity
173: */
174: protected void setRequestBody(
175: HttpInvokerClientConfiguration config,
176: PostMethod postMethod, ByteArrayOutputStream baos)
177: throws IOException {
178:
179: postMethod.setRequestEntity(new ByteArrayRequestEntity(baos
180: .toByteArray(), getContentType()));
181: }
182:
183: /**
184: * Execute the given PostMethod instance.
185: * @param config the HTTP invoker configuration that specifies the target service
186: * @param httpClient the HttpClient to execute on
187: * @param postMethod the PostMethod to execute
188: * @throws IOException if thrown by I/O methods
189: * @see org.apache.commons.httpclient.HttpClient#executeMethod(org.apache.commons.httpclient.HttpMethod)
190: */
191: protected void executePostMethod(
192: HttpInvokerClientConfiguration config,
193: HttpClient httpClient, PostMethod postMethod)
194: throws IOException {
195:
196: httpClient.executeMethod(postMethod);
197: }
198:
199: /**
200: * Validate the given response as contained in the PostMethod object,
201: * throwing an exception if it does not correspond to a successful HTTP response.
202: * <p>Default implementation rejects any HTTP status code beyond 2xx, to avoid
203: * parsing the response body and trying to deserialize from a corrupted stream.
204: * @param config the HTTP invoker configuration that specifies the target service
205: * @param postMethod the executed PostMethod to validate
206: * @throws IOException if validation failed
207: * @see org.apache.commons.httpclient.methods.PostMethod#getStatusCode()
208: * @see org.apache.commons.httpclient.HttpException
209: */
210: protected void validateResponse(
211: HttpInvokerClientConfiguration config, PostMethod postMethod)
212: throws IOException {
213:
214: if (postMethod.getStatusCode() >= 300) {
215: throw new HttpException(
216: "Did not receive successful HTTP response: status code = "
217: + postMethod.getStatusCode()
218: + ", status message = ["
219: + postMethod.getStatusText() + "]");
220: }
221: }
222:
223: /**
224: * Extract the response body from the given executed remote invocation
225: * request.
226: * <p>The default implementation simply fetches the PostMethod's response
227: * body stream. If the response is recognized as GZIP response, the
228: * InputStream will get wrapped in a GZIPInputStream.
229: * @param config the HTTP invoker configuration that specifies the target service
230: * @param postMethod the PostMethod to read the response body from
231: * @return an InputStream for the response body
232: * @throws IOException if thrown by I/O methods
233: * @see #isGzipResponse
234: * @see java.util.zip.GZIPInputStream
235: * @see org.apache.commons.httpclient.methods.PostMethod#getResponseBodyAsStream()
236: * @see org.apache.commons.httpclient.methods.PostMethod#getResponseHeader(String)
237: */
238: protected InputStream getResponseBody(
239: HttpInvokerClientConfiguration config, PostMethod postMethod)
240: throws IOException {
241:
242: if (isGzipResponse(postMethod)) {
243: return new GZIPInputStream(postMethod
244: .getResponseBodyAsStream());
245: } else {
246: return postMethod.getResponseBodyAsStream();
247: }
248: }
249:
250: /**
251: * Determine whether the given response indicates a GZIP response.
252: * <p>Default implementation checks whether the HTTP "Content-Encoding"
253: * header contains "gzip" (in any casing).
254: * @param postMethod the PostMethod to check
255: * @return whether the given response indicates a GZIP response
256: */
257: protected boolean isGzipResponse(PostMethod postMethod) {
258: Header encodingHeader = postMethod
259: .getResponseHeader(HTTP_HEADER_CONTENT_ENCODING);
260: return (encodingHeader != null
261: && encodingHeader.getValue() != null && encodingHeader
262: .getValue().toLowerCase().indexOf(ENCODING_GZIP) != -1);
263: }
264:
265: }
|