View Javadoc
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  
20  package org.apache.commons.compress.utils;
21  
22  import java.io.IOException;
23  import java.nio.ByteBuffer;
24  import java.nio.channels.ClosedChannelException;
25  import java.nio.channels.SeekableByteChannel;
26  import java.util.Arrays;
27  import java.util.concurrent.atomic.AtomicBoolean;
28  
29  /**
30   * A {@link SeekableByteChannel} implementation that wraps a byte[].
31   * <p>
32   * When this channel is used for writing an internal buffer grows to accommodate incoming data. The natural size limit is the value of {@link Integer#MAX_VALUE}
33   * and it is not possible to {@link #position(long) set the position} or {@link #truncate truncate} to a value bigger than that. Internal buffer can be accessed
34   * via {@link SeekableInMemoryByteChannel#array()}.
35   * </p>
36   *
37   * @since 1.13
38   * @NotThreadSafe
39   */
40  public class SeekableInMemoryByteChannel implements SeekableByteChannel {
41  
42      private static final int NAIVE_RESIZE_LIMIT = Integer.MAX_VALUE >> 1;
43  
44      private byte[] data;
45      private final AtomicBoolean closed = new AtomicBoolean();
46      private int position;
47      private int size;
48  
49      /**
50       * Constructs a new instance using a default empty buffer.
51       */
52      public SeekableInMemoryByteChannel() {
53          this(ByteUtils.EMPTY_BYTE_ARRAY);
54      }
55  
56      /**
57       * Constructs a new instance from a byte array.
58       * <p>
59       * This constructor is intended to be used with pre-allocated buffer or when reading from a given byte array.
60       * </p>
61       *
62       * @param data input data or pre-allocated array.
63       */
64      public SeekableInMemoryByteChannel(final byte[] data) {
65          this.data = data;
66          this.size = data.length;
67      }
68  
69      /**
70       * Constructs a new instance from a size of storage to be allocated.
71       * <p>
72       * Creates a channel and allocates internal storage of a given size.
73       * </p>
74       *
75       * @param size size of internal buffer to allocate, in bytes.
76       */
77      public SeekableInMemoryByteChannel(final int size) {
78          this(new byte[size]);
79      }
80  
81      /**
82       * Obtains the array backing this channel.
83       * <p>
84       * NOTE: The returned buffer is not aligned with containing data, use {@link #size()} to obtain the size of data stored in the buffer.
85       * </p>
86       *
87       * @return internal byte array.
88       */
89      public byte[] array() {
90          return data;
91      }
92  
93      @Override
94      public void close() {
95          closed.set(true);
96      }
97  
98      private void ensureOpen() throws ClosedChannelException {
99          if (!isOpen()) {
100             throw new ClosedChannelException();
101         }
102     }
103 
104     @Override
105     public boolean isOpen() {
106         return !closed.get();
107     }
108 
109     /**
110      * Returns this channel's position.
111      * <p>
112      * This method violates the contract of {@link SeekableByteChannel#position()} as it will not throw any exception when invoked on a closed channel. Instead
113      * it will return the position the channel had when close has been called.
114      * </p>
115      */
116     @Override
117     public long position() {
118         return position;
119     }
120 
121     @Override
122     public SeekableByteChannel position(final long newPosition) throws IOException {
123         ensureOpen();
124         if (newPosition < 0L || newPosition > Integer.MAX_VALUE) {
125             throw new IOException("Position must be in range [0.." + Integer.MAX_VALUE + "]");
126         }
127         position = (int) newPosition;
128         return this;
129     }
130 
131     @Override
132     public int read(final ByteBuffer buf) throws IOException {
133         ensureOpen();
134         int wanted = buf.remaining();
135         final int possible = size - position;
136         if (possible <= 0) {
137             return -1;
138         }
139         if (wanted > possible) {
140             wanted = possible;
141         }
142         buf.put(data, position, wanted);
143         position += wanted;
144         return wanted;
145     }
146 
147     private void resize(final int newLength) {
148         int len = data.length;
149         if (len <= 0) {
150             len = 1;
151         }
152         if (newLength < NAIVE_RESIZE_LIMIT) {
153             while (len < newLength) {
154                 len <<= 1;
155             }
156         } else { // avoid overflow
157             len = newLength;
158         }
159         data = Arrays.copyOf(data, len);
160     }
161 
162     /**
163      * Returns the current size of entity to which this channel is connected.
164      * <p>
165      * This method violates the contract of {@link SeekableByteChannel#size} as it will not throw any exception when invoked on a closed channel. Instead it
166      * will return the size the channel had when close has been called.
167      * </p>
168      */
169     @Override
170     public long size() {
171         return size;
172     }
173 
174     /**
175      * Truncates the entity, to which this channel is connected, to the given size.
176      * <p>
177      * This method violates the contract of {@link SeekableByteChannel#truncate} as it will not throw any exception when invoked on a closed channel.
178      * </p>
179      *
180      * @throws IllegalArgumentException if size is negative or bigger than the maximum of a Java integer
181      */
182     @Override
183     public SeekableByteChannel truncate(final long newSize) {
184         if (newSize < 0L || newSize > Integer.MAX_VALUE) {
185             throw new IllegalArgumentException("Size must be range [0.." + Integer.MAX_VALUE + "]");
186         }
187         if (size > newSize) {
188             size = (int) newSize;
189         }
190         if (position > newSize) {
191             position = (int) newSize;
192         }
193         return this;
194     }
195 
196     @Override
197     public int write(final ByteBuffer b) throws IOException {
198         ensureOpen();
199         int wanted = b.remaining();
200         final int possibleWithoutResize = size - position;
201         if (wanted > possibleWithoutResize) {
202             final int newSize = position + wanted;
203             if (newSize < 0) { // overflow
204                 resize(Integer.MAX_VALUE);
205                 wanted = Integer.MAX_VALUE - position;
206             } else {
207                 resize(newSize);
208             }
209         }
210         b.get(data, position, wanted);
211         position += wanted;
212         if (size < position) {
213             size = position;
214         }
215         return wanted;
216     }
217 
218 }