All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.archivepatcher.applier.PartiallyCompressingOutputStream Maven / Gradle / Ivy

The newest version!
// Copyright 2016 Google Inc. 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.
// 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.

package com.google.archivepatcher.applier;

import com.google.archivepatcher.shared.IDeflater;
import com.google.archivepatcher.shared.IDeflaterOutputStream;
import com.google.archivepatcher.shared.JreDeflateParameters;
import com.google.archivepatcher.shared.TypedRange;

import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.List;
import java.util.function.BiFunction;

/**
 * An {@link OutputStream} that is pre-configured to compress some of the bytes that are written to
 * it according to the specified parameters.
 */
public class PartiallyCompressingOutputStream extends FilterOutputStream {

  /**
   * The underlying stream.
   */
  private final OutputStream normalOut;
  private final BiFunction deflaterFactory;

  /**
   * The deflater, non-null only during compression.
   */
  private IDeflater deflater = null;

  /**
   * The deflater stream, non-null only during compression.
   */
  private IDeflaterOutputStream deflaterOut = null;

  /**
   * Used when writing one byte at a time.
   */
  private final byte[] internalCopyBuffer = new byte[1];

  /**
   * The size of the buffer to use when compressing bytes.
   */
  private final int compressionBufferSize;

  /**
   * The number of bytes written so far.
   */
  private long numBytesWritten;

  /**
   * The iterator that is used to iterate over the compression ranges.
   */
  private final Iterator> rangeIterator;

  /**
   * The compress range that is either being worked on or that is coming up next.
   */
  private TypedRange nextCompressedRange = null;

  /**
   * The last {@link JreDeflateParameters} that were used.
   */
  private JreDeflateParameters lastDeflateParameters = null;

  /**
   * Creates a new stream that wraps the specified other stream, compressing the specified ranges
   * with the specified parameters. All unspecified ranges are implicitly copied without
   * modification.
   * @param compressionRanges ranges to be compressed, with accompanying parameters
   * @param out the stream to write to
   * @param compressionBufferSize the size of the buffer to use when compressing data
   */
  public PartiallyCompressingOutputStream(
      List> compressionRanges,
      OutputStream out,
      int compressionBufferSize,
      BiFunction deflaterFactory) {
    super(out);
    this.normalOut = out;
    this.compressionBufferSize = compressionBufferSize;
    this.deflaterFactory = deflaterFactory;
    rangeIterator = compressionRanges.iterator();
    if (rangeIterator.hasNext()) {
      nextCompressedRange = rangeIterator.next();
    } else {
      // Degenerate case, no compression at all/
      nextCompressedRange = null;
    }
  }

  @Override
  public void write(int b) throws IOException {
    internalCopyBuffer[0] = (byte) b;
    write(internalCopyBuffer, 0, 1);
  }

  @Override
  public void write(byte[] buffer) throws IOException {
    write(buffer, 0, buffer.length);
  }

  @Override
  public void write(byte[] buffer, int offset, int length) throws IOException {
    int writtenSoFar = 0;
    while (writtenSoFar < length) {
      writtenSoFar += writeChunk(buffer, offset + writtenSoFar, length - writtenSoFar);
    }
  }

  /**
   * Write up to length bytes from the specified buffer to the underlying stream. For
   * simplicity, this method stops at the edges of ranges; it is always either copying OR
   * compressing bytes, but never both. All manipulation of the compression state machinery is done
   * within this method. When the end of a compression range is reached it is completely flushed to
   * the output stream, to keep things as straightforward as possible.
   * @param buffer the buffer to copy/compress bytes from
   * @param offset the offset at which to start copying/compressing
   * @param length the maximum number of bytes to copy or compress
   * @return the number of bytes of the buffer that have been consumed
   */
  private int writeChunk(byte[] buffer, int offset, int length) throws IOException {
    if (bytesTillCompressionStarts() == 0 && !currentlyCompressing()) {
      // Compression will begin immediately.
      JreDeflateParameters parameters = nextCompressedRange.getMetadata();
      if (deflater == null) {
        deflater = deflaterFactory.apply(parameters.level, parameters.nowrap);
      } else if (lastDeflateParameters.nowrap != parameters.nowrap) {
        // Last deflater must be destroyed because nowrap settings do not match.
        deflater.end();
        deflater = deflaterFactory.apply(parameters.level, parameters.nowrap);
      }
      // Deflater will already have been reset at the end of this method, no need to do it again.
      // Just set up the right parameters.
      deflater.setLevel(parameters.level);
      deflater.setStrategy(parameters.strategy);
      deflaterOut = new IDeflaterOutputStream(normalOut, deflater, compressionBufferSize);
    }

    int numBytesToWrite;
    OutputStream writeTarget;
    if (currentlyCompressing()) {
      // Don't write past the end of the compressed range.
      numBytesToWrite = (int) Math.min(length, bytesTillCompressionEnds());
      writeTarget = deflaterOut;
    } else {
      writeTarget = normalOut;
      if (nextCompressedRange == null) {
        // All compression ranges have been consumed.
        numBytesToWrite = length;
      } else {
        // Don't write past the point where the next compressed range begins.
        numBytesToWrite = (int) Math.min(length, bytesTillCompressionStarts());
      }
    }

    writeTarget.write(buffer, offset, numBytesToWrite);
    numBytesWritten += numBytesToWrite;

    if (currentlyCompressing() && bytesTillCompressionEnds() == 0) {
      // Compression range complete. Finish the output and set up for the next run.
      deflaterOut.finish();
      deflaterOut.flush();
      deflaterOut = null;
      deflater.reset();
      lastDeflateParameters = nextCompressedRange.getMetadata();
      if (rangeIterator.hasNext()) {
        // More compression ranges await in the future.
        nextCompressedRange = rangeIterator.next();
      } else {
        // All compression ranges have been consumed.
        nextCompressedRange = null;
        deflater.end();
        deflater = null;
      }
    }

    return numBytesToWrite;
  }

  private boolean currentlyCompressing() {
    return deflaterOut != null;
  }

  private long bytesTillCompressionStarts() {
    if (nextCompressedRange == null) {
      // All compression ranges have been consumed
      return -1L;
    }
    return nextCompressedRange.getOffset() - numBytesWritten;
  }

  private long bytesTillCompressionEnds() {
    if (nextCompressedRange == null) {
      // All compression ranges have been consumed
      return -1L;
    }
    return (nextCompressedRange.getOffset() + nextCompressedRange.getLength()) - numBytesWritten;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy