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: * @since 1.1
044: * @see SimpleHttpInvokerRequestExecutor
045: */
046: public class CommonsHttpInvokerRequestExecutor extends
047: AbstractHttpInvokerRequestExecutor {
048:
049: private HttpClient httpClient;
050:
051: /**
052: * Create a new CommonsHttpInvokerRequestExecutor with a default
053: * HttpClient that uses a default MultiThreadedHttpConnectionManager.
054: * @see org.apache.commons.httpclient.HttpClient
055: * @see org.apache.commons.httpclient.MultiThreadedHttpConnectionManager
056: */
057: public CommonsHttpInvokerRequestExecutor() {
058: this .httpClient = new HttpClient(
059: new MultiThreadedHttpConnectionManager());
060: }
061:
062: /**
063: * Create a new CommonsHttpInvokerRequestExecutor with the given
064: * HttpClient instance.
065: * @param httpClient the HttpClient instance to use for this request executor
066: */
067: public CommonsHttpInvokerRequestExecutor(HttpClient httpClient) {
068: this .httpClient = httpClient;
069: }
070:
071: /**
072: * Set the HttpClient instance to use for this request executor.
073: */
074: public void setHttpClient(HttpClient httpClient) {
075: this .httpClient = httpClient;
076: }
077:
078: /**
079: * Return the HttpClient instance that this request executor uses.
080: */
081: public HttpClient getHttpClient() {
082: return this .httpClient;
083: }
084:
085: /**
086: * Execute the given request through Commons HttpClient.
087: * <p>This method implements the basic processing workflow:
088: * The actual work happens in this class's template methods.
089: * @see #createPostMethod
090: * @see #setRequestBody
091: * @see #executePostMethod
092: * @see #validateResponse
093: * @see #getResponseBody
094: */
095: protected RemoteInvocationResult doExecuteRequest(
096: HttpInvokerClientConfiguration config,
097: ByteArrayOutputStream baos) throws IOException,
098: ClassNotFoundException {
099:
100: PostMethod postMethod = createPostMethod(config);
101: try {
102: setRequestBody(config, postMethod, baos);
103: executePostMethod(config, getHttpClient(), postMethod);
104: validateResponse(config, postMethod);
105: InputStream responseBody = getResponseBody(config,
106: postMethod);
107: return readRemoteInvocationResult(responseBody, config
108: .getCodebaseUrl());
109: } finally {
110: // Need to explicitly release because it might be pooled.
111: postMethod.releaseConnection();
112: }
113: }
114:
115: /**
116: * Create a PostMethod for the given configuration.
117: * <p>The default implementation creates a standard PostMethod with
118: * "application/x-java-serialized-object" as "Content-Type" header.
119: * @param config the HTTP invoker configuration that specifies the
120: * target service
121: * @return the PostMethod instance
122: * @throws IOException if thrown by I/O methods
123: */
124: protected PostMethod createPostMethod(
125: HttpInvokerClientConfiguration config) throws IOException {
126: PostMethod postMethod = new PostMethod(config.getServiceUrl());
127: if (isAcceptGzipEncoding()) {
128: postMethod.addRequestHeader(HTTP_HEADER_ACCEPT_ENCODING,
129: ENCODING_GZIP);
130: }
131: return postMethod;
132: }
133:
134: /**
135: * Set the given serialized remote invocation as request body.
136: * <p>The default implementation simply sets the serialized invocation
137: * as the PostMethod's request body. This can be overridden, for example,
138: * to write a specific encoding and potentially set appropriate HTTP
139: * request headers.
140: * @param config the HTTP invoker configuration that specifies the target service
141: * @param postMethod the PostMethod to set the request body on
142: * @param baos the ByteArrayOutputStream that contains the serialized
143: * RemoteInvocation object
144: * @throws IOException if thrown by I/O methods
145: * @see org.apache.commons.httpclient.methods.PostMethod#setRequestBody(java.io.InputStream)
146: * @see org.apache.commons.httpclient.methods.PostMethod#setRequestEntity
147: * @see org.apache.commons.httpclient.methods.InputStreamRequestEntity
148: */
149: protected void setRequestBody(
150: HttpInvokerClientConfiguration config,
151: PostMethod postMethod, ByteArrayOutputStream baos)
152: throws IOException {
153:
154: postMethod.setRequestEntity(new ByteArrayRequestEntity(baos
155: .toByteArray(), getContentType()));
156: }
157:
158: /**
159: * Execute the given PostMethod instance.
160: * @param config the HTTP invoker configuration that specifies the target service
161: * @param httpClient the HttpClient to execute on
162: * @param postMethod the PostMethod to execute
163: * @throws IOException if thrown by I/O methods
164: * @see org.apache.commons.httpclient.HttpClient#executeMethod(org.apache.commons.httpclient.HttpMethod)
165: */
166: protected void executePostMethod(
167: HttpInvokerClientConfiguration config,
168: HttpClient httpClient, PostMethod postMethod)
169: throws IOException {
170:
171: httpClient.executeMethod(postMethod);
172: }
173:
174: /**
175: * Validate the given response as contained in the PostMethod object,
176: * throwing an exception if it does not correspond to a successful HTTP response.
177: * <p>Default implementation rejects any HTTP status code beyond 2xx, to avoid
178: * parsing the response body and trying to deserialize from a corrupted stream.
179: * @param config the HTTP invoker configuration that specifies the target service
180: * @param postMethod the executed PostMethod to validate
181: * @throws IOException if validation failed
182: * @see org.apache.commons.httpclient.methods.PostMethod#getStatusCode()
183: * @see org.apache.commons.httpclient.HttpException
184: */
185: protected void validateResponse(
186: HttpInvokerClientConfiguration config, PostMethod postMethod)
187: throws IOException {
188:
189: if (postMethod.getStatusCode() >= 300) {
190: throw new HttpException(
191: "Did not receive successful HTTP response: status code = "
192: + postMethod.getStatusCode()
193: + ", status message = ["
194: + postMethod.getStatusText() + "]");
195: }
196: }
197:
198: /**
199: * Extract the response body from the given executed remote invocation
200: * request.
201: * <p>The default implementation simply fetches the PostMethod's response
202: * body stream. If the response is recognized as GZIP response, the
203: * InputStream will get wrapped in a GZIPInputStream.
204: * @param config the HTTP invoker configuration that specifies the target service
205: * @param postMethod the PostMethod to read the response body from
206: * @return an InputStream for the response body
207: * @throws IOException if thrown by I/O methods
208: * @see #isGzipResponse
209: * @see java.util.zip.GZIPInputStream
210: * @see org.apache.commons.httpclient.methods.PostMethod#getResponseBodyAsStream()
211: * @see org.apache.commons.httpclient.methods.PostMethod#getResponseHeader(String)
212: */
213: protected InputStream getResponseBody(
214: HttpInvokerClientConfiguration config, PostMethod postMethod)
215: throws IOException {
216:
217: if (isGzipResponse(postMethod)) {
218: return new GZIPInputStream(postMethod
219: .getResponseBodyAsStream());
220: } else {
221: return postMethod.getResponseBodyAsStream();
222: }
223: }
224:
225: /**
226: * Determine whether the given response indicates a GZIP response.
227: * <p>Default implementation checks whether the HTTP "Content-Encoding"
228: * header contains "gzip" (in any casing).
229: * @param postMethod the PostMethod to check
230: * @return whether the given response indicates a GZIP response
231: */
232: protected boolean isGzipResponse(PostMethod postMethod) {
233: Header encodingHeader = postMethod
234: .getResponseHeader(HTTP_HEADER_CONTENT_ENCODING);
235: return (encodingHeader != null
236: && encodingHeader.getValue() != null && encodingHeader
237: .getValue().toLowerCase().indexOf(ENCODING_GZIP) != -1);
238: }
239:
240: }
|