BytesBuilder.java
package space.sunqian.common.base.bytes;
import space.sunqian.annotations.Nonnull;
import space.sunqian.common.Check;
import space.sunqian.common.base.chars.CharsKit;
import space.sunqian.common.io.BufferKit;
import space.sunqian.common.io.IOKit;
import space.sunqian.common.io.IORuntimeException;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;
/**
* {@code BytesBuilder} is used to build byte arrays and their derived objects by appending byte data. It is similar to
* {@link ByteArrayOutputStream}, provides compatible methods, but is not thread-safe. This class also extends the
* {@link OutputStream}, but the {@code close()} method has no effect.
*
* @author sunqian
*/
public class BytesBuilder extends OutputStream {
// Max array size.
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private final int maxSize;
private byte @Nonnull [] buf;
private int count;
/**
* Constructs with 32-bytes initial capacity.
*/
public BytesBuilder() {
this(32);
}
/**
* Constructs with the specified initial capacity in bytes.
*
* @param initialCapacity the specified initial capacity in bytes
* @throws IllegalArgumentException if size is negative
*/
public BytesBuilder(int initialCapacity) throws IllegalArgumentException {
this(initialCapacity, MAX_ARRAY_SIZE);
}
/**
* Constructs with the specified initial capacity and the max capacity in bytes.
*
* @param initialCapacity the specified initial capacity in bytes
* @param maxCapacity the max capacity in bytes
* @throws IllegalArgumentException if the {@code initialCapacity < 0} or {@code maxCapacity < 0} or
* {@code initialCapacity > maxCapacity}
*/
public BytesBuilder(int initialCapacity, int maxCapacity) throws IllegalArgumentException {
if (initialCapacity < 0) {
throw new IllegalArgumentException("Negative initial capacity: " + initialCapacity + ".");
}
if (maxCapacity < 0) {
throw new IllegalArgumentException("Negative max capacity: " + maxCapacity + ".");
}
if (initialCapacity > maxCapacity) {
throw new IllegalArgumentException("Initial capacity must <= max capacity!");
}
buf = new byte[initialCapacity];
this.maxSize = maxCapacity;
}
/**
* Appends the specified byte to this builder.
*
* @param b the specified byte
*/
@Override
public void write(int b) {
ensureCapacity(count + 1);
buf[count] = (byte) b;
count += 1;
}
/**
* Appends all bytes from the given array.
*
* @param b the given array
*/
@Override
public void write(byte @Nonnull [] b) {
ensureCapacity(count + b.length);
System.arraycopy(b, 0, buf, count, b.length);
count += b.length;
}
/**
* Appends the specified number of bytes from the given array, starting at the specified offset.
*
* @param b the given array
* @param off the specified offset
* @param len the specified number
* @throws IndexOutOfBoundsException if the offset or number is out of bounds
*/
@Override
public void write(byte @Nonnull [] b, int off, int len) throws IndexOutOfBoundsException {
Check.checkOffLen(off, len, b.length);
ensureCapacity(count + len);
System.arraycopy(b, off, buf, count, len);
count += len;
}
/**
* Writes the appended data of this builder to the specified output stream.
*
* @param out the specified output stream
* @throws IORuntimeException if an I/O error occurs
*/
public void writeTo(@Nonnull OutputStream out) throws IORuntimeException {
try {
out.write(buf, 0, count);
} catch (Exception e) {
throw new IORuntimeException(e);
}
}
/**
* Writes the appended buffered data of this builder to the specified buffer.
*
* @param out the specified buffer
* @throws IORuntimeException if an I/O error occurs
*/
public void writeTo(@Nonnull ByteBuffer out) throws IORuntimeException {
try {
out.put(buf, 0, count);
} catch (Exception e) {
throw new IORuntimeException(e);
}
}
/**
* Resets this builder, the appended data will be discarded.
* <p>
* This method doesn't guarantee releasing the allocated space for the appended data. To trim and release the unused
* space, use {@link #trim()}.
*/
public void reset() {
count = 0;
}
/**
* Trims and releases the allocated but unused space.
*/
public void trim() {
if (count < buf.length) {
buf = Arrays.copyOf(buf, count);
}
}
/**
* Returns the size of appended data.
*
* @return the size of appended data
*/
public int size() {
return count;
}
/**
* Returns a new array containing a copy of the appended data.
*
* @return a new array containing a copy of the appended data
*/
public byte @Nonnull [] toByteArray() {
return Arrays.copyOf(buf, count);
}
/**
* Returns a new buffer containing a copy of the appended data.
*
* @return a new buffer containing a copy of the appended data
*/
public @Nonnull ByteBuffer toByteBuffer() {
return ByteBuffer.wrap(toByteArray());
}
/**
* Returns a string decoded from the appended data using {@link CharsKit#defaultCharset()}. Note that the behavior
* of this method is <b>different</b> from {@link ByteArrayOutputStream#toString()}
*
* @return a string decoded from the appended data using {@link CharsKit#defaultCharset()}
* @see ByteArrayOutputStream#toString()
*/
@Override
public @Nonnull String toString() {
return toString(CharsKit.defaultCharset());
}
/**
* Returns a string decoded from the appended data using the specified charset. This is a compatible method of
* {@link ByteArrayOutputStream#toString(String)}.
*
* @param charsetName name of the specified charset
* @return a string decoded from the appended data using the specified charset
* @throws UnsupportedEncodingException If the named charset is not supported
* @see ByteArrayOutputStream#toString(String)
*/
public @Nonnull String toString(@Nonnull String charsetName) throws UnsupportedEncodingException {
return new String(buf, 0, count, charsetName);
}
/**
* Returns a string decoded from the appended data using the specified charset.
*
* @param charset the specified charset
* @return a string decoded from the appended data using the specified charset
*/
public @Nonnull String toString(@Nonnull Charset charset) {
return new String(buf, 0, count, charset);
}
/**
* No effect for this builder.
*/
@Override
public void close() {
}
/**
* Appends the specified byte to this builder.
*
* @param b the specified byte
* @return this builder
*/
public BytesBuilder append(int b) {
write(b);
return this;
}
/**
* Appends the specified byte to this builder.
*
* @param b the specified byte
* @return this builder
*/
public @Nonnull BytesBuilder append(byte b) {
write(b);
return this;
}
/**
* Appends all bytes from the given array.
*
* @param bytes the given array
* @return this builder
*/
public @Nonnull BytesBuilder append(byte @Nonnull [] bytes) {
write(bytes);
return this;
}
/**
* Appends the specified number of bytes from the given array, starting at the specified offset.
*
* @param bytes the given array
* @param offset the specified offset
* @param length the specified number
* @return this builder
* @throws IndexOutOfBoundsException if the offset or number is out of bounds
*/
public @Nonnull BytesBuilder append(byte @Nonnull [] bytes, int offset, int length) throws IndexOutOfBoundsException {
write(bytes, offset, length);
return this;
}
/**
* Reads and appends all bytes from the given buffer.
*
* @param bytes the given buffer
* @return this builder
*/
public @Nonnull BytesBuilder append(@Nonnull ByteBuffer bytes) {
int remaining = bytes.remaining();
if (remaining == 0) {
return this;
}
if (bytes.hasArray()) {
write(bytes.array(), BufferKit.arrayStartIndex(bytes), bytes.remaining());
bytes.position(bytes.position() + bytes.remaining());
} else {
byte[] data = new byte[remaining];
bytes.get(data);
write(data);
}
return this;
}
/**
* Reads and appends all bytes from the given stream.
*
* @param in the given stream
* @return this builder
* @throws IORuntimeException if an I/O error occurs
*/
public @Nonnull BytesBuilder append(@Nonnull InputStream in) throws IORuntimeException {
return append(in, IOKit.bufferSize());
}
/**
* Reads and appends all bytes from the given stream with the specified buffer size for each reading.
*
* @param in the given stream
* @param bufSize the specified buffer size for each reading
* @return this builder
* @throws IllegalArgumentException if the buffer size {@code <=0}
* @throws IORuntimeException if an I/O error occurs
*/
public @Nonnull BytesBuilder append(
@Nonnull InputStream in, int bufSize
) throws IllegalArgumentException, IORuntimeException {
if (bufSize <= 0) {
throw new IllegalArgumentException("The buffer size must > 0.");
}
byte[] buffer = new byte[bufSize];
while (true) {
try {
int readSize = in.read(buffer);
if (readSize < 0) {
return this;
}
write(buffer, 0, readSize);
} catch (Exception e) {
throw new IORuntimeException(e);
}
}
}
/**
* Appends all bytes from the given builder.
*
* @param builder the given builder
* @return this builder
*/
public @Nonnull BytesBuilder append(@Nonnull BytesBuilder builder) {
write(builder.buf, 0, builder.count);
return this;
}
private void ensureCapacity(int minCapacity) {
if (buf.length < minCapacity) {
grow(minCapacity);
}
}
private void grow(int minCapacity) {
if (minCapacity < 0 || minCapacity > maxSize) {
throw new IllegalStateException("Buffer out of size: " + minCapacity + ".");
}
int oldCapacity = buf.length;
int newCapacity;
if (oldCapacity == 0) {
newCapacity = minCapacity;
} else {
newCapacity = oldCapacity * 2;
}
newCapacity = newCapacity(newCapacity, minCapacity);
buf = Arrays.copyOf(buf, newCapacity);
}
private int newCapacity(int newCapacity, int minCapacity) {
if (newCapacity <= 0 || newCapacity > maxSize) {
return maxSize;
}
return Math.max(newCapacity, minCapacity);
}
}