2 * Copyright (c) 2017 Brocade Communications Systems, Inc. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.controller.cluster.io;
10 import com.google.common.io.ByteSource;
11 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
12 import java.io.ByteArrayInputStream;
13 import java.io.ByteArrayOutputStream;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.io.OutputStream;
18 import java.lang.ref.Cleaner;
19 import java.lang.ref.Cleaner.Cleanable;
20 import java.nio.file.Files;
21 import org.checkerframework.checker.lock.qual.GuardedBy;
22 import org.checkerframework.checker.lock.qual.Holding;
23 import org.eclipse.jdt.annotation.NonNull;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.slf4j.Logger;
26 import org.slf4j.LoggerFactory;
29 * An {@link OutputStream} that starts buffering to a byte array, but switches to file buffering once the data
30 * reaches a configurable size. This class is thread-safe.
32 * @author Thomas Pantelis
34 public class FileBackedOutputStream extends OutputStream {
35 private static final Logger LOG = LoggerFactory.getLogger(FileBackedOutputStream.class);
38 * A Cleaner instance responsible for deleting any files which may be lost due to us not being cleaning up
41 private static final Cleaner FILE_CLEANER = Cleaner.create();
43 private final int fileThreshold;
44 private final String fileDirectory;
47 private MemoryOutputStream memory = new MemoryOutputStream();
50 private OutputStream out = memory;
56 private Cleanable fileCleanup;
59 private ByteSource source;
61 private volatile long count;
64 * Creates a new instance that uses the given file threshold, and does not reset the data when the
65 * {@link ByteSource} returned by {@link #asByteSource} is finalized.
67 * @param fileThreshold the number of bytes before the stream should switch to buffering to a file
68 * @param fileDirectory the directory in which to create the file if needed. If null, the default temp file
71 public FileBackedOutputStream(final int fileThreshold, @Nullable final String fileDirectory) {
72 this.fileThreshold = fileThreshold;
73 this.fileDirectory = fileDirectory;
77 * Returns a readable {@link ByteSource} view of the data that has been written to this stream. This stream is
78 * closed and further attempts to write to it will result in an IOException.
80 * @return a ByteSource instance
81 * @throws IOException if close fails
83 public synchronized @NonNull ByteSource asByteSource() throws IOException {
87 source = new ByteSource() {
89 public InputStream openStream() throws IOException {
90 synchronized (FileBackedOutputStream.this) {
92 return Files.newInputStream(file.toPath());
94 return new ByteArrayInputStream(memory.buf(), 0, memory.count());
110 @SuppressFBWarnings(value = "VO_VOLATILE_INCREMENT", justification = "Findbugs erroneously complains that the "
111 + "increment of count needs to be atomic even though it is inside a synchronized block.")
112 public synchronized void write(final int value) throws IOException {
113 possiblySwitchToFile(1);
119 public synchronized void write(final byte[] bytes) throws IOException {
120 write(bytes, 0, bytes.length);
124 public synchronized void write(final byte[] bytes, final int off, final int len) throws IOException {
125 possiblySwitchToFile(len);
126 out.write(bytes, off, len);
131 public synchronized void close() throws IOException {
133 OutputStream closeMe = out;
140 public synchronized void flush() throws IOException {
146 public synchronized long getCount() {
151 * Calls {@link #close} if not already closed and, if data was buffered to a file, deletes the file.
153 public synchronized void cleanup() {
154 LOG.debug("In cleanup");
156 if (fileCleanup != null) {
159 // Already deleted above
164 private void closeQuietly() {
167 } catch (IOException e) {
168 LOG.warn("Error closing output stream {}", out, e);
173 * Checks if writing {@code len} bytes would go over threshold, and switches to file buffering if so.
176 private void possiblySwitchToFile(final int len) throws IOException {
178 throw new IOException("Stream already closed");
181 if (file == null && memory.count() + len > fileThreshold) {
182 final File temp = File.createTempFile("FileBackedOutputStream", null,
183 fileDirectory == null ? null : new File(fileDirectory));
185 final Cleaner.Cleanable cleanup = FILE_CLEANER.register(this, () -> deleteFile(temp));
187 LOG.debug("Byte count {} has exceeded threshold {} - switching to file: {}", memory.count() + len,
188 fileThreshold, temp);
190 final OutputStream transfer;
192 transfer = Files.newOutputStream(temp.toPath());
194 transfer.write(memory.buf(), 0, memory.count());
196 } catch (IOException e) {
199 } catch (IOException ex) {
200 LOG.debug("Error closing temp file {}", temp, ex);
204 } catch (IOException e) {
209 // We've successfully transferred the data; switch to writing to file
212 fileCleanup = cleanup;
217 private static void deleteFile(final File file) {
218 LOG.debug("Deleting temp file {}", file);
219 if (!file.delete()) {
220 LOG.warn("Could not delete temp file {}", file);
225 * ByteArrayOutputStream that exposes its internals for efficiency.
227 private static final class MemoryOutputStream extends ByteArrayOutputStream {