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.SeekableByteChannel; 028import java.util.Arrays; 029import java.util.Objects; 030import java.util.concurrent.locks.ReentrantLock; 031 032import org.apache.commons.io.IOUtils; 033 034/** 035 * A {@link SeekableByteChannel} implementation backed by a byte array. 036 * <p> 037 * 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} 038 * 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 039 * accessed via {@link ByteArraySeekableByteChannel#array()}. 040 * </p> 041 * 042 * @since 2.21.0 043 */ 044public class ByteArraySeekableByteChannel implements SeekableByteChannel { 045 046 private static final int RESIZE_LIMIT = Integer.MAX_VALUE >> 1; 047 048 /** 049 * Constructs a new channel backed directly by the given byte array. 050 * 051 * <p>The channel initially contains the full contents of the array, with its 052 * size set to {@code bytes.length} and its position set to {@code 0}.</p> 053 * 054 * <p>Reads and writes operate on the shared array. 055 * If a write operation extends beyond the current capacity, the channel will 056 * automatically allocate a larger backing array and copy the existing contents.</p> 057 * 058 * @param bytes The byte array to wrap, must not be {@code null} 059 * @return A new channel that uses the given array as its initial backing store 060 * @throws NullPointerException If {@code bytes} is {@code null} 061 * @see #array() 062 * @see ByteArrayInputStream#ByteArrayInputStream(byte[]) 063 */ 064 public static ByteArraySeekableByteChannel wrap(final byte[] bytes) { 065 Objects.requireNonNull(bytes, "bytes"); 066 return new ByteArraySeekableByteChannel(bytes); 067 } 068 069 private byte[] data; 070 private volatile boolean closed; 071 private int position; 072 private int size; 073 private final ReentrantLock lock = new ReentrantLock(); 074 075 /** 076 * Constructs a new instance, with a default internal buffer capacity. 077 * <p> 078 * The initial size and position of the channel are 0. 079 * </p> 080 * 081 * @see ByteArrayOutputStream#ByteArrayOutputStream() 082 */ 083 public ByteArraySeekableByteChannel() { 084 this(IOUtils.DEFAULT_BUFFER_SIZE); 085 } 086 087 private ByteArraySeekableByteChannel(final byte[] data) { 088 this.data = data; 089 this.position = 0; 090 this.size = data.length; 091 } 092 093 /** 094 * Constructs a new instance, with an internal buffer of the given capacity, in bytes. 095 * <p> 096 * The initial size and position of the channel are 0. 097 * </p> 098 * 099 * @param size Capacity of the internal buffer to allocate, in bytes. 100 * @see ByteArrayOutputStream#ByteArrayOutputStream(int) 101 */ 102 public ByteArraySeekableByteChannel(final int size) { 103 if (size < 0) { 104 throw new IllegalArgumentException("Size must be non-negative"); 105 } 106 this.data = new byte[size]; 107 this.position = 0; 108 this.size = 0; 109 } 110 111 /** 112 * Gets the raw byte array backing this channel, <em>this is not a copy</em>. 113 * <p> 114 * NOTE: The returned buffer is not aligned with containing data, use {@link #size()} to obtain the size of data stored in the buffer. 115 * </p> 116 * 117 * @return internal byte array. 118 */ 119 public byte[] array() { 120 return data; 121 } 122 123 private void checkOpen() throws ClosedChannelException { 124 if (!isOpen()) { 125 throw new ClosedChannelException(); 126 } 127 } 128 129 private int checkRange(final long newSize, final String method) { 130 if (newSize < 0L || newSize > IOUtils.SOFT_MAX_ARRAY_LENGTH) { 131 throw new IllegalArgumentException(String.format("%s must be in range [0..%,d]: %,d", method, IOUtils.SOFT_MAX_ARRAY_LENGTH, newSize)); 132 } 133 return (int) newSize; 134 } 135 136 @Override 137 public void close() { 138 closed = true; 139 } 140 141 /** 142 * Like {@link #size()} but never throws {@link ClosedChannelException}. 143 * 144 * @return See {@link #size()}. 145 */ 146 public long getSize() { 147 return size; 148 } 149 150 @Override 151 public boolean isOpen() { 152 return !closed; 153 } 154 155 @Override 156 public long position() throws ClosedChannelException { 157 checkOpen(); 158 lock.lock(); 159 try { 160 return position; 161 } finally { 162 lock.unlock(); 163 } 164 } 165 166 @Override 167 public SeekableByteChannel position(final long newPosition) throws IOException { 168 checkOpen(); 169 final int intPos = checkRange(newPosition, "position()"); 170 lock.lock(); 171 try { 172 position = intPos; 173 } finally { 174 lock.unlock(); 175 } 176 return this; 177 } 178 179 @Override 180 public int read(final ByteBuffer buf) throws IOException { 181 checkOpen(); 182 lock.lock(); 183 try { 184 int wanted = buf.remaining(); 185 final int possible = size - position; 186 if (possible <= 0) { 187 return IOUtils.EOF; 188 } 189 if (wanted > possible) { 190 wanted = possible; 191 } 192 buf.put(data, position, wanted); 193 position += wanted; 194 return wanted; 195 } finally { 196 lock.unlock(); 197 } 198 } 199 200 private void resize(final int newLength) { 201 int len = data.length; 202 if (len == 0) { 203 len = 1; 204 } 205 if (newLength < RESIZE_LIMIT) { 206 while (len < newLength) { 207 len <<= 1; 208 } 209 } else { // avoid overflow 210 len = newLength; 211 } 212 data = Arrays.copyOf(data, len); 213 } 214 215 @Override 216 public long size() throws ClosedChannelException { 217 checkOpen(); 218 lock.lock(); 219 try { 220 return size; 221 } finally { 222 lock.unlock(); 223 } 224 } 225 226 /** 227 * Gets a copy of the data stored in this channel. 228 * <p> 229 * The returned array is a copy of the internal buffer, sized to the actual data stored in this channel. 230 * </p> 231 * 232 * @return a new byte array containing the data stored in this channel. 233 */ 234 public byte[] toByteArray() { 235 return Arrays.copyOf(data, size); 236 } 237 238 @Override 239 public SeekableByteChannel truncate(final long newSize) throws ClosedChannelException { 240 checkOpen(); 241 final int intSize = checkRange(newSize, "truncate()"); 242 lock.lock(); 243 try { 244 if (size > intSize) { 245 size = intSize; 246 } 247 if (position > intSize) { 248 position = intSize; 249 } 250 } finally { 251 lock.unlock(); 252 } 253 return this; 254 } 255 256 @Override 257 public int write(final ByteBuffer b) throws IOException { 258 checkOpen(); 259 lock.lock(); 260 try { 261 final int wanted = b.remaining(); 262 final int possibleWithoutResize = Math.max(0, size - position); 263 if (wanted > possibleWithoutResize) { 264 final int newSize = position + wanted; 265 if (newSize < 0 || newSize > IOUtils.SOFT_MAX_ARRAY_LENGTH) { // overflow 266 throw new OutOfMemoryError("required array size " + Integer.toUnsignedString(newSize) + " too large"); 267 } 268 resize(newSize); 269 } 270 b.get(data, position, wanted); 271 position += wanted; 272 if (size < position) { 273 size = position; 274 } 275 return wanted; 276 } finally { 277 lock.unlock(); 278 } 279 } 280}