org.nuxeo.ecm.core.io.download.BufferingServletOutputStream.java Source code

Java tutorial

Introduction

Here is the source code for org.nuxeo.ecm.core.io.download.BufferingServletOutputStream.java

Source

/*
 * (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);

    protected static ThreadLocal<BufferingServletOutputStream> threadLocal = new ThreadLocal<>();

    /** 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;
        threadLocal.set(this);
    }

    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 {
        threadLocal.remove();
        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();
        }
    }

    /**
     * Stop buffering the {@link OutputStream} for this thread (if it was).
     *
     * @since 5.5 (HF01)
     */
    public static void stopBufferingThread() throws IOException {
        @SuppressWarnings("resource")
        BufferingServletOutputStream out = threadLocal.get();
        if (out != null) {
            out.stopBuffering();
        }
    }

}