BeaconIdentifier.java
/*
* Copyright (C) 2018 Alberto Irurueta Carro (alberto@irurueta.com)
*
* Licensed 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 com.irurueta.navigation.indoor;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.UUID;
import java.util.regex.Pattern;
/**
* Encapsulates a beacon identifier of arbitrary byte length.
* It can encapsulate an identifier that is a 16-byte UUID, or an integer.
* Based on:
* <a href="https://github.com/AltBeacon/android-beacon-library/blob/master/src/main/java/org/altbeacon/beacon/Identifier.java">
* https://github.com/AltBeacon/android-beacon-library/blob/master/src/main/java/org/altbeacon/beacon/Identifier.java
* </a>
*/
public class BeaconIdentifier implements Comparable<BeaconIdentifier>, Serializable {
/**
* Parses beacon identifiers in hexadecimal format.
*/
private static final Pattern HEX_PATTERN = Pattern.compile("^0x[0-9A-Fa-f]*$");
/**
* Parses beacon identifiers in hexadecimal format without prefix.
*/
private static final Pattern HEX_PATTERN_NO_PREFIX = Pattern.compile("^[0-9A-Fa-f]*$");
/**
* Parses beacon identifiers in decimal format.
*/
private static final Pattern DECIMAL_PATTERN = Pattern.compile("^(0|[1-9][0-9]*)$");
/**
* Parses beacon identifiers in UUID format.
*/
private static final Pattern UUID_PATTERN = Pattern.compile(
"^[0-9A-Fa-f]{8}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{12}$");
/**
* Maximum allowed identifier value from an integer.
*/
private static final int MAX_INTEGER = 65535;
/**
* Contains digits to represent this instance in hexadecimal format.
*/
private static final char[] HEX_DIGITS =
{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
/**
* Internal value holding a beacon identifier as a byte array.
*/
private byte[] value;
/**
* Empty constructor to prevent deserialization issues.
*/
protected BeaconIdentifier() {
}
/**
* Creates a nw instance of a beacon identifier.
*
* @param value value to use.
* @throws NullPointerException if provided value is null.
*/
protected BeaconIdentifier(final byte[] value) {
if (value == null) {
throw new NullPointerException(
"Identifiers cannot be constructed from null pointers but \"value\" is null.");
}
this.value = value;
}
/**
* Takes the passed string and tries to figure out what format it is in.
* Then turns the string into plain bytes and constructs an identifier.
* <p>
* This method parses UUIDs without dashes for compatibility (although this is not a standard behaviour).
* <p>
* Allowed formats:
* <ul>
* <li>UUID: 2F234454-CF6D-4A0F-ADF2-F4911BA9FFA6 (16 bytes)</li>
* <li>Hexadecimal: 0x000000000003 (variable length)</li>
* <li>Decimal: 1337 (2 bytes)</li>
* </ul>
*
* @param stringValue string to be parsed.
* @return an identifier representing the specified value.
* @throws NullPointerException if string value is null.
* @throws IllegalArgumentException if parsing fails for some other reason (invalid format, etc.).
* @see <a href="https://www.ietf.org/rfc/rfc4122.txt">RFC 4122 on UUIDs</a>
*/
public static BeaconIdentifier parse(final String stringValue) {
return parse(stringValue, -1);
}
/**
* Variant of the parse method that allows specifying the byte length of the identifier.
*
* @param stringValue value to be parsed.
* @param desiredByteLength requested number of bytes to hold the identifier or -1 if not specified.
* @return the parsed identifier.
* @throws NullPointerException if string value is null.
* @throws IllegalArgumentException if parsing fails for some other reason (invalid format, etc.).
*/
public static BeaconIdentifier parse(final String stringValue, final int desiredByteLength) {
if (stringValue == null) {
throw new NullPointerException(
"Identifiers cannot be constructed from null pointers but \"stringValue\" is null.");
}
if (HEX_PATTERN.matcher(stringValue).matches()) {
// parse hexadecimal format
return parseHex(stringValue.substring(2), desiredByteLength);
}
if (UUID_PATTERN.matcher(stringValue).matches()) {
// parse UUID format
return parseHex(stringValue.replace("-", ""), desiredByteLength);
}
if (DECIMAL_PATTERN.matcher(stringValue).matches()) {
// parse decimal format
var value = Integer.parseInt(stringValue);
if (desiredByteLength <= 0 || desiredByteLength == 2) {
return fromInt(value);
} else {
return fromLong(value, desiredByteLength);
}
}
if (HEX_PATTERN_NO_PREFIX.matcher(stringValue).matches()) {
// parse hexadecimal format without prefix
return parseHex(stringValue, desiredByteLength);
}
throw new IllegalArgumentException("Unable to parse identifier");
}
/**
* Creates an identifier backed by an array of length desiredByteLength.
*
* @param longValue a long to put into the identifier.
* @param desiredByteLength how many bytes to make the identifier.
* @return the parsed identifier.
* @throws IllegalArgumentException if desired number of bytes is negative.
*/
public static BeaconIdentifier fromLong(long longValue, final int desiredByteLength) {
if (desiredByteLength < 0) {
throw new IllegalArgumentException("identifier length must be > 0");
}
final var newValue = new byte[desiredByteLength];
for (var i = desiredByteLength - 1; i >= 0; i--) {
newValue[i] = (byte) (longValue & 0xff);
longValue = longValue >> 8;
}
return new BeaconIdentifier(newValue);
}
/**
* Creates an identifier backed by a two byte array (big endian).
*
* @param intValue an integer between 0 and 65535 (inclusive).
* @return an identifier with the specified value.
* @throws IllegalArgumentException if provided value is out of valid range (from 0 to 65535).
*/
public static BeaconIdentifier fromInt(final int intValue) {
if (intValue < 0 || intValue > MAX_INTEGER) {
throw new IllegalArgumentException(
"Identifiers can only be constructed from integers between 0 and " + MAX_INTEGER + " (inclusive).");
}
final var newValue = new byte[2];
newValue[0] = (byte) (intValue >> 8);
newValue[1] = (byte) (intValue);
return new BeaconIdentifier(newValue);
}
/**
* Creates an identifier from the specified byte array.
*
* @param bytes array to copy from.
* @param start the start index, inclusive.
* @param end the end index, exclusive.
* @param littleEndian whether the bytes are ordered in little endian.
* @return a new identifier.
* @throws NullPointerException if bytes is null.
* @throws ArrayIndexOutOfBoundsException if start or end are outside the bounds of the array.
* @throws IllegalArgumentException start is larger than end.
*/
public static BeaconIdentifier fromBytes(
final byte[] bytes, final int start, final int end, final boolean littleEndian) {
if (bytes == null) {
throw new NullPointerException(
"Identifiers cannot be constructed from null pointers but \"bytes\" is null.");
}
if (start < 0 || start > bytes.length) {
throw new ArrayIndexOutOfBoundsException("start < 0 || start > bytes.length");
}
if (end > bytes.length) {
throw new ArrayIndexOutOfBoundsException("end > bytes.length");
}
if (start > end) {
throw new IllegalArgumentException("start > end");
}
final var byteRange = Arrays.copyOfRange(bytes, start, end);
if (littleEndian) {
reverseArray(byteRange);
}
return new BeaconIdentifier(byteRange);
}
/**
* Transforms a {@link UUID} into an identifier.
* No mangling with strings, only the underlying bytes of the
* UUID are used so this is fast and stable.
*
* @param uuid UUID to create identifier from.
* @return a new identifier.
*/
public static BeaconIdentifier fromUuid(final UUID uuid) {
final var buf = ByteBuffer.allocate(16);
buf.putLong(uuid.getMostSignificantBits());
buf.putLong(uuid.getLeastSignificantBits());
return new BeaconIdentifier(buf.array());
}
/**
* Represents the value as a String. The output varies based on the length of the value.
* <ul><li>When the value is 2 bytes long: decimal, for example 6536.
* <li>When the value is 16 bytes long: uuid, for example 2f234454-cf6d-4a0f-adf2-f4911ba9ffa6
* <li>Else: hexadecimal prefixed with <code>0x</code>, for example 0x0012ab</ul>
*
* @return string representation of the current value.
*/
@Override
public String toString() {
// Note: the toString() method is also used for serialization and deserialization. So
// toString() and parse() must always return objects that return true when you call equals()
if (value == null) {
return super.toString();
}
if (value.length == 2) {
return Integer.toString(toInt());
}
if (value.length == 16) {
return toUuid().toString();
}
return toHexString();
}
/**
* Represents the value as an <code>int</code>.
*
* @return value represented as int.
* @throws UnsupportedOperationException when value length is longer than 2.
*/
public int toInt() {
if (value == null) {
return 0;
}
if (value.length > 2) {
throw new UnsupportedOperationException("Only supported for Identifiers with max byte length of 2");
}
var result = 0;
for (var i = 0; i < value.length; i++) {
result |= (value[i] & 0xFF) << ((value.length - i - 1) * 8);
}
return result;
}
/**
* Converts identifier to a byte array.
*
* @param bigEndian true if bytes are MSB first.
* @return a new byte array with a copy of the value.
*/
public byte[] toByteArrayOfSpecifiedEndianness(final boolean bigEndian) {
if (value == null) {
return null;
}
final var copy = Arrays.copyOf(value, value.length);
if (!bigEndian) {
reverseArray(copy);
}
return copy;
}
/**
* Returns the byte length of this identifier.
*
* @return length of identifier.
*/
public int getByteCount() {
return value != null ? value.length : 0;
}
/**
* Represents the value as a hexadecimal String. The String is prefixed with <code>0x</code>. For example
* 0x0034ab.
*
* @return value as hexadecimal String.
*/
public String toHexString() {
if (value == null) {
return null;
}
final var l = value.length;
final var out = new char[l * 2 + 2];
out[0] = '0';
out[1] = 'x';
for (int i = 0, j = 2; i < l; i++) {
out[j] = HEX_DIGITS[(0xF0 & value[i]) >>> 4];
j++;
out[j] = HEX_DIGITS[0x0F & value[i]];
j++;
}
return new String(out);
}
/**
* Gives you the identifier as a UUID if possible.
*
* @return the identifier as a UUID.
* @throws UnsupportedOperationException if conversion to UUID fails.
*/
public UUID toUuid() {
if (value == null) {
return null;
}
if (value.length != 16) {
throw new UnsupportedOperationException("Only Identifiers backed by a byte array with length of exactly 16 can be UUIDs.");
}
final var buf = ByteBuffer.wrap(value).asLongBuffer();
return new UUID(buf.get(), buf.get());
}
/**
* Gives you the byte array backing this identifier. Note that identifiers are immutable,
* so changing that the returned array will not result in a changed identifier.
*
* @return a deep copy of the data backing this identifier.
*/
public byte[] toByteArray() {
return value != null ? value.clone() : null;
}
/**
* Computes hash code for this instance.
*
* @return this instance hash code.
*/
@Override
public int hashCode() {
return value != null ? Arrays.hashCode(value) : 0;
}
/**
* Returns whether both identifiers contain equal value.
* This is the case when the value is the same and has the same length.
*
* @param that object to compare to.
* @return whether that equals this.
*/
@Override
public boolean equals(final Object that) {
if (!(that instanceof BeaconIdentifier thatIdentifier)) {
return false;
}
return Arrays.equals(value, thatIdentifier.value);
}
/**
* Compares two identifiers.
* When the identifiers don't have the same length, the identifier having the shortest
* array is considered smaller than the other.
*
* @param that the other identifier.
* @return 0 if both identifiers are equal. Otherwise, returns -1 or 1 depending on
* which is bigger than th other.
* @see Comparable#compareTo(Object)
*/
@Override
public int compareTo(final BeaconIdentifier that) {
if (value.length != that.value.length) {
return value.length < that.value.length ? -1 : 1;
}
for (var i = 0; i < value.length; i++) {
if (value[i] != that.value[i]) {
return value[i] < that.value[i] ? -1 : 1;
}
}
return 0;
}
/**
* Reverses provided array.
*
* @param bytes array to be reversed.
*/
private static void reverseArray(final byte[] bytes) {
for (var i = 0; i < bytes.length / 2; i++) {
final var mirroredIndex = bytes.length - i - 1;
final var tmp = bytes[i];
bytes[i] = bytes[mirroredIndex];
bytes[mirroredIndex] = tmp;
}
}
/**
* Parses a string containing a beacon identifier in hexadecimal format.
*
* @param identifierString string to be parsed.
* @param desiredByteLength length of byte array to create to hold provided value.
* @return the parsed identifier.
*/
private static BeaconIdentifier parseHex(final String identifierString, final int desiredByteLength) {
var str = identifierString.length() % 2 == 0 ? "" : "0";
str += identifierString.toUpperCase();
var len = str.length();
if (desiredByteLength > 0 && desiredByteLength < len / 2) {
str = str.substring(len - desiredByteLength * 2);
len = str.length();
}
if (desiredByteLength > 0 && desiredByteLength > len / 2) {
final var extraCharsToAdd = desiredByteLength * 2 - len;
final var sb = new StringBuilder();
while (sb.length() < extraCharsToAdd) {
sb.append("0");
}
str = sb + str;
len = str.length();
}
final var result = new byte[len / 2];
for (var i = 0; i < result.length; i++) {
result[i] = (byte) (Integer.parseInt(str.substring(i * 2, i * 2 + 2), 16) & 0xFF);
}
return new BeaconIdentifier(result);
}
}