org.apache.fluo.api.data.Bytes Maven / Gradle / Ivy
Show all versions of fluo-api Show documentation
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package org.apache.fluo.api.data;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
import com.google.common.base.Preconditions;
/**
* Represents bytes in Fluo. Bytes is an immutable wrapper around a byte array. Bytes always copies
* on creation and never lets its internal byte array escape. Its modeled after Java's String which
* is an immutable wrapper around a char array. It was created because there is nothing in Java like
* it at the moment. Its very nice having this immutable type, it avoids having to do defensive
* copies to ensure correctness. Maybe one day Java will have equivalents of String, StringBuilder,
* and Charsequence for bytes.
*
*
* The reason Fluo did not use ByteBuffer is because its not immutable, even a read only ByteBuffer
* has a mutable position. This makes ByteBuffer unsuitable for place where an immutable data type
* is desirable, like a key for a map.
*
*
* Bytes.EMPTY is used to represent a Bytes object with no data.
*
* @since 1.0.0
*/
public final class Bytes implements Comparable, Serializable {
private static final long serialVersionUID = 1L;
private final byte[] data;
private final int offset;
private final int length;
public static final Bytes EMPTY = new Bytes(new byte[0]);
private Integer hashCode = null;
public Bytes() {
data = EMPTY.data;
offset = 0;
length = 0;
}
private Bytes(byte[] data) {
this.data = data;
this.offset = 0;
this.length = data.length;
}
private Bytes(byte[] data, int offset, int length) {
if (offset < 0 || offset > data.length || length < 0 || (offset + length) > data.length) {
throw new IndexOutOfBoundsException(" Bad offset and/or length data.length = " + data.length
+ " offset = " + offset + " length = " + length);
}
this.data = data;
this.offset = offset;
this.length = length;
}
/**
* Gets a byte within this sequence of bytes
*
* @param i index into sequence
* @return byte
* @throws IllegalArgumentException if i is out of range
*/
public byte byteAt(int i) {
if (i < 0) {
throw new IndexOutOfBoundsException("i < 0, " + i);
}
if (i >= length) {
throw new IndexOutOfBoundsException("i >= length, " + i + " >= " + length);
}
return data[offset + i];
}
/**
* Gets the length of bytes
*/
public int length() {
return length;
}
/**
* Returns a portion of the Bytes object
*
* @param start index of subsequence start (inclusive)
* @param end index of subsequence end (exclusive)
*/
public Bytes subSequence(int start, int end) {
if (start > end || start < 0 || end > length) {
throw new IndexOutOfBoundsException("Bad start and/end start = " + start + " end=" + end
+ " offset=" + offset + " length=" + length);
}
return new Bytes(data, offset + start, end - start);
}
/**
* Returns a byte array containing a copy of the bytes
*/
public byte[] toArray() {
byte[] copy = new byte[length];
System.arraycopy(data, offset, copy, 0, length);
return copy;
}
/**
* Creates UTF-8 String using Bytes data
*/
@Override
public String toString() {
return new String(data, offset, length, StandardCharsets.UTF_8);
}
/**
* @return A read only byte buffer thats backed by the internal byte array.
*/
public ByteBuffer toByteBuffer() {
return ByteBuffer.wrap(data, offset, length).asReadOnlyBuffer();
}
/**
* @return An input stream thats backed by the internal byte array
*/
public InputStream toInputStream() {
return new ByteArrayInputStream(data, offset, length);
}
public void writeTo(OutputStream out) throws IOException {
// since Bytes is immutable, its important the we do not let the internal byte array escape
if (length <= 32) {
int end = offset + length;
for (int i = offset; i < end; i++) {
out.write(data[i]);
}
} else {
out.write(toArray());
}
}
/**
* Compares this to the passed bytes, byte by byte, returning a negative, zero, or positive result
* if the first sequence is less than, equal to, or greater than the second. The comparison is
* performed starting with the first byte of each sequence, and proceeds until a pair of bytes
* differs, or one sequence runs out of byte (is shorter). A shorter sequence is considered less
* than a longer one.
*
* @return comparison result
*/
@Override
public final int compareTo(Bytes other) {
int minLen = Math.min(this.length(), other.length());
for (int i = 0; i < minLen; i++) {
int a = (this.byteAt(i) & 0xff);
int b = (other.byteAt(i) & 0xff);
if (a != b) {
return a - b;
}
}
return this.length() - other.length();
}
/**
* Returns true if this Bytes object equals another.
*/
@Override
public final boolean equals(Object other) {
if (other instanceof Bytes) {
Bytes ob = (Bytes) other;
if (this == other) {
return true;
}
if (length() != ob.length()) {
return false;
}
return compareTo(ob) == 0;
}
return false;
}
@Override
public final int hashCode() {
if (hashCode == null) {
int hash = 1;
for (int i = 0; i < length(); i++) {
hash = (31 * hash) + byteAt(i);
}
hashCode = hash;
}
return hashCode;
}
/**
* Creates a Bytes object by copying the data of the given byte array
*/
public static final Bytes of(byte[] array) {
Objects.requireNonNull(array);
if (array.length == 0) {
return EMPTY;
}
byte[] copy = new byte[array.length];
System.arraycopy(array, 0, copy, 0, array.length);
return new Bytes(copy);
}
/**
* Creates a Bytes object by copying the data of a subsequence of the given byte array
*
* @param data Byte data
* @param offset Starting offset in byte array (inclusive)
* @param length Number of bytes to include
*/
public static final Bytes of(byte[] data, int offset, int length) {
Objects.requireNonNull(data);
if (length == 0) {
return EMPTY;
}
byte[] copy = new byte[length];
System.arraycopy(data, offset, copy, 0, length);
return new Bytes(copy);
}
/**
* Creates a Bytes object by copying the data of the given ByteBuffer.
*
* @param bb Data will be read from this ByteBuffer in such a way that its position is not
* changed.
*/
public static final Bytes of(ByteBuffer bb) {
Objects.requireNonNull(bb);
if (bb.remaining() == 0) {
return EMPTY;
}
byte[] data;
if (bb.hasArray()) {
data =
Arrays.copyOfRange(bb.array(), bb.position() + bb.arrayOffset(),
bb.limit() + bb.arrayOffset());
} else {
data = new byte[bb.remaining()];
// duplicate so that it does not change position
bb.duplicate().get(data);
}
return new Bytes(data);
}
/**
* Creates a Bytes object by copying the data of the CharSequence and encoding it using UTF-8.
*/
public static final Bytes of(CharSequence cs) {
if (cs instanceof String) {
return of((String) cs);
}
Objects.requireNonNull(cs);
if (cs.length() == 0) {
return EMPTY;
}
ByteBuffer bb = StandardCharsets.UTF_8.encode(CharBuffer.wrap(cs));
if (bb.hasArray()) {
// this byte buffer has never escaped so can use its byte array directly
return new Bytes(bb.array(), bb.position() + bb.arrayOffset(), bb.limit());
} else {
byte[] data = new byte[bb.remaining()];
bb.get(data);
return new Bytes(data);
}
}
/**
* Creates a Bytes object by copying the value of the given String
*/
public static final Bytes of(String s) {
Objects.requireNonNull(s);
if (s.length() == 0) {
return EMPTY;
}
byte[] data = s.getBytes(StandardCharsets.UTF_8);
return new Bytes(data);
}
/**
* Creates a Bytes object by copying the value of the given String with a given charset
*/
public static final Bytes of(String s, Charset c) {
Objects.requireNonNull(s);
Objects.requireNonNull(c);
if (s.length() == 0) {
return EMPTY;
}
byte[] data = s.getBytes(c);
return new Bytes(data);
}
/**
* This class provides an easy, efficient, reusable mechanism for building immutable Bytes
* objects.
*
* @since 1.0.0
*/
public static class BytesBuilder {
private byte[] ba;
private int len;
BytesBuilder(int initialCapacity) {
ba = new byte[initialCapacity];
len = 0;
}
BytesBuilder() {
this(32);
}
private void ensureCapacity(int min) {
if (ba.length < min) {
int newLen = ba.length * 2;
if (newLen < min) {
newLen = min;
}
ba = Arrays.copyOf(ba, newLen);
}
}
public BytesBuilder append(CharSequence cs) {
if (cs instanceof String) {
return append((String) cs);
}
ByteBuffer bb = StandardCharsets.UTF_8.encode(CharBuffer.wrap(cs));
int length = bb.remaining();
ensureCapacity(len + length);
bb.get(ba, len, length);
len += length;
return this;
}
/**
* Converts string to bytes using UTF-8 encoding and appends bytes.
*
* @return self
*/
public BytesBuilder append(String s) {
return append(s.getBytes(StandardCharsets.UTF_8));
}
public BytesBuilder append(Bytes b) {
ensureCapacity(len + b.length());
System.arraycopy(b.data, b.offset, ba, len, b.length);
len += b.length();
return this;
}
public BytesBuilder append(byte[] bytes) {
ensureCapacity(len + bytes.length);
System.arraycopy(bytes, 0, ba, len, bytes.length);
len += bytes.length;
return this;
}
/**
* Append a single byte.
*
* @param b take the lower 8 bits and appends it.
* @return self
*/
public BytesBuilder append(int b) {
ensureCapacity(len + 1);
ba[len] = (byte) b;
len += 1;
return this;
}
/**
* Append a section of bytes from array
*
* @param bytes - bytes to be appended
* @param offset - start of bytes to be appended
* @param length - how many bytes from 'offset' to be appended
* @return self
*/
public BytesBuilder append(byte[] bytes, int offset, int length) {
ensureCapacity(len + length);
System.arraycopy(bytes, offset, ba, len, length);
len += length;
return this;
}
/**
* Append a sequence of bytes from an InputStream
*
* @param in data source to append from
* @param length number of bytes to read from data source
* @return self
*/
public BytesBuilder append(InputStream in, int length) throws IOException {
ensureCapacity(len + length);
new DataInputStream(in).readFully(ba, len, length);
len += length;
return this;
}
/**
* Append data from a ByteBuffer
*
* @param bb data is read from the ByteBuffer in such a way that its position is not changed.
* @return self
*/
public BytesBuilder append(ByteBuffer bb) {
int length = bb.remaining();
ensureCapacity(len + length);
bb.duplicate().get(ba, len, length);
len += length;
return this;
}
/**
* Sets the point at which appending will start. This method can shrink or grow the ByteBuilder
* from its current state. If it grows it will zero pad.
*/
public void setLength(int newLen) {
Preconditions.checkArgument(newLen >= 0, "Negative length passed : " + newLen);
if (newLen > ba.length) {
ba = Arrays.copyOf(ba, newLen);
}
if (newLen > len) {
Arrays.fill(ba, len, newLen, (byte) 0);
}
len = newLen;
}
public int getLength() {
return len;
}
public Bytes toBytes() {
return Bytes.of(ba, 0, len);
}
}
/**
* Provides an efficient and reusable way to build immutable Bytes objects.
*/
public static BytesBuilder builder() {
return new BytesBuilder();
}
/**
* @param initialCapacity The initial size of the byte builders internal array.
*/
public static BytesBuilder builder(int initialCapacity) {
return new BytesBuilder(initialCapacity);
}
}