0 (for the first
+ * byte) and length() - 1 for the last byte.
+ *
+ * @param index the position within the sequence to retrieve
+ * @return the byte at the specified index
+ */
+ byte byteAt(int index);
+
+ /**
+ * Retrieve a sequence of bytes from the original sequence. The returned sequence includes all
+ * bytes between begin and end - 1, inclusive.
+ *
+ * @param begin the index of the first byte to be included in the result
+ * @param end the index after the last byte to be included in the result
+ * @return a byte sequence containing the bytes between begin and
+ * end - 1, inclusive
+ */
+ ByteSequence subSequence(int begin, int end);
+
+ /**
+ * Return an IntStream representation of this byte sequence. This avoids auto-boxing when not
+ * necessary. Each int represents a single byte from the sequence.
+ *
+ * @return a stream of integers, one for each byte in the sequence
+ */
+ IntStream intStream();
+
+ /**
+ * Compares this sequence with the provided byte array using a lexicographical comparison.
+ *
+ * @param bytes the byte array with which to compare this sequence
+ * @return a value following the same conventions as {@link Comparable#compareTo(Object)}
+ */
+ int compareTo(byte[] bytes);
+
+ /**
+ * Determines if the contents of this byte sequence is equivalent to the content of the provided
+ * byte array.
+ *
+ * @param bytes the byte array with which to compare this sequence
+ * @return true if the bytes they represent are the same
+ */
+ boolean contentEquals(byte[] bytes);
+}
diff --git a/src/main/java/org/apache/bytes/Bytes.java b/src/main/java/org/apache/bytes/Bytes.java
new file mode 100644
index 0000000..c76d125
--- /dev/null
+++ b/src/main/java/org/apache/bytes/Bytes.java
@@ -0,0 +1,452 @@
+/*
+ * 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.bytes;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.lang.ref.WeakReference;
+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;
+
+/**
+ * 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 extends AbstractByteSequence implements Comparabledest, at position
+ * destPos.
+ *
+ * @param dest destination array into which bytes are copied
+ * @param destPos the position in the destination array where the subsequence will be copied
+ * @exception IndexOutOfBoundsException if copying would cause access of data outside array
+ * bounds.
+ * @exception NullPointerException if either src or dest is
+ * null.
+ */
+ public void copyTo(byte[] dest, int destPos) {
+ copyTo(0, dest, destPos, length());
+ }
+
+ /**
+ * Copy length bytes from this Bytes object, starting at the begin
+ * position into the destination byte array, dest, at position destPos.
+ * All bytes between begin and begin+length-1, inclusive, are copied.
+ * The destination array must be large enough.
+ *
+ * @param begin index of the beginning of the subsequence to copy (inclusive)
+ * @param dest destination array into which bytes are copied
+ * @param destPos the position in the destination array where the subsequence will be copied
+ * @param length the length of the sequence to copy
+ * @exception IndexOutOfBoundsException if copying would cause access of data outside array
+ * bounds.
+ * @exception NullPointerException if either src or dest is
+ * null.
+ */
+ public void copyTo(int begin, byte[] dest, int destPos, int length) {
+ // since dest is byte[], we can't get the ArrayStoreException
+ System.arraycopy(data, begin, dest, destPos, length);
+ }
+
+}
diff --git a/src/main/java/org/apache/bytes/BytesBuilder.java b/src/main/java/org/apache/bytes/BytesBuilder.java
new file mode 100644
index 0000000..d5d8d2d
--- /dev/null
+++ b/src/main/java/org/apache/bytes/BytesBuilder.java
@@ -0,0 +1,225 @@
+/*
+ * 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.bytes;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+/**
+ * This class provides an easy, efficient, reusable mechanism for building immutable Bytes objects.
+ *
+ * @since 1.0.0
+ */
+public class BytesBuilder extends AbstractByteSequence {
+
+ private byte[] ba;
+ private int len;
+
+ /**
+ * Construct a builder with the specified initial capacity
+ *
+ * @param initialCapacity the initial size of the internal buffer
+ */
+ public BytesBuilder(int initialCapacity) {
+ ba = new byte[initialCapacity];
+ len = 0;
+ }
+
+ /**
+ * Construct a builder with the default initial capacity (32)
+ */
+ public 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);
+ }
+ }
+
+ /**
+ * Converts a character sequence to bytes using UTF-8 encoding and appends the resulting bytes
+ *
+ * @return self
+ */
+ public BytesBuilder append(CharSequence cs) {
+ return append(cs, StandardCharsets.UTF_8);
+ }
+
+ public BytesBuilder append(CharSequence cs, Charset charset) {
+ if (cs instanceof String) {
+ return append((String) cs, charset);
+ }
+
+ ByteBuffer bb = charset.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, StandardCharsets.UTF_8);
+ }
+
+ public BytesBuilder append(String s, Charset charset) {
+ return append(s.getBytes(charset));
+ }
+
+ public BytesBuilder append(Bytes b) {
+ ensureCapacity(len + b.length());
+ b.copyTo(ba, len);
+ 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) {
+ if (newLen < 0) {
+ throw new IllegalArgumentException("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;
+ }
+
+ @Override
+ public int length() {
+ return len;
+ }
+
+ public Bytes toBytes() {
+ return Bytes.of(ba, 0, len);
+ }
+
+ @Override
+ public byte byteAt(int index) {
+ return ba[index];
+ }
+
+ @Override
+ public BytesBuilder subSequence(int begin, int end) {
+ checkBounds(begin, end);
+ int size = end - begin;
+ return new BytesBuilder(size).append(ba, begin, size);
+ }
+
+ @Override
+ public int compareTo(byte[] bytes) {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ @Override
+ public boolean contentEquals(byte[] bytes) {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+}
diff --git a/src/test/java/org/apache/bytes/BytesTest.java b/src/test/java/org/apache/bytes/BytesTest.java
new file mode 100644
index 0000000..5806728
--- /dev/null
+++ b/src/test/java/org/apache/bytes/BytesTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.bytes;
+
+import java.nio.ByteBuffer;
+
+import org.junit.Test;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.nio.charset.StandardCharsets.UTF_16;
+import static java.nio.charset.StandardCharsets.UTF_16BE;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class BytesTest {
+
+ private static final Bytes BYTES_EMPTY = Bytes.EMPTY;
+ private static final Bytes BYTES_STRING = Bytes.of("test String");
+ private static final Bytes BYTES_STRING_CHARSET = Bytes.of("test String with Charset", US_ASCII);
+ private static final Bytes BYTES_CHARSEQ = Bytes.of(new StringBuilder("test CharSequence"));
+ private static final Bytes BYTES_CHARSEQ_CHARSET = Bytes.of(new StringBuilder(
+ "test CharSequence with Charset"), US_ASCII);
+ private static final Bytes BYTES_BB = Bytes.of(ByteBuffer.wrap("test ByteBuffer"
+ .getBytes(US_ASCII)));
+ private static final Bytes BYTES_ARRAY = Bytes.of("test byte[]".getBytes(US_ASCII));
+ private static final Bytes BYTES_ARRAY_OFFSET = Bytes.of(
+ "---test byte[] with offset and length---".getBytes(US_ASCII), 3, 34);
+
+ @Test
+ public void testToString() {
+ assertEquals("", BYTES_EMPTY.toString());
+ assertEquals("test String", BYTES_STRING.toString());
+ assertEquals("test String with Charset", BYTES_STRING_CHARSET.toString());
+ assertEquals("test CharSequence", BYTES_CHARSEQ.toString());
+ assertEquals("test CharSequence with Charset", BYTES_CHARSEQ_CHARSET.toString());
+ assertEquals("test ByteBuffer", BYTES_BB.toString());
+ assertEquals("test byte[]", BYTES_ARRAY.toString());
+ assertEquals("test byte[] with offset and length", BYTES_ARRAY_OFFSET.toString());
+ }
+
+ @Test
+ public void testBeginsWith() {
+ assertTrue(BYTES_EMPTY.beginsWith(BYTES_EMPTY));
+ assertFalse(BYTES_EMPTY.beginsWith(BYTES_STRING));
+ assertTrue(BYTES_STRING.beginsWith(BYTES_EMPTY));
+ assertTrue(BYTES_STRING_CHARSET.beginsWith(BYTES_STRING));
+ assertFalse(BYTES_STRING.beginsWith(BYTES_STRING_CHARSET));
+ assertFalse(BYTES_CHARSEQ.beginsWith(BYTES_STRING));
+ assertFalse(Bytes.of("abcdef").beginsWith(Bytes.of("Abcd")));
+ assertFalse(Bytes.of("abcdef").beginsWith(Bytes.of("abcD")));
+ assertFalse(Bytes.of("abcdef").beginsWith(Bytes.of("abCd")));
+ }
+
+ @Test
+ public void testEndsWith() {
+ assertTrue(BYTES_EMPTY.endsWith(BYTES_EMPTY));
+ assertFalse(BYTES_EMPTY.endsWith(BYTES_STRING));
+ assertTrue(BYTES_STRING.endsWith(BYTES_EMPTY));
+ assertTrue(BYTES_STRING.endsWith(Bytes.of("ing")));
+ assertFalse(Bytes.of("ing").endsWith(BYTES_STRING));
+ assertFalse(BYTES_CHARSEQ.endsWith(BYTES_STRING));
+ assertFalse(Bytes.of("abcdef").endsWith(Bytes.of("Cdef")));
+ assertFalse(Bytes.of("abcdef").endsWith(Bytes.of("cdeF")));
+ assertFalse(Bytes.of("abcdef").endsWith(Bytes.of("cdEf")));
+ }
+
+ @Test
+ public void testToArray() {
+ assertArrayEquals("".getBytes(US_ASCII), BYTES_EMPTY.toArray());
+ assertArrayEquals("test String".getBytes(UTF_8), BYTES_STRING.toArray());
+ assertArrayEquals("test String with Charset".getBytes(UTF_8), BYTES_STRING_CHARSET.toArray());
+ assertArrayEquals("test CharSequence".getBytes(UTF_8), BYTES_CHARSEQ.toArray());
+ assertArrayEquals("test CharSequence with Charset".getBytes(UTF_8),
+ BYTES_CHARSEQ_CHARSET.toArray());
+ assertArrayEquals("test ByteBuffer".getBytes(UTF_8), BYTES_BB.toArray());
+ assertArrayEquals("test byte[]".getBytes(UTF_8), BYTES_ARRAY.toArray());
+ assertArrayEquals("test byte[] with offset and length".getBytes(UTF_8),
+ BYTES_ARRAY_OFFSET.toArray());
+ // test array with custom charset for String
+ assertArrayEquals("test utf16".getBytes(UTF_16), Bytes.of("test utf16", UTF_16).toArray());
+ // test array with custom charset for CharSequence
+ assertArrayEquals("test ISO_8859_1".getBytes(ISO_8859_1),
+ Bytes.of(new StringBuilder("test ISO_8859_1"), ISO_8859_1).toArray());
+ }
+
+ @Test
+ public void testByteAt() {
+ String s = "1234";
+ Bytes b = Bytes.of(s, UTF_16BE);
+ assertEquals(s.length() * 2, b.length()); // no BOM with UTF_16BE
+ // for each char in string, check that its corresponding bytes exist in the correct position
+ for (int i = 0; i < s.length(); ++i) {
+ int codePoint = s.codePointAt(i);
+ assertEquals(codePoint >> Byte.SIZE, b.byteAt(2 * i)); // check most significant bits
+ assertEquals(codePoint & 0xFF, b.byteAt(2 * i + 1)); // check least significant bits
+ }
+
+ try {
+ int a = b.byteAt(b.length());
+ fail("Previous line should have failed; byte: " + a);
+ } catch (IndexOutOfBoundsException e) {
+ // this is expected
+ }
+
+ try {
+ int a = b.byteAt(-1);
+ fail("Previous line should have failed; byte: " + a);
+ } catch (IndexOutOfBoundsException e) {
+ // this is expected
+ }
+ }
+
+ @Test
+ public void testLength() {
+ assertEquals(0, BYTES_EMPTY.length());
+ // string length should be equal to array length, because all these use 7-bit ASCII chars with
+ // US_ASCII or UTF_8 encoding
+ assertEquals("test String".length(), BYTES_STRING.length());
+ assertEquals("test String with Charset".length(), BYTES_STRING_CHARSET.length());
+ assertEquals("test CharSequence".length(), BYTES_CHARSEQ.length());
+ assertEquals("test CharSequence with Charset".length(), BYTES_CHARSEQ_CHARSET.length());
+ assertEquals("test ByteBuffer".length(), BYTES_BB.length());
+ assertEquals("test byte[]".length(), BYTES_ARRAY.length());
+ assertEquals("test byte[] with offset and length".length(), BYTES_ARRAY_OFFSET.length());
+
+ // UTF_16 uses 2 bytes per char
+ assertEquals("test UTF_16BE".length() * 2, Bytes.of("test UTF_16BE", UTF_16BE).length());
+ }
+
+}