/**
* 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.BufferedOutputStream;
import java.io.FilterInputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter;
import org.springframework.remoting.support.RemoteInvocation;
import org.springframework.remoting.support.RemoteInvocationResult;
/**
* Extends <code>HttpInvokerServiceExporter</code> to allow
* <code>InputStream</code> parameters to remote service methods and
* <code>InputStream</code> return values from remote service methods. See
* the documentation for
* <code>StreamSupportingHttpInvokerProxyFactoryBean</code> for important
* restrictions and usage information. Also see
* <code>HttpInvokerServiceExporter</code> for general usage of the exporter
* facility.
*
* @author Andy DePue
* @since 1.2.3
* @see StreamSupportingHttpInvokerProxyFactoryBean
* @see HttpInvokerServiceExporter
*/
public class StreamSupportingHttpInvokerServiceExporter extends HttpInvokerServiceExporter {
private static final Log log = LogFactory.getLog(StreamSupportingHttpInvokerServiceExporter.class);
public StreamSupportingHttpInvokerServiceExporter() {
setRegisterTraceInterceptor(false);
}
private boolean emptyInputStreamParameterBeforeReturn = false;
//
// METHODS FROM CLASS HttpInvokerServiceExporter
//
protected RemoteInvocation readRemoteInvocation(final HttpServletRequest request, final InputStream is) throws IOException, ClassNotFoundException {
final RemoteInvocation ret = super.readRemoteInvocation(request, new StreamSupportingHttpInvokerRequestExecutor.CloseShieldedInputStream(is));
boolean closeIs = true;
if (ret instanceof StreamSupportingRemoteInvocation) {
final StreamSupportingRemoteInvocation ssri = (StreamSupportingRemoteInvocation) ret;
if (ssri.getInputStreamParam() >= 0 && !ssri.isInputStreamParamNull()) {
ssri.getArguments()[ssri.getInputStreamParam()] = new ParameterInputStream(is);
closeIs = false;
}
}
if (closeIs) {
is.close();
}
return ret;
}
protected void writeRemoteInvocationResult(final HttpServletRequest request, final HttpServletResponse response, final RemoteInvocationResult result) throws IOException {
if (hasStreamResult(result)) {
response.setContentType(StreamSupportingHttpInvokerRequestExecutor.CONTENT_TYPE_SERIALIZED_OBJECT_WITH_STREAM);
} else {
response.setContentType(CONTENT_TYPE_SERIALIZED_OBJECT);
}
writeRemoteInvocationResult(request, response, result, response.getOutputStream());
}
protected void writeRemoteInvocationResult(final HttpServletRequest request, final HttpServletResponse response, final RemoteInvocationResult result, final OutputStream os) throws IOException {
if (hasStreamResult(result)) {
final OutputStream decoratedOut = decorateOutputStream(request, response, os);
response.setHeader("Transfer-Encoding", "chunked");
try {
// We want to be able to close the ObjectOutputStream in order to
// properly flush and clear it out, but we don't want it closing
// our underlying OutputStream.
final ObjectOutputStream oos = new ObjectOutputStream(new CloseShieldedOutputStream(new BufferedOutputStream(decoratedOut, 4096)));
try {
doWriteRemoteInvocationResult(result, oos);
oos.flush();
} finally {
oos.close();
}
doWriteReturnInputStream((StreamSupportingRemoteInvocationResult) result, decoratedOut);
} finally {
decoratedOut.close();
}
} else {
super.writeRemoteInvocationResult(request, response, result, os);
}
}
protected RemoteInvocationResult invokeAndCreateResult(final RemoteInvocation invocation, final Object targetObject) {
try {
final Object value = invoke(invocation, targetObject);
if (invocation instanceof StreamSupportingRemoteInvocation) {
final Boolean closedInputStreamParam = getParameterInputStreamClosedFlag(invocation);
if (value instanceof InputStream) {
return new StreamSupportingRemoteInvocationResult((InputStream) value, closedInputStreamParam);
} else {
return new StreamSupportingRemoteInvocationResult(value, closedInputStreamParam);
}
} else {
return new RemoteInvocationResult(value);
}
} catch (Throwable ex) {
if (invocation instanceof StreamSupportingRemoteInvocation) {
return new StreamSupportingRemoteInvocationResult(ex, getParameterInputStreamClosedFlag(invocation));
} else {
if (log.isWarnEnabled()) {
log.warn(ex.getCause().getMessage());
}
if (log.isDebugEnabled()) {
log.debug(ex);
}
if (ex.getCause() != null) {
return new RemoteInvocationResult(ex.getCause());
} else {
return new RemoteInvocationResult(new Exception(ex.getMessage()));
}
}
} finally {
final ParameterInputStream pi = getParameterInputStreamFrom(invocation);
if (pi != null) {
try {
pi.doRealClose(getEmptyInputStreamParameterBeforeReturn());
} catch (IOException e) {
log.warn("Error while attempting to close InputStream parameter for RemoteInvocation '" + invocation + "'", e);
}
}
}
}
//
// HELPER METHODS
//
/**
* See {@link #setEmptyInputStreamParameterBeforeReturn(boolean)}.
*
* @return <code>true</code> if any InputStream parameter should be
* "emptied" before sending the response to the client.
* @see #setEmptyInputStreamParameterBeforeReturn(boolean)
*/
public boolean getEmptyInputStreamParameterBeforeReturn() {
return this.emptyInputStreamParameterBeforeReturn;
}
/**
* Determines if this servlet should "empty" any InputStream parameter to a
* service method before returning to the client. This is provided as a
* workaround for some servlet containers in order to ensure that if an
* exception is thrown or the service method returns before the InputStream
* parameter is read that the client will not block trying to send the
* remaining InputStream to the server. This means that in the face of an
* exception or early return from a method that the client will still finish
* uploading all of its data before it becomes aware of the situation,
* taking up unnecessary time and bandwidth. Because of this, a better
* solution should be found to this problem in the future. This property
* defaults to <code>false</code>.
*
* @param emptyInputStreamParameterBeforeReturn
*
*
* @see #getEmptyInputStreamParameterBeforeReturn()
*/
public void setEmptyInputStreamParameterBeforeReturn(final boolean emptyInputStreamParameterBeforeReturn) {
this.emptyInputStreamParameterBeforeReturn = emptyInputStreamParameterBeforeReturn;
}
protected boolean hasStreamResult(final RemoteInvocationResult result) {
return result instanceof StreamSupportingRemoteInvocationResult && ((StreamSupportingRemoteInvocationResult) result).getHasReturnStream();
}
protected void doWriteReturnInputStream(final StreamSupportingRemoteInvocationResult result, final OutputStream unbufferedChunkedOut) throws IOException {
// We use the unbuffered chunked out with a custom buffer for optimum
// performance - partly because we can't be sure that the returned
// InputStream is itself buffered.
final InputStream isResult = result.getServerSideInputStream();
if (isResult != null) {
try {
final byte[] buffer = new byte[4096];
int read;
while ((read = isResult.read(buffer)) != -1) {
unbufferedChunkedOut.write(buffer, 0, read);
}
} finally {
result.setServerSideInputStream(null);
isResult.close();
}
}
}
protected ParameterInputStream getParameterInputStreamFrom(final RemoteInvocation invocation) {
if (invocation instanceof StreamSupportingRemoteInvocation) {
final StreamSupportingRemoteInvocation ssri = (StreamSupportingRemoteInvocation) invocation;
if (ssri.getInputStreamParam() >= 0 && !ssri.isInputStreamParamNull()) {
return (ParameterInputStream) ssri.getArguments()[ssri.getInputStreamParam()];
}
}
return null;
}
protected Boolean getParameterInputStreamClosedFlag(final RemoteInvocation invocation) {
final ParameterInputStream pi = getParameterInputStreamFrom(invocation);
if (pi != null) {
return pi.isClosed() ? Boolean.TRUE : Boolean.FALSE;
} else {
return null;
}
}
/**
* Shields an underlying OutputStream from being closed.
*/
public static class CloseShieldedOutputStream extends FilterOutputStream {
public CloseShieldedOutputStream(final OutputStream out) {
super(out);
}
public void close() throws IOException {
flush();
}
}
/**
* Tracks if an InputStream parameter is closed by a service method, if
* any input method threw an exception during operation, and if the
* service method read the InputStream to the end-of-stream. Also provides
* the ability to optionally read an InputStream to end-of-stream if the
* service method did not.
*/
public static class ParameterInputStream extends FilterInputStream {
private boolean fullyRead = false;
private boolean erroredOut = false;
private boolean closed = false;
public ParameterInputStream(final InputStream in) {
super(in);
}
public boolean isFullyRead() {
return this.fullyRead;
}
public boolean isErroredOut() {
return this.erroredOut;
}
public boolean isClosed() {
return this.closed;
}
public void doRealClose(final boolean emptyStream) throws IOException {
if (!isClosed()) {
if (log.isDebugEnabled()) log.debug("Service method failed to close InputStream parameter from remote invocation. Will perform the close anyway.");
}
if (!isFullyRead() && emptyStream && !isErroredOut()) {
final byte[] buf = new byte[4096];
//noinspection StatementWithEmptyBody
while (read(buf) != -1);
}
super.close();
}
protected int checkEos(final int read) {
if (read == -1) {
this.fullyRead = true;
}
return read;
}
protected IOException checkException(final IOException ioe) {
this.erroredOut = true;
return ioe;
}
protected void assertOpen() throws IOException {
if (this.closed) {
throw new IOException("Stream closed");
}
}
//
// METHODS FROM CLASS FilterInputStream
//
public int read() throws IOException {
assertOpen();
try {
return checkEos(super.read());
} catch (IOException e) {
throw checkException(e);
}
}
public int read(byte b[]) throws IOException {
assertOpen();
try {
return checkEos(super.read(b));
} catch (IOException e) {
throw checkException(e);
}
}
public int read(byte b[], int off, int len) throws IOException {
assertOpen();
try {
return checkEos(super.read(b, off, len));
} catch (IOException e) {
throw checkException(e);
}
}
public long skip(long n) throws IOException {
assertOpen();
try {
return super.skip(n);
} catch (IOException e) {
throw checkException(e);
}
}
public int available() throws IOException {
assertOpen();
try {
return super.available();
} catch (IOException e) {
throw checkException(e);
}
}
public void close() throws IOException {
// Close will happen later.
this.closed = true;
}
}
}