001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * https://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019 020package org.apache.commons.io.channels; 021 022import java.io.ByteArrayInputStream; 023import java.io.ByteArrayOutputStream; 024import java.io.IOException; 025import java.nio.ByteBuffer; 026import java.nio.channels.ClosedChannelException; 027import java.nio.channels.NonWritableChannelException; 028import java.nio.channels.SeekableByteChannel; 029import java.nio.file.OpenOption; 030import java.nio.file.StandardOpenOption; 031import java.util.Arrays; 032import java.util.Objects; 033import java.util.concurrent.locks.ReentrantLock; 034 035import org.apache.commons.io.IOUtils; 036import org.apache.commons.io.build.AbstractStreamBuilder; 037 038/** 039 * A {@link SeekableByteChannel} implementation backed by a byte array. 040 * <p> 041 * When used for writing, the internal buffer grows to accommodate incoming data. The natural size limit is the value of {@link IOUtils#SOFT_MAX_ARRAY_LENGTH} 042 * and it's not possible to {@link #position(long) set the position} or {@link #truncate(long) truncate} to a value bigger than that. The raw internal buffer is 043 * accessed via {@link ByteArraySeekableByteChannel#array()}. 044 * </p> 045 * <p> 046 * Building a read-only channel from an existing byte array is supported with: 047 * </p> 048 * <pre>{@code 049 * try (ByteArraySeekableByteChannel channel = ByteArraySeekableByteChannel.builder() 050 * .setByteArray(...) 051 * .setOpenOptions(StandardOpenOption.READ) 052 * .get()) { 053 * // read from channel 054 * } 055 * }</pre> 056 * 057 * @since 2.21.0 058 */ 059public class ByteArraySeekableByteChannel implements SeekableByteChannel { 060 061 /** 062 * Builds for {@link ByteArraySeekableByteChannel}. 063 * <p> 064 * Building a read-only channel from an existing byte array is supported with: 065 * </p> 066 * <pre>{@code 067 * try (ByteArraySeekableByteChannel channel = ByteArraySeekableByteChannel.builder() 068 * .setByteArray(...) 069 * .setOpenOptions(StandardOpenOption.READ) 070 * .get()) { 071 * // read from channel 072 * } 073 * }</pre> 074 * 075 * @since 2.22.0 076 */ 077 public static class Builder extends AbstractStreamBuilder<ByteArraySeekableByteChannel, Builder> { 078 079 /** 080 * Constructs a new builder for {@link ByteArraySeekableByteChannel}. 081 */ 082 public Builder() { 083 setByteArray(IOUtils.EMPTY_BYTE_ARRAY); 084 } 085 086 @Override 087 public ByteArraySeekableByteChannel get() throws IOException { 088 return new ByteArraySeekableByteChannel(this); 089 } 090 } 091 092 private static final int RESIZE_LIMIT = Integer.MAX_VALUE >> 1; 093 094 /** 095 * Constructs a new builder for {@link ByteArraySeekableByteChannel}. 096 * 097 * @return a new builder for {@link ByteArraySeekableByteChannel}. 098 * @since 2.22.0 099 */ 100 public static Builder builder() { 101 return new Builder(); 102 } 103 104 /** 105 * Constructs a new channel backed directly by the given byte array. 106 * 107 * <p> 108 * The channel initially contains the full contents of the array, with its size set to {@code bytes.length} and its position set to {@code 0}. 109 * </p> 110 * 111 * <p> 112 * Reads and writes operate on the shared array. If a write operation extends beyond the current capacity, the channel will automatically allocate a larger 113 * backing array and copy the existing contents. 114 * </p> 115 * 116 * @param bytes The byte array to wrap, must not be {@code null} 117 * @return A new channel that uses the given array as its initial backing store. 118 * @throws NullPointerException If {@code bytes} is {@code null} 119 * @see #array() 120 * @see ByteArrayInputStream#ByteArrayInputStream(byte[]) 121 */ 122 public static ByteArraySeekableByteChannel wrap(final byte[] bytes) { 123 Objects.requireNonNull(bytes, "bytes"); 124 return new ByteArraySeekableByteChannel(bytes); 125 } 126 private byte[] data; 127 private volatile boolean closed; 128 private long position; 129 private int size; 130 private final boolean isWritable; 131 private final ReentrantLock lock = new ReentrantLock(); 132 133 /** 134 * Constructs a new instance, with a default internal buffer capacity. 135 * <p> 136 * The initial size and position of the channel are 0. 137 * </p> 138 * 139 * @see ByteArrayOutputStream#ByteArrayOutputStream() 140 */ 141 public ByteArraySeekableByteChannel() { 142 this(IOUtils.DEFAULT_BUFFER_SIZE); 143 } 144 145 private ByteArraySeekableByteChannel(final Builder builder) throws IOException { 146 this.data = builder.getByteArray(); 147 this.size = data.length; 148 final OpenOption[] openOptions = builder.getOpenOptions(); 149 Arrays.sort(openOptions); 150 this.isWritable = openOptions.length == 0 || Arrays.binarySearch(openOptions, StandardOpenOption.WRITE) >= 0 151 || Arrays.binarySearch(openOptions, StandardOpenOption.APPEND) >= 0; 152 } 153 154 private ByteArraySeekableByteChannel(final byte[] data) { 155 this.data = data; 156 this.size = data.length; 157 this.isWritable = true; 158 } 159 160 /** 161 * Constructs a new instance, with an internal buffer of the given capacity, in bytes. 162 * <p> 163 * The initial size and position of the channel are 0. 164 * </p> 165 * 166 * @param size Capacity of the internal buffer to allocate, in bytes. 167 * @see ByteArrayOutputStream#ByteArrayOutputStream(int) 168 */ 169 public ByteArraySeekableByteChannel(final int size) { 170 if (size < 0) { 171 throw new IllegalArgumentException("Size must be non-negative"); 172 } 173 this.data = new byte[size]; 174 this.isWritable = true; 175 } 176 177 /** 178 * Gets the raw byte array backing this channel, <em>this is not a copy</em>. 179 * <p> 180 * NOTE: The returned buffer is not aligned with containing data, use {@link #size()} to obtain the size of data stored in the buffer. 181 * </p> 182 * 183 * @return internal byte array. 184 */ 185 public byte[] array() { 186 return data; 187 } 188 189 private void checkOpen() throws ClosedChannelException { 190 if (!isOpen()) { 191 throw new ClosedChannelException(); 192 } 193 } 194 195 private void checkRange(final long newSize, final String method) { 196 if (newSize < 0L) { 197 throw new IllegalArgumentException(String.format("%s must be positive: %,d", method, newSize)); 198 } 199 } 200 201 private void checkWritable() { 202 if (!isWritable) { 203 throw new NonWritableChannelException(); 204 } 205 } 206 207 @Override 208 public void close() { 209 closed = true; 210 } 211 212 /** 213 * Like {@link #size()} but never throws {@link ClosedChannelException}. 214 * 215 * @return See {@link #size()}. 216 */ 217 public long getSize() { 218 return size; 219 } 220 221 @Override 222 public boolean isOpen() { 223 return !closed; 224 } 225 226 @Override 227 public long position() throws ClosedChannelException { 228 checkOpen(); 229 lock.lock(); 230 try { 231 return position; 232 } finally { 233 lock.unlock(); 234 } 235 } 236 237 @Override 238 public SeekableByteChannel position(final long newPosition) throws IOException { 239 checkOpen(); 240 checkRange(newPosition, "position()"); 241 lock.lock(); 242 try { 243 position = newPosition; 244 } finally { 245 lock.unlock(); 246 } 247 return this; 248 } 249 250 @Override 251 public int read(final ByteBuffer buf) throws IOException { 252 checkOpen(); 253 lock.lock(); 254 try { 255 if (position > Integer.MAX_VALUE) { 256 return IOUtils.EOF; 257 } 258 int wanted = buf.remaining(); 259 final int possible = size - (int) position; 260 if (possible <= 0) { 261 return IOUtils.EOF; 262 } 263 if (wanted > possible) { 264 wanted = possible; 265 } 266 buf.put(data, (int) position, wanted); 267 position += wanted; 268 return wanted; 269 } finally { 270 lock.unlock(); 271 } 272 } 273 274 private void resize(final int newLength) { 275 int len = data.length; 276 if (len == 0) { 277 len = 1; 278 } 279 if (newLength < RESIZE_LIMIT) { 280 while (len < newLength) { 281 len <<= 1; 282 } 283 } else { // avoid overflow 284 len = newLength; 285 } 286 data = Arrays.copyOf(data, len); 287 } 288 289 @Override 290 public long size() throws ClosedChannelException { 291 checkOpen(); 292 lock.lock(); 293 try { 294 return size; 295 } finally { 296 lock.unlock(); 297 } 298 } 299 300 /** 301 * Gets a copy of the data stored in this channel. 302 * <p> 303 * The returned array is a copy of the internal buffer, sized to the actual data stored in this channel. 304 * </p> 305 * 306 * @return a new byte array containing the data stored in this channel. 307 */ 308 public byte[] toByteArray() { 309 return Arrays.copyOf(data, size); 310 } 311 312 @Override 313 public SeekableByteChannel truncate(final long newSize) throws ClosedChannelException { 314 checkOpen(); 315 checkWritable(); 316 checkRange(newSize, "truncate()"); 317 lock.lock(); 318 try { 319 if (size > newSize) { 320 size = (int) newSize; 321 } 322 if (position > newSize) { 323 position = newSize; 324 } 325 } finally { 326 lock.unlock(); 327 } 328 return this; 329 } 330 331 @Override 332 public int write(final ByteBuffer b) throws IOException { 333 checkOpen(); 334 checkWritable(); 335 // 336 if (position > Integer.MAX_VALUE) { 337 throw new IOException("position > Integer.MAX_VALUE"); 338 } 339 lock.lock(); 340 try { 341 final int wanted = b.remaining(); 342 // intPos <= Integer.MAX_VALUE 343 final int intPos = (int) position; 344 final long newPosition = position + wanted; 345 if (newPosition > IOUtils.SOFT_MAX_ARRAY_LENGTH) { 346 throw new IOException(String.format("Requested array size %,d is too large.", newPosition)); 347 } 348 if (newPosition > size) { 349 final int newPositionInt = (int) newPosition; 350 // Ensure that newPositionInt ≤ data.length 351 resize(newPositionInt); 352 size = newPositionInt; 353 } 354 b.get(data, intPos, wanted); 355 position = newPosition; 356 if (size < intPos) { 357 size = intPos; 358 } 359 return wanted; 360 } finally { 361 lock.unlock(); 362 } 363 } 364}