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.  * http://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. /**
  29.  * This class supports writing to an OutputStream or WritableByteChannel in fixed length blocks.
  30.  * <p>
  31.  * 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
  32.  * fill an entire block, the output will be padded to a full block size.
  33.  * </p>
  34.  *
  35.  * <p>
  36.  * 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
  37.  * to such devices, as the padding serves no useful purpose in such cases.
  38.  * </p>
  39.  *
  40.  * <p>
  41.  * This class should normally wrap a FileOutputStream or associated WritableByteChannel directly. If there is an intervening filter that modified the output,
  42.  * such as a CompressorOutputStream, or performs its own buffering, such as BufferedOutputStream, output to the device may no longer be of the specified size.
  43.  * </p>
  44.  *
  45.  * <p>
  46.  * Any content written to this stream should be self-delimiting and should tolerate any padding added to fill the last block.
  47.  * </p>
  48.  *
  49.  * @since 1.15
  50.  */
  51. public class FixedLengthBlockOutputStream extends OutputStream implements WritableByteChannel {

  52.     /**
  53.      * 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
  54.      * 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
  55.      * simple class to take care of it.
  56.      */
  57.     private static final class BufferAtATimeOutputChannel implements WritableByteChannel {

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

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

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

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

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

  81.             try {
  82.                 final int pos = buffer.position();
  83.                 final int len = buffer.limit() - pos;
  84.                 out.write(buffer.array(), buffer.arrayOffset() + pos, len);
  85.                 buffer.position(buffer.limit());
  86.                 return len;
  87.             } catch (final IOException e) {
  88.                 try {
  89.                     close();
  90.                 } catch (final IOException ignored) { // NOSONAR
  91.                 }
  92.                 throw e;
  93.             }
  94.         }

  95.     }

  96.     private final WritableByteChannel out;
  97.     private final int blockSize;
  98.     private final ByteBuffer buffer;

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

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

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

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

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

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

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

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

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

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

  197.     @Override
  198.     public int write(final ByteBuffer src) throws IOException {
  199.         if (!isOpen()) {
  200.             throw new ClosedChannelException();
  201.         }
  202.         final int srcRemaining = src.remaining();

  203.         if (srcRemaining < buffer.remaining()) {
  204.             // if we don't have enough bytes in src to fill up a block we must buffer
  205.             buffer.put(src);
  206.         } else {
  207.             int srcLeft = srcRemaining;
  208.             final int savedLimit = src.limit();
  209.             // If we're not at the start of buffer, we have some bytes already buffered
  210.             // fill up the reset of buffer and write the block.
  211.             if (buffer.position() != 0) {
  212.                 final int n = buffer.remaining();
  213.                 src.limit(src.position() + n);
  214.                 buffer.put(src);
  215.                 writeBlock();
  216.                 srcLeft -= n;
  217.             }
  218.             // whilst we have enough bytes in src for complete blocks,
  219.             // write them directly from src without copying them to buffer
  220.             while (srcLeft >= blockSize) {
  221.                 src.limit(src.position() + blockSize);
  222.                 out.write(src);
  223.                 srcLeft -= blockSize;
  224.             }
  225.             // copy any remaining bytes into buffer
  226.             src.limit(savedLimit);
  227.             buffer.put(src);
  228.         }
  229.         return srcRemaining;
  230.     }

  231.     @Override
  232.     public void write(final int b) throws IOException {
  233.         if (!isOpen()) {
  234.             throw new ClosedChannelException();
  235.         }
  236.         buffer.put((byte) b);
  237.         maybeFlush();
  238.     }

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

  249. }