/** * */ package com.emc.vipr.transform.compression; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.emc.vipr.transform.TransformConstants; import com.emc.vipr.transform.compression.CompressionTransformFactory.LzmaProfile; import com.emc.vipr.transform.encryption.KeyUtils; import com.emc.vipr.transform.util.CountingOutputStream; import SevenZip.Compression.LZMA.Encoder; /** * @author cwikj * */ public class LZMAOutputStream extends OutputStream implements CompressionStream, Runnable { private static final Logger log = LoggerFactory.getLogger(LZMAOutputStream.class); private CountingOutputStream compressedOutput; private Thread compressionThread; private InputStream inputPipe; private CountingOutputStream uncompressedSize; private DigestOutputStream outputPipe; private boolean closed; private Encoder lzma; private Exception compressionFailure; private byte[] uncompressedDigest; static ThreadGroup LZ_COMP_TG = new ThreadGroup("LZMACompress"); public LZMAOutputStream(OutputStream out, LzmaProfile compressionProfile) throws IOException { compressedOutput = new CountingOutputStream(out); closed = false; uncompressedDigest = new byte[0]; // The LZMA Encoder requires an input stream and thus does not make a good // filter. We need to create a pipe and and use an auxiliary thread to compress // the data. inputPipe = new PipedInputStream(); uncompressedSize = new CountingOutputStream(new PipedOutputStream((PipedInputStream) inputPipe)); try { outputPipe = new DigestOutputStream(uncompressedSize, MessageDigest.getInstance("SHA1")); } catch (NoSuchAlgorithmException e) { throw new IOException("Could not create LZMAOutputStream", e); } lzma = new Encoder(); lzma.SetDictionarySize(compressionProfile.dictionarySize); lzma.SetNumFastBytes(compressionProfile.fastBytes); lzma.SetMatchFinder(compressionProfile.matchFinder); lzma.SetLcLpPb(compressionProfile.lc, compressionProfile.lp, compressionProfile.pb); lzma.SetEndMarkerMode(true); lzma.WriteCoderProperties(compressedOutput); compressionThread = new Thread(LZ_COMP_TG, this); compressionThread.start(); } public LZMAOutputStream(OutputStream out, int compressionLevel) throws IOException { this(out, CompressionTransformFactory.LZMA_COMPRESSION_PROFILE[compressionLevel]); } /* * @see com.emc.vipr.transform.compression.CompressionOutputStream#getStreamMetadata() */ @Override public Map<String, String> getStreamMetadata() { if(!closed) { throw new IllegalStateException("Stream must be closed before getting metadata"); } Map<String,String> metadata = new HashMap<String, String>(); long compSize = compressedOutput.getByteCount(); long uncompSize = uncompressedSize.getByteCount(); String compRatioString = String.format("%.1f%%", 100.0 - (compSize*100.0/uncompSize)); metadata.put(TransformConstants.META_COMPRESSION_UNCOMP_SIZE, ""+uncompSize); metadata.put(TransformConstants.META_COMPRESSION_COMP_SIZE, ""+compSize); metadata.put(TransformConstants.META_COMPRESSION_COMP_RATIO, ""+compRatioString); metadata.put(TransformConstants.META_COMPRESSION_UNCOMP_SHA1, KeyUtils.toHexPadded(uncompressedDigest)); return metadata; } @Override public void write(int b) throws IOException { outputPipe.write(b); } @Override public void close() throws IOException { if(closed) { return; } outputPipe.flush(); outputPipe.close(); closed = true; // Wait for encoder to finish try { compressionThread.join(); } catch (InterruptedException e) { throw new IOException("Error waiting for compression thread to exit", e); } compressedOutput.close(); uncompressedDigest = outputPipe.getMessageDigest().digest(); // Free the encoder lzma = null; } @Override public void flush() throws IOException { writeCheck(); outputPipe.flush(); } @Override public void write(byte[] b, int off, int len) throws IOException { writeCheck(); outputPipe.write(b, off, len); } @Override public void write(byte[] b) throws IOException { writeCheck(); write(b, 0, b.length); } @Override public void run() { // Start compressing data try { lzma.Code(inputPipe, compressedOutput, -1, -1, null); } catch(Exception e) { compressionFailure(e); } } private synchronized void compressionFailure(Exception e) { compressionFailure = e; log.error("Error compressing data", e); } private synchronized void writeCheck() throws IOException { if(compressionFailure != null) { throw new IOException("Error during stream compression", compressionFailure); } if(closed) { throw new IOException("Stream closed"); } } }