/** * Copyright (c) 2009 Juwi MacMillan Group GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.tizzit.util.spring.httpinvoker; import java.io.*; import org.apache.commons.httpclient.Cookie; import org.apache.commons.httpclient.HttpState; import org.apache.commons.httpclient.cookie.CookiePolicy; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor; import org.springframework.remoting.httpinvoker.HttpInvokerClientConfiguration; import org.springframework.remoting.rmi.CodebaseAwareObjectInputStream; import org.springframework.remoting.support.RemoteInvocation; import org.springframework.remoting.support.RemoteInvocationResult; /** * <p>An HttpInvokerRequestExecutor that supports InputStream return types from * remote methods, as well as (at most one) InputStream parameter. If no * InputStream return type or parameter is present for a particular remote * invocation then this class will delegate the invocation to * CommonsHttpInvokerRequestExecutor, otherwise it will enable * "chunking" on the PostMethod being used and append the * InputStream to the end of the post body (after the serialized * RemoteInvocation), in the case of an InputStream parameter. For * InputStream return types, this implementation relies on a corresponding * StreamSupportingHttpInvokerServiceExporter (on the server side) * to append the returned InputStream content on the response to the post * method, immediately following the serialized RemoteInvocationResult.</p> * <p>One of the major reasons for supporting InputStreams via the invoker * is to transport large amounts of data across the wire (often more data * than what is reasonable to fit into memory), therefore care is taken to * ensure that the InputStream is not fully buffered or read into memory, * but rather streamed as it is read across the wire.</p> * <p>See <code>StreamSupportingHttpInvokerProxyFactoryBean</code> for more * detail.</p> * * @author Andy DePue * @since 1.2.3 * @see StreamSupportingHttpInvokerProxyFactoryBean * @see org.springframework.remoting.httpinvoker.HttpInvokerRequestExecutor * @see org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor * @see org.springframework.remoting.support.RemoteInvocation * @see org.springframework.remoting.support.RemoteInvocationResult * @see org.apache.commons.httpclient.methods.PostMethod * @see com.marathon.util.spring.StreamSupportingHttpInvokerServiceExporter * @see java.io.InputStream */ public class StreamSupportingHttpInvokerRequestExecutor extends CommonsHttpInvokerRequestExecutor { private static final Log log = LogFactory.getLog(StreamSupportingHttpInvokerRequestExecutor.class); public static final String CONTENT_TYPE_SERIALIZED_OBJECT_WITH_STREAM = "application/x-java-serialized-object-with-stream"; // // METHODS FROM CLASS CommonsHttpInvokerRequestExecutor // // It sure would have been nice to override executeRequest(...), // BUT, AbstractHttpInvokerRequestExecutor implements // executeRequest(...) as final, and since the final // executeRequest(...) makes some poor assumptions (such as the assumption // that doExecuteRequest has no need of the original RemoteInvocation), we // are forced to either employ some hack (such as creating a custom // ByteArrayOutputStream) OR duplicating a ton of code. I decided to employ // the hack. protected ByteArrayOutputStream getByteArrayOutputStream(final RemoteInvocation invocation) throws IOException { // The constant is private in AbstractHttpInvokerRequestExecutor. final WorkaroundByteArrayOutputStream baos = new WorkaroundByteArrayOutputStream(1024, invocation); writeRemoteInvocation(invocation, baos); return baos; } protected RemoteInvocationResult doExecuteRequest(final HttpInvokerClientConfiguration config, final ByteArrayOutputStream baos) throws IOException, ClassNotFoundException { final WorkaroundByteArrayOutputStream wbaos = (WorkaroundByteArrayOutputStream) baos; RemoteInvocationResult result = null; if (wbaos.getRemoteInvocation() instanceof StreamSupportingRemoteInvocation) { result = doExecuteRequest(config, wbaos, (StreamSupportingRemoteInvocation) wbaos.getRemoteInvocation()); } else { PostMethod postMethod = createPostMethod(config); try { setRequestBody(config, postMethod, baos); executePostMethod(config, getHttpClient(), postMethod); validateResponse(config, postMethod); InputStream responseBody = getResponseBody(config, postMethod); result = readRemoteInvocationResult(responseBody, config.getCodebaseUrl()); } finally { // Need to explicitly release because it might be pooled. postMethod.releaseConnection(); } } return result; } protected RemoteInvocationResult readRemoteInvocationResult(final InputStream is, final String codebaseUrl) throws IOException, ClassNotFoundException { final RemoteInvocationResult ret = super.readRemoteInvocationResult(is, codebaseUrl); if (!(ret instanceof StreamSupportingRemoteInvocationResult)) { is.close(); } return ret; } protected ObjectInputStream createObjectInputStream(final InputStream is, final String codebaseUrl) throws IOException { return new SourceStreamPreservingObjectInputStream(is, codebaseUrl); } protected RemoteInvocationResult doReadRemoteInvocationResult(final ObjectInputStream ois) throws IOException, ClassNotFoundException { final RemoteInvocationResult source = super.doReadRemoteInvocationResult(ois); if (source instanceof StreamSupportingRemoteInvocationResult) { ((StreamSupportingRemoteInvocationResult) source).setClientSideInputStream(((SourceStreamPreservingObjectInputStream) ois).getSourceInputStream()); } return source; } // // HELPER METHODS // /** * Execute a request to send the given serialized remote invocation. * <p>Implementations will usually call <code>readRemoteInvocationResult</code> * to deserialize a returned RemoteInvocationResult object. * * @param config the HTTP invoker configuration that specifies the target * service * @param baos the ByteArrayOutputStream that contains the serialized * RemoteInvocation object * * @return the RemoteInvocationResult object * * @throws IOException if thrown by I/O operations * @throws ClassNotFoundException if thrown during deserialization * @see #readRemoteInvocationResult(java.io.InputStream, String) */ protected RemoteInvocationResult doExecuteRequest(final HttpInvokerClientConfiguration config, final ByteArrayOutputStream baos, final StreamSupportingRemoteInvocation invocation) throws IOException, ClassNotFoundException { final ByteArrayInputStream serializedInvocation = new ByteArrayInputStream(baos.toByteArray()); final PostMethod postMethod; final InputStream body; if (invocation.getClientSideInputStream() != null) { // We don't want to close the client side input stream unless the remote // method closes the input stream, so we "shield" the close for now. body = new CompositeInputStream(new InputStream[] {serializedInvocation, new CloseShieldedInputStream(invocation.getClientSideInputStream())}); postMethod = createPostMethodForStreaming(config); } else { body = serializedInvocation; postMethod = createPostMethod(config); } boolean delayReleaseConnection = false; try { postMethod.setRequestBody(body); executePostMethod(config, getHttpClient(), postMethod); HttpState state = getHttpClient().getState(); /*postMethod.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY); String host = targetURL.getHost(); String path = targetURL.getPath(); boolean secure = ("https".equalsIgnoreCase(targetURL.getProtocol())) ? true : false; //$NON-NLS-1$ String ck1 = (String) postMethod.getParams().g msgContext.getProperty(HTTPConstants.HEADER_COOKIE); String ck2 = (String) msgContext.getProperty(HTTPConstants.HEADER_COOKIE2); if (ck1 != null) { int index = ck1.indexOf('='); state.addCookie(new Cookie(host, ck1.substring(0, index), ck1.substring(index + 1), path, null, secure)); } if (ck2 != null) { int index = ck2.indexOf('='); state.addCookie(new Cookie(host, ck2.substring(0, index), ck2.substring(index + 1), path, null, secure)); } httpClient.setState(state); */ final RemoteInvocationResult ret = readRemoteInvocationResult(postMethod.getResponseBodyAsStream(), config.getCodebaseUrl()); if (ret instanceof StreamSupportingRemoteInvocationResult) { final StreamSupportingRemoteInvocationResult ssrir = (StreamSupportingRemoteInvocationResult) ret; // Close the local InputStream parameter if the remote method // explicitly closed the InputStream parameter on the other side. if (invocation.getClientSideInputStream() != null) { if (ssrir.getMethodClosedParamInputStream() != null) { if (Boolean.TRUE.equals(ssrir.getMethodClosedParamInputStream())) { invocation.getClientSideInputStream().close(); } } else { warnInputStreamParameterStateNotSpecified(invocation); } } // If there is a return stream, then we need to leave the PostMethod // connection open until the return stream is closed, so augment the // return stream for this. if (ssrir.getHasReturnStream()) { final InputStream sourceRetIs = ssrir.getClientSideInputStream(); if (sourceRetIs != null) { ssrir.setClientSideInputStream(new FilterInputStream(sourceRetIs) { public void close() throws IOException { super.close(); postMethod.releaseConnection(); } }); delayReleaseConnection = true; } } } else if (invocation.getClientSideInputStream() != null) { warnInputStreamParameterStateNotSpecified(invocation); } return ret; } finally { // need to explicitly release because it might be pooled if (!delayReleaseConnection) { postMethod.releaseConnection(); } } } private void warnInputStreamParameterStateNotSpecified(final StreamSupportingRemoteInvocation invocation) { log.warn("Remote method invocation with InputStream parameter did not indicate if remote method closed the InputStream parameter! Will leave the stream open. RemoteInvocation: " + invocation); } protected PostMethod createPostMethodForStreaming(final HttpInvokerClientConfiguration config) throws IOException { final PostMethod postMethod = new PostMethod(config.getServiceUrl()); postMethod.setRequestHeader(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_SERIALIZED_OBJECT_WITH_STREAM); postMethod.setRequestContentLength(PostMethod.CONTENT_LENGTH_CHUNKED); return postMethod; } // // INNER CLASSES // /** * Works around the "final" executeRequest(...) method and sneaks in the * RemoteInvocation reference. */ public static class WorkaroundByteArrayOutputStream extends ByteArrayOutputStream { private final RemoteInvocation remoteInvocation; public WorkaroundByteArrayOutputStream(final int size, final RemoteInvocation remoteInvocation) { super(size); this.remoteInvocation = remoteInvocation; } public RemoteInvocation getRemoteInvocation() { return this.remoteInvocation; } } /** * Prevents the source InputStream from being closed, but allows the * ObjectInputStream to go through the motions of closing the stream so * that its internal state is properly cleared. */ public static class SourceStreamPreservingObjectInputStream extends CodebaseAwareObjectInputStream { private InputStream sourceInputStream; public SourceStreamPreservingObjectInputStream(final InputStream in, final String codebaseUrl) throws IOException { // Prevent the source InputStream from being closed when the // ObjectInputStream is closed. We do it this way rather than // overriding close() to ensure that ObjectInputStream has a chance to // clear out itself when close() is called. super(new CloseShieldedInputStream(in), codebaseUrl); this.sourceInputStream = in; } public InputStream getSourceInputStream() { return this.sourceInputStream; } } /** * Shields an underlying InputStream from being closed. */ public static class CloseShieldedInputStream extends FilterInputStream { public CloseShieldedInputStream(final InputStream in) { super(in); } public void close() throws IOException { } } /** * Allows multiple InputStreams to be composited into a single InputStream. */ public static class CompositeInputStream extends FilterInputStream { private InputStream[] inputStreams; private int currentInputStreamIdx; public CompositeInputStream(final InputStream[] inputStreams) { super(inputStreams[0]); this.inputStreams = inputStreams; this.currentInputStreamIdx = 0; } public InputStream[] getInputStreams() { return this.inputStreams; } public int getCurrentInputStreamIdx() { return this.currentInputStreamIdx; } protected InputStream incCurrentInputStream() throws IOException { if ((++this.currentInputStreamIdx) >= getInputStreams().length) { return null; } else { this.in.close(); return this.in = getInputStreams()[this.currentInputStreamIdx]; } } public int read() throws IOException { final int read = super.read(); if (read == -1) { if (incCurrentInputStream() == null) { return -1; } else { return read(); } } else { return read; } } public int read(byte b[]) throws IOException { final int read = super.read(b); if (read == -1) { if (incCurrentInputStream() == null) { return -1; } else { return read(b); } } else { return read; } } public int read(byte b[], int off, int len) throws IOException { final int read = super.read(b, off, len); if (read == -1) { if (incCurrentInputStream() == null) { return -1; } else { return read(b, off, len); } } else { return read; } } public void close() throws IOException { // All InputStreams preceeding the current one have already been closed. // Be sure to close all InputStreams following the current one as well. final InputStream[] inputStreams = getInputStreams(); for (int i = getCurrentInputStreamIdx(); i < inputStreams.length; i++) { inputStreams[i].close(); } } } }