/*
* Copyright 2013-2016 EMC Corporation. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0.txt
*
* or in the "license" file accompanying this file. This file 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 com.emc.ecs.sync.storage.cas;
import com.emc.ecs.sync.util.EnhancedInputStream;
import com.emc.object.util.ProgressListener;
import com.emc.object.util.ProgressOutputStream;
import com.filepool.fplibrary.FPTag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
public class BlobInputStream extends EnhancedInputStream {
private static final Logger log = LoggerFactory.getLogger(BlobInputStream.class);
private FPTag tag;
private BlobReader blobReader;
private Thread readerThread;
private Future readFuture;
public BlobInputStream(FPTag tag, int bufferSize) throws IOException {
this(tag, bufferSize, null, null);
}
public BlobInputStream(FPTag tag, int bufferSize, ExecutorService readExecutor) throws IOException {
this(tag, bufferSize, null, readExecutor);
}
public BlobInputStream(FPTag tag, int bufferSize, ProgressListener listener) throws IOException {
this(tag, bufferSize, listener, null);
}
public BlobInputStream(FPTag tag, int bufferSize, ProgressListener listener, ExecutorService readExecutor) throws IOException {
super(null);
this.tag = tag;
// piped streams and a blobReader task are necessary because of the odd stream handling in the CAS JNI wrapper
PipedInputStream pin = new PipedInputStream(bufferSize);
PipedOutputStream pout = new PipedOutputStream(pin);
try {
in = new DigestInputStream(pin, MessageDigest.getInstance("md5"));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("could not initialize MD5 digest", e);
}
OutputStream out = pout;
if (listener != null) out = new ProgressOutputStream(out, listener);
blobReader = new BlobReader(out);
if (readExecutor != null) {
readFuture = readExecutor.submit(blobReader);
} else {
readerThread = new Thread(blobReader);
readerThread.setDaemon(true);
readerThread.start();
}
}
@Override
public int read() throws IOException {
checkReader();
int result = super.read();
checkReader(); // also check after read in case we were blocked
return result;
}
@Override
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
checkReader();
int result = super.read(b, off, len);
checkReader(); // also check after read in case we were blocked
return result;
}
private void checkReader() throws IOException {
if (blobReader.isFailed()) throw new IOException("blob blobReader failed", blobReader.getError());
}
@Override
public void close() throws IOException {
try {
super.close();
} finally {
// if the blobReader is active, closing the pipe (above) will throw an exception in PipedOutputStream.write
// if it is waiting for buffer space however, it will need to be interrupted or it will be frozen indefinitely
if (!blobReader.isComplete() && !blobReader.isFailed()) {
if (readerThread != null) readerThread.interrupt();
else readFuture.cancel(true);
}
// if the blobReader is complete, this does nothing; if close was called early, this will wait until the blobReader
// thread is notified of the close (an IOException will be thrown from PipedOutputStream.write)
try {
if (readerThread != null) readerThread.join();
else readFuture.get();
} catch (Throwable t) {
log.warn("could not join blobReader thread", t);
}
}
}
public byte[] getMd5Digest() {
if (!(in instanceof DigestInputStream)) throw new UnsupportedOperationException("MD5 checksum is not enabled");
if (!isClosed()) throw new UnsupportedOperationException("cannot get MD5 until stream is closed");
return ((DigestInputStream) in).getMessageDigest().digest();
}
private class BlobReader implements Runnable {
private OutputStream out;
private volatile boolean complete = false;
private volatile boolean failed = false;
private volatile Throwable error;
BlobReader(OutputStream out) {
this.out = out;
}
@Override
public synchronized void run() {
try (OutputStream outputStream = out) {
tag.BlobRead(outputStream);
complete = true;
} catch (Throwable t) {
failed = true;
error = t;
}
}
public boolean isComplete() {
return complete;
}
public boolean isFailed() {
return failed;
}
public Throwable getError() {
return error;
}
}
}