FixedLengthBlockOutputStream.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one
  3.  * or more contributor license agreements.  See the NOTICE file
  4.  * distributed with this work for additional information
  5.  * regarding copyright ownership.  The ASF licenses this file
  6.  * to you under the Apache License, Version 2.0 (the
  7.  * "License"); you may not use this file except in compliance
  8.  * with the License.  You may obtain a copy of the License at
  9.  *
  10.  *   https://www.apache.org/licenses/LICENSE-2.0
  11.  *
  12.  * Unless required by applicable law or agreed to in writing,
  13.  * software distributed under the License is distributed on an
  14.  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15.  * KIND, either express or implied.  See the License for the
  16.  * specific language governing permissions and limitations
  17.  * under the License.
  18.  */
  19. package org.apache.commons.compress.utils;

  20. import java.io.FileOutputStream;
  21. import java.io.IOException;
  22. import java.io.OutputStream;
  23. import java.nio.ByteBuffer;
  24. import java.nio.ByteOrder;
  25. import java.nio.channels.ClosedChannelException;
  26. import java.nio.channels.WritableByteChannel;
  27. import java.util.concurrent.atomic.AtomicBoolean;

  28. import org.apache.commons.io.IOUtils;

  29. /**
  30.  * This class supports writing to an OutputStream or WritableByteChannel in fixed length blocks.
  31.  * <p>
  32.  * It can be be used to support output to devices such as tape drives that require output in this format. If the final block does not have enough content to
  33.  * fill an entire block, the output will be padded to a full block size.
  34.  * </p>
  35.  *
  36.  * <p>
  37.  * This class can be used to support TAR,PAX, and CPIO blocked output to character special devices. It is not recommended that this class be used unless writing
  38.  * to such devices, as the padding serves no useful purpose in such cases.
  39.  * </p>
  40.  *
  41.  * <p>
  42.  * This class should normally wrap a FileOutputStream or associated WritableByteChannel directly. If there is an intervening filter that modified the output,
  43.  * such as a CompressorOutputStream, or performs its own buffering, such as BufferedOutputStream, output to the device may no longer be of the specified size.
  44.  * </p>
  45.  *
  46.  * <p>
  47.  * Any content written to this stream should be self-delimiting and should tolerate any padding added to fill the last block.
  48.  * </p>
  49.  *
  50.  * @since 1.15
  51.  */
  52. public class FixedLengthBlockOutputStream extends OutputStream implements WritableByteChannel {

  53.     /**
  54.      * Helper class to provide channel wrapper for arbitrary output stream that doesn't alter the size of writes. We can't use Channels.newChannel, because for
  55.      * non FileOutputStreams, it breaks up writes into 8KB max chunks. Since the purpose of this class is to always write complete blocks, we need to write a
  56.      * simple class to take care of it.
  57.      */
  58.     private static final class BufferAtATimeOutputChannel implements WritableByteChannel {

  59.         private final OutputStream out;
  60.         private final AtomicBoolean closed = new AtomicBoolean();

  61.         private BufferAtATimeOutputChannel(final OutputStream out) {
  62.             this.out = out;
  63.         }

  64.         @Override
  65.         public void close() throws IOException {
  66.             if (closed.compareAndSet(false, true)) {
  67.                 out.close();
  68.             }
  69.         }

  70.         @Override
  71.         public boolean isOpen() {
  72.             return !closed.get();
  73.         }

  74.         @Override
  75.         public int write(final ByteBuffer buffer) throws IOException {
  76.             if (!isOpen()) {
  77.                 throw new ClosedChannelException();
  78.             }
  79.             if (!buffer.hasArray()) {
  80.                 throw new IOException("Direct buffer somehow written to BufferAtATimeOutputChannel");
  81.             }

  82.             try {
  83.                 final int pos = buffer.position();
  84.                 final int len = buffer.limit() - pos;
  85.                 out.write(buffer.array(), buffer.arrayOffset() + pos, len);
  86.                 buffer.position(buffer.limit());
  87.                 return len;
  88.             } catch (final IOException e) {
  89.                 IOUtils.closeQuietly(this);
  90.                 throw e;
  91.             }
  92.         }

  93.     }

  94.     private final WritableByteChannel out;
  95.     private final int blockSize;
  96.     private final ByteBuffer buffer;

  97.     private final AtomicBoolean closed = new AtomicBoolean();

  98.     /**
  99.      * Constructs a fixed length block output stream with given destination stream and block size.
  100.      *
  101.      * @param os        The stream to wrap.
  102.      * @param blockSize The block size to use.
  103.      */
  104.     public FixedLengthBlockOutputStream(final OutputStream os, final int blockSize) {
  105.         if (os instanceof FileOutputStream) {
  106.             final FileOutputStream fileOutputStream = (FileOutputStream) os;
  107.             out = fileOutputStream.getChannel();
  108.             buffer = ByteBuffer.allocateDirect(blockSize);
  109.         } else {
  110.             out = new BufferAtATimeOutputChannel(os);
  111.             buffer = ByteBuffer.allocate(blockSize);
  112.         }
  113.         this.blockSize = blockSize;
  114.     }

  115.     /**
  116.      * Constructs a fixed length block output stream with given destination writable byte channel and block size.
  117.      *
  118.      * @param out       The writable byte channel to wrap.
  119.      * @param blockSize The block size to use.
  120.      */
  121.     public FixedLengthBlockOutputStream(final WritableByteChannel out, final int blockSize) {
  122.         this.out = out;
  123.         this.blockSize = blockSize;
  124.         this.buffer = ByteBuffer.allocateDirect(blockSize);
  125.     }

  126.     @Override
  127.     public void close() throws IOException {
  128.         if (closed.compareAndSet(false, true)) {
  129.             try {
  130.                 flushBlock();
  131.             } finally {
  132.                 out.close();
  133.             }
  134.         }
  135.     }

  136.     /**
  137.      * Potentially pads and then writes the current block to the underlying stream.
  138.      *
  139.      * @throws IOException if writing fails
  140.      */
  141.     public void flushBlock() throws IOException {
  142.         if (buffer.position() != 0) {
  143.             padBlock();
  144.             writeBlock();
  145.         }
  146.     }

  147.     @Override
  148.     public boolean isOpen() {
  149.         if (!out.isOpen()) {
  150.             closed.set(true);
  151.         }
  152.         return !closed.get();
  153.     }

  154.     private void maybeFlush() throws IOException {
  155.         if (!buffer.hasRemaining()) {
  156.             writeBlock();
  157.         }
  158.     }

  159.     private void padBlock() {
  160.         buffer.order(ByteOrder.nativeOrder());
  161.         int bytesToWrite = buffer.remaining();
  162.         if (bytesToWrite > 8) {
  163.             final int align = buffer.position() & 7;
  164.             if (align != 0) {
  165.                 final int limit = 8 - align;
  166.                 for (int i = 0; i < limit; i++) {
  167.                     buffer.put((byte) 0);
  168.                 }
  169.                 bytesToWrite -= limit;
  170.             }

  171.             while (bytesToWrite >= 8) {
  172.                 buffer.putLong(0L);
  173.                 bytesToWrite -= 8;
  174.             }
  175.         }
  176.         while (buffer.hasRemaining()) {
  177.             buffer.put((byte) 0);
  178.         }
  179.     }

  180.     @Override
  181.     public void write(final byte[] b, final int offset, final int length) throws IOException {
  182.         if (!isOpen()) {
  183.             throw new ClosedChannelException();
  184.         }
  185.         int off = offset;
  186.         int len = length;
  187.         while (len > 0) {
  188.             final int n = Math.min(len, buffer.remaining());
  189.             buffer.put(b, off, n);
  190.             maybeFlush();
  191.             len -= n;
  192.             off += n;
  193.         }
  194.     }

  195.     @Override
  196.     public int write(final ByteBuffer src) throws IOException {
  197.         if (!isOpen()) {
  198.             throw new ClosedChannelException();
  199.         }
  200.         final int srcRemaining = src.remaining();
  201.         if (srcRemaining >= buffer.remaining()) {
  202.             int srcLeft = srcRemaining;
  203.             final int savedLimit = src.limit();
  204.             // If we're not at the start of buffer, we have some bytes already buffered
  205.             // fill up the reset of buffer and write the block.
  206.             if (buffer.position() != 0) {
  207.                 final int n = buffer.remaining();
  208.                 src.limit(src.position() + n);
  209.                 buffer.put(src);
  210.                 writeBlock();
  211.                 srcLeft -= n;
  212.             }
  213.             // whilst we have enough bytes in src for complete blocks,
  214.             // write them directly from src without copying them to buffer
  215.             while (srcLeft >= blockSize) {
  216.                 src.limit(src.position() + blockSize);
  217.                 out.write(src);
  218.                 srcLeft -= blockSize;
  219.             }
  220.             // copy any remaining bytes into buffer
  221.             src.limit(savedLimit);
  222.         }
  223.         // if we don't have enough bytes in src to fill up a block we must buffer
  224.         buffer.put(src);
  225.         return srcRemaining;
  226.     }

  227.     @Override
  228.     public void write(final int b) throws IOException {
  229.         if (!isOpen()) {
  230.             throw new ClosedChannelException();
  231.         }
  232.         buffer.put((byte) b);
  233.         maybeFlush();
  234.     }

  235.     private void writeBlock() throws IOException {
  236.         buffer.flip();
  237.         final int i = out.write(buffer);
  238.         final boolean hasRemaining = buffer.hasRemaining();
  239.         if (i != blockSize || hasRemaining) {
  240.             throw new IOException(String.format("Failed to write %,d bytes atomically. Only wrote  %,d", blockSize, i));
  241.         }
  242.         buffer.clear();
  243.     }

  244. }