Binary.java
/*
* Copyright (c) 2016, Stein Eldar Johnsen
*
* 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 net.morimekta.collect.util;
import net.morimekta.collect.SecureHashable;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Base64;
import java.util.BitSet;
import java.util.Locale;
import static java.lang.Byte.toUnsignedInt;
import static java.util.Objects.requireNonNull;
/**
* Simplistic byte sequence wrapper with lots of convenience methods. Used to
* wrap byte arrays for the binary data type. This class does not give any
* access not modifiers to it's contained byte array, so can be considered
* fully immutable as long as the wrapped byte array is not modified.
*/
public final class Binary implements SecureHashable, Comparable<Binary>, Serializable {
private static final long serialVersionUID = 196610438780516680L;
/**
* Create a binary instance that wraps a created byte array. Exposed so
* it can be used in places where constructors are expected.
*
* @param bytes The byte array to wrap.
*/
public Binary(byte[] bytes) {
requireNonNull(bytes, "bytes == null");
this.bytes = bytes;
this.offset = 0;
this.length = bytes.length;
}
/**
* Create a binary instance that wraps a created byte array. Exposed so
* it can be used in places where constructors are expected.
*
* @param bytes The byte array to wrap.
* @param offset The offset in the array.
* @param length The length of array to wrap.
*/
public Binary(byte[] bytes, int offset, int length) {
requireNonNull(bytes, "bytes == null");
if (offset < 0) {
throw new IllegalArgumentException("offset " + offset + " < 0");
}
if (length < 0) {
throw new IllegalArgumentException("length " + length + " < 0");
}
if (offset + length > bytes.length) {
throw new IllegalArgumentException("End after end of bytes: " + offset + " + " + length + " > " + bytes.length);
}
this.bytes = bytes;
this.offset = offset;
this.length = length;
}
/**
* Convenience method to wrap a byte array into a byte sequence.
*
* @param bytes Bytes to wrap.
* @return The wrapped byte sequence.
*/
public static Binary wrap(byte[] bytes) {
requireNonNull(bytes, "bytes == null");
if (bytes.length == 0) {
return EMPTY;
}
return new Binary(bytes);
}
/**
* Convenience method to copy a byte array into a byte sequence.
*
* @param bytes Bytes to wrap.
* @return The wrapped byte sequence.
*/
public static Binary copy(byte[] bytes) {
return copy(bytes, 0, bytes.length);
}
/**
* Convenience method to copy a part of a byte array into a byte sequence.
*
* @param bytes Bytes to wrap.
* @param off Offset in source bytes to start reading from.
* @param len Number of bytes to copy.
* @return The wrapped byte sequence.
*/
public static Binary copy(byte[] bytes, int off, int len) {
requireNonNull(bytes, "bytes == null");
if (len == 0) {
return EMPTY;
}
byte[] cpy = new byte[len];
System.arraycopy(bytes, off, cpy, 0, len);
return wrap(cpy);
}
/**
* Method to create a Binary with 0 bytes.
*
* @return Empty Binary object.
*/
public static Binary empty() {
return EMPTY;
}
/**
* Get the length of the backing array.
*
* @return Byte count.
*/
public int length() {
return length;
}
/**
* Get a copy of the backing array.
*
* @return The copy.
*/
public byte[] get() {
byte[] cpy = new byte[length];
System.arraycopy(bytes, offset, cpy, 0, length);
return cpy;
}
/**
* Get a copy of the backing array.
*
* @param into Target ro copy into.
* @return Number of bytes written.
*/
public int get(byte[] into) {
requireNonNull(into, "into == null");
int len = Math.min(into.length, length);
System.arraycopy(bytes, offset, into, 0, len);
return len;
}
/**
* Create a slice of the binary content. Will not copy the underlying bytes.
*
* @param start Start index of new slice.
* @param end End index of new slice (excluding).
* @return The created slice.
*/
public Binary slice(int start, int end) {
if (start == 0 && end == length) {
return this;
}
if (start == end) {
return EMPTY;
}
if (start < 0) {
throw new IllegalArgumentException("start " + start + " < 0");
}
if (start > end) {
throw new IllegalArgumentException("start " + start + " > end " + end);
}
if (end > length) {
throw new IllegalArgumentException("end " + end + " > length " + length);
}
return new Binary(bytes, offset + start, end - start);
}
/**
* Create a slice of the binary content until it's end. Will not copy the
* underlying bytes.
*
* @param start Start index of new slice.
* @return The created slice.
*/
public Binary slice(int start) {
if (start == 0) {
return this;
}
return slice(start, length);
}
/**
* Decode base64 string and wrap the result in a byte sequence.
*
* @param base64 The string to decode.
* @return The resulting sequence.
*/
public static Binary fromBase64(String base64) {
requireNonNull(base64, "base64 == null");
if (base64.isEmpty()) {
return EMPTY;
}
byte[] arr = Base64.getDecoder().decode(base64);
return new Binary(arr);
}
/**
* Get the sequence encoded as base64.
*
* @return The encoded string.
*/
public String toBase64() {
return Base64.getEncoder().withoutPadding().encodeToString(getOrSame());
}
/**
* Parse a hex string as bytes.
*
* @param hex The hex string.
* @return The corresponding bytes.
* @throws NumberFormatException If the string is not valid as hex string.
*/
public static Binary fromHexString(String hex) {
requireNonNull(hex, "hex == null");
if (hex.isEmpty()) {
return EMPTY;
}
if (hex.length() % 2 != 0) {
throw new IllegalArgumentException("Illegal hex string length: " + hex.length());
}
final int len = hex.length() / 2;
final byte[] out = new byte[len];
for (int i = 0; i < len; ++i) {
int pos = i * 2;
String part = hex.substring(pos, pos + 2);
out[i] = (byte) Integer.parseInt(part, 16);
}
return new Binary(out);
}
/**
* Make a hex string from a byte array.
*
* @return The hex string.
*/
public String toHexString() {
if (length == 0) {
return "";
}
StringBuilder builder = new StringBuilder();
for (int i = offset; i < (offset + length); ++i) {
builder.append(String.format(Locale.US, "%02x", bytes[i]));
}
return builder.toString();
}
/**
* Encode a string and return the data as a binary.
*
* @param str The string to encode.
* @param encoding The charset to encode with.
* @return The encoded binary.
*/
public static Binary encodeFromString(String str, Charset encoding) {
byte[] data = str.getBytes(encoding);
return wrap(data);
}
/**
* Decode data as a string using the specified charset encoding.
*
* @param encoding The encoding to use.
* @return The decoded string.
*/
public String decodeToString(Charset encoding) {
return new String(bytes, offset, length, encoding);
}
/**
* Get a binary from the remaining content of the byte buffer.
*
* @param buffer The buffer to get bytes from.
* @return The resulting binary.
*/
public static Binary fromByteBuffer(ByteBuffer buffer) {
if (buffer.remaining() < 1) {
return EMPTY;
}
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
return new Binary(data);
}
/**
* Get a byte buffer wrapping the binary data.
*
* @return A byte buffer.
*/
public ByteBuffer getByteBuffer() {
return ByteBuffer.wrap(get());
}
/**
* Get a binary from the byte representation of a bitset.
*
* @param bitSet The bitset to get binary bytes from.
* @return The binary.
*/
public static Binary fromBitSet(BitSet bitSet) {
return new Binary(bitSet.toByteArray());
}
/**
* Get a bitset from the binary data.
*
* @return The resulting bitset.
*/
public BitSet toBitSet() {
return BitSet.valueOf(getOrSame());
}
/**
* Get an input stream that reads from the stored bytes.
*
* @return An input stream.
*/
public InputStream getInputStream() {
return new ByteArrayInputStream(bytes, offset, length);
}
/**
* Read a binary buffer from input stream. This will read all of the
* content of the stream, and put in the binary.
*
* @param in Input stream to read.
* @return The binary instance.
* @throws IOException If unable to read completely what's expected.
*/
public static Binary read(InputStream in) throws IOException {
ByteArrayOutputStream tmp = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int l;
while ((l = in.read(buf)) > 0) {
tmp.write(buf, 0, l);
}
if (tmp.size() == 0) {
return EMPTY;
}
return wrap(tmp.toByteArray());
}
/**
* Read a binary buffer from input stream. This will read a specific
* number of bytes form the stream, and wrap in the binary instance.
*
* @param in Input stream to read.
* @param len Number of bytes to read.
* @return The binary instance.
* @throws IOException If unable to read completely what's expected.
*/
public static Binary read(InputStream in, int len) throws IOException {
if (len == 0) {
return EMPTY;
}
byte[] bytes = new byte[len];
int pos = 0;
while (pos < len) {
int i = in.read(bytes, pos, len - pos);
if (i <= 0) {
throw new IOException("End of stream before complete buffer read.");
}
pos += i;
}
return wrap(bytes);
}
/**
* Write bytes to output stream.
*
* @param out Stream to write to.
* @return Number of bytes written.
* @throws IOException When unable to write to stream.
*/
public int write(OutputStream out) throws IOException {
out.write(bytes, offset, length);
return length;
}
// --- Comparable ---
@Override
public int compareTo(Binary other) {
final int c = Math.min(length, other.length);
for (int i = 0; i < c; ++i) {
if (bytes[offset + i] != other.bytes[other.offset + i]) {
return bytes[offset + i] > other.bytes[other.offset + i] ? 1 : -1;
}
}
if (length == other.length) {
return 0;
}
return length > other.length ? 1 : -1;
}
// --- Object ---
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof Binary)) {
return false;
}
Binary other = (Binary) o;
if (other.length != length) {
return false;
}
if (other.bytes == bytes && other.offset == offset) {
// same data, same offset (and same length).
return true;
}
for (int i = 0; i < length; ++i) {
if (bytes[offset + i] != other.bytes[other.offset + i]) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
if (hash == null) {
int result = 5023;
for (int i = 0; i < length; ++i) {
result *= 3119;
result += (512 + toUnsignedInt(bytes[i + offset])) * 9013;
}
hash = result;
}
return hash;
}
@Override
public long secureHashCode() {
if (secureHash == null) {
secureHash = SecureHashable.secureHash(bytes, offset, length);
}
return secureHash;
}
@Override
public String toString() {
return "binary(" + toHexString() + ")";
}
private byte[] getOrSame() {
if (offset == 0 && length == bytes.length) {
return bytes;
}
return get();
}
private final byte[] bytes;
private final int offset;
private final int length;
private transient Integer hash;
private transient Long secureHash;
private static final Binary EMPTY = new Binary(new byte[0]);
}