/* * (C) Copyright 2011 Nuxeo SA (http://nuxeo.com/) and others. * * 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. * * Contributors: * Florent Guillaume */ package org.nuxeo.ecm.core.io.download; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import javax.servlet.ServletOutputStream; import org.apache.commons.io.IOUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.runtime.api.Framework; /** * A {@link ServletOutputStream} that buffers everything until {@link #stopBuffering()} is called. * <p> * There may only be one such instance per thread. * <p> * Buffering is done first in memory, then on disk if the size exceeds a limit. */ public class BufferingServletOutputStream extends ServletOutputStream { private static final Log log = LogFactory.getLog(BufferingServletOutputStream.class); /** Initial memory buffer size. */ public static final int INITIAL = 4 * 1024; // 4 KB /** Maximum memory buffer size, after this a file is used. */ public static final int MAX = 64 * 1024; // 64 KB /** Used for 0-length writes. */ private final static OutputStream EMPTY = new ByteArrayOutputStream(0); /** Have we stopped buffering to pass writes directly to the output stream. */ protected boolean streaming; protected boolean needsFlush; protected boolean needsClose; protected final OutputStream outputStream; protected PrintWriter writer; protected ByteArrayOutputStream memory; protected OutputStream file; protected File tmp; /** * A {@link ServletOutputStream} wrapper that buffers everything until {@link #stopBuffering()} is called. * <p> * {@link #stopBuffering()} <b>MUST</b> be called in a {@code finally} statement in order for resources to be closed * properly. * * @param outputStream the underlying output stream */ public BufferingServletOutputStream(OutputStream outputStream) { this.outputStream = outputStream; } public PrintWriter getWriter() { if (writer == null) { writer = new PrintWriter(new OutputStreamWriter(this)); } return writer; } /** * Finds the proper output stream where we can write {@code len} bytes. */ protected OutputStream getOutputStream(int len) throws IOException { if (streaming) { return outputStream; } if (len == 0) { return EMPTY; } if (file != null) { // already to file return file; } int total; if (memory == null) { // no buffer yet if (len <= MAX) { memory = new ByteArrayOutputStream(Math.max(INITIAL, len)); return memory; } total = len; } else { total = memory.size() + len; } if (total <= MAX) { return memory; } else { // switch to a file createTempFile(); file = new BufferedOutputStream(new FileOutputStream(tmp)); if (memory != null) { memory.writeTo(file); memory = null; } return file; } } protected void createTempFile() throws IOException { tmp = Framework.createTempFile("nxout", null); } @Override public void write(int b) throws IOException { getOutputStream(1).write(b); } @Override public void write(byte[] b) throws IOException { getOutputStream(b.length).write(b); } @Override public void write(byte[] b, int off, int len) throws IOException { getOutputStream(len).write(b, off, len); } /** * This implementation does nothing, we still want to keep buffering and not flush. * <p> * {@inheritDoc} */ @Override public void flush() throws IOException { if (streaming) { outputStream.flush(); } else { needsFlush = true; } } /** * This implementation does nothing, we still want to keep the buffer until {@link #stopBuffering()} time. * <p> * {@inheritDoc} */ @Override public void close() throws IOException { if (streaming) { outputStream.close(); } else { needsClose = true; } } /** * Writes any buffered data to the underlying {@link OutputStream} and from now on don't buffer anymore. */ public void stopBuffering() throws IOException { if (streaming) { return; } if (writer != null) { writer.flush(); // don't close, streaming needs it } streaming = true; if (log.isDebugEnabled()) { long len; if (memory != null) { len = memory.size(); } else if (file != null) { len = tmp.length(); } else { len = 0; } log.debug("buffered bytes: " + len); } boolean clientAbort = false; try { if (memory != null) { memory.writeTo(outputStream); } else if (file != null) { try { try { file.flush(); } finally { file.close(); } FileInputStream in = new FileInputStream(tmp); try { IOUtils.copy(in, outputStream); } catch (IOException e) { if (DownloadHelper.isClientAbortError(e)) { DownloadHelper.logClientAbort(e); clientAbort = true; } else { throw e; } } finally { in.close(); } } finally { tmp.delete(); } } } catch (IOException e) { if (DownloadHelper.isClientAbortError(e)) { if (!clientAbort) { DownloadHelper.logClientAbort(e); clientAbort = true; } } else { throw e; } } finally { memory = null; file = null; tmp = null; try { if (needsFlush) { outputStream.flush(); } } catch (IOException e) { if (DownloadHelper.isClientAbortError(e)) { if (!clientAbort) { DownloadHelper.logClientAbort(e); } } else { throw e; } } finally { if (needsClose) { outputStream.close(); } } } } /** * Tells the given {@link OutputStream} to stop buffering (if it was). */ public static void stopBuffering(OutputStream out) throws IOException { if (out instanceof BufferingServletOutputStream) { ((BufferingServletOutputStream) out).stopBuffering(); } } }