/**
* This file is part of git-as-svn. It is subject to the license terms
* in the LICENSE file found in the top-level directory of this distribution
* and at http://www.gnu.org/licenses/gpl-2.0.html. No part of git-as-svn,
* including this file, may be copied, modified, propagated, or distributed
* except according to the terms contained in the LICENSE file.
*/
package svnserver;
import com.google.common.io.ByteStreams;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.io.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Stream for write-then-read functionality.
*
* @author Artem V. Navrotskiy
*/
public class TemporaryOutputStream extends OutputStream {
public interface Holder extends AutoCloseable {
@NotNull
Holder copy();
@Override
void close() throws IOException;
}
private interface CloseAction extends AutoCloseable {
@Override
void close() throws IOException;
}
@SuppressWarnings("MagicNumber")
private static final int MAX_MEMORY_SIZE = 8 * 1024 * 1024;
private final int maxMemorySize;
@NotNull
private final ByteArrayOutputStream memoryStream = new ByteArrayOutputStream();
@NotNull
private final Holder holder;
@Nullable
private File file;
@Nullable
private FileOutputStream fileOutputStream;
private long totalSize = 0;
public TemporaryOutputStream() {
this(MAX_MEMORY_SIZE);
}
public TemporaryOutputStream(@NotNull InputStream stream) throws IOException {
this(MAX_MEMORY_SIZE);
ByteStreams.copy(stream, this);
}
public TemporaryOutputStream(int maxMemorySize) {
this.maxMemorySize = maxMemorySize;
this.holder = new FileHolder(this::cleanup);
}
@Override
public void write(int b) throws IOException {
if (memoryStream.size() < maxMemorySize) {
memoryStream.write(b);
totalSize++;
return;
}
ensureFile().write(b);
totalSize++;
}
public long size() {
return totalSize;
}
public Holder holder() {
return holder.copy();
}
@TestOnly
@Nullable
File tempFile() {
return file;
}
@NotNull
private FileOutputStream ensureFile() throws IOException {
if (fileOutputStream == null) {
file = File.createTempFile("tmp", "");
file.deleteOnExit();
fileOutputStream = new FileOutputStream(file);
}
return fileOutputStream;
}
@Override
public void write(@NotNull byte[] b, int off, int len) throws IOException {
if (memoryStream.size() < maxMemorySize) {
final int size = Math.min(maxMemorySize - memoryStream.size(), len);
memoryStream.write(b, off, size);
if (size < len) {
ensureFile().write(b, off + size, len - size);
}
} else {
ensureFile().write(b, off, len);
}
totalSize += len;
}
@NotNull
public InputStream toInputStream() throws IOException {
if (fileOutputStream != null) {
flush();
}
if (file != null) {
return new TemporaryInputStream(memoryStream.toByteArray(), file, holder);
} else {
return new ByteArrayInputStream(memoryStream.toByteArray());
}
}
@Override
public void flush() throws IOException {
if (fileOutputStream != null) {
fileOutputStream.flush();
}
}
@Override
public void close() throws IOException {
if (fileOutputStream != null) {
fileOutputStream.close();
}
holder.close();
}
private void cleanup() throws IOException {
if (file != null && !file.delete()) {
throw new IOException("Can't delete temporary file: " + file.getAbsolutePath());
}
}
private static class TemporaryInputStream extends InputStream {
@NotNull
private final byte[] memoryBytes;
@NotNull
private final FileInputStream fileStream;
@NotNull
private final Holder holder;
private int offset = 0;
private TemporaryInputStream(@NotNull byte[] memoryBytes, @NotNull File file, @NotNull Holder holder) throws FileNotFoundException {
this.memoryBytes = memoryBytes;
this.holder = holder.copy();
this.fileStream = new FileInputStream(file);
}
@Override
public int read() throws IOException {
if (offset < memoryBytes.length) {
//noinspection MagicNumber
return memoryBytes[offset++] & 0xff;
}
return fileStream.read();
}
@Override
public int read(@NotNull byte[] buf, int off, int len) throws IOException {
if (len == 0) {
return 0;
}
if (this.offset < memoryBytes.length) {
final int count = Math.min(len, memoryBytes.length - this.offset);
System.arraycopy(memoryBytes, offset, buf, off, count);
offset += count;
return count;
}
return fileStream.read(buf, off, len);
}
@Override
public void close() throws IOException {
fileStream.close();
holder.close();
}
}
private static class FileHolder implements Holder {
@NotNull
private final CloseAction action;
@NotNull
private final AtomicInteger usages;
@NotNull
private final AtomicBoolean closed = new AtomicBoolean(false);
public FileHolder(@NotNull CloseAction action) {
this(action, new AtomicInteger(1));
}
private FileHolder(@NotNull CloseAction action, @NotNull AtomicInteger usages) {
this.action = action;
this.usages = usages;
}
@NotNull
public FileHolder copy() {
usages.incrementAndGet();
return new FileHolder(action, usages);
}
@Override
public void close() throws IOException {
if (closed.compareAndSet(false, true)) {
if (usages.decrementAndGet() == 0) {
action.close();
}
}
}
}
}