package org.altbeacon.beacon;
import android.annotation.TargetApi;
import android.os.Build;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.LongBuffer;
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.
* <p>
* Instances of this class are immutable, so those can be shared without problem between threads.
* <p>
* The value is internally this is stored as a byte array.
*/
public class Identifier implements Comparable<Identifier>, Serializable {
private static final Pattern HEX_PATTERN = Pattern.compile("^0x[0-9A-Fa-f]*$");
private static final Pattern HEX_PATTERN_NO_PREFIX = Pattern.compile("^[0-9A-Fa-f]*$");
private static final Pattern DECIMAL_PATTERN = Pattern.compile("^0|[1-9][0-9]*$");
// BUG: Dashes in UUIDs are not optional!
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}$");
private static final int MAX_INTEGER = 65535;
private final byte[] mValue;
/**
* 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.
*
* Known bug: This method happily parses UUIDs without dashes (normally
* invalid). Although the bug is left unfixed for backward compatibility,
* please check your UUIDs or even better, use
* {@link #fromUuid(java.util.UUID)} directly, which is safe.
*
* 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 parse
* @return Identifier representing the specified value
* @throws IllegalArgumentException if the passed string cannot be parsed
* @throws NullPointerException if the passed string is <code>null</code>
* @see <a href="https://www.ietf.org/rfc/rfc4122.txt">RFC 4122 on UUIDs</a>
*/
public static Identifier parse(String stringValue) {
return parse(stringValue, -1);
}
/**
* Variant of the parse method that allows specifying the byte length of the identifier.
* @see #parse(String)
* @param stringValue
* @param desiredByteLength
* @return
*/
public static Identifier parse(String stringValue, 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()) {
return parseHex(stringValue.substring(2), desiredByteLength);
}
if (UUID_PATTERN.matcher(stringValue).matches()) {
return parseHex(stringValue.replace("-", ""), desiredByteLength);
}
if (DECIMAL_PATTERN.matcher(stringValue).matches()) {
int value = -1;
try {
value = Integer.valueOf(stringValue);
}
catch (Throwable t) {
throw new IllegalArgumentException("Unable to parse Identifier in decimal format.", t);
}
if (desiredByteLength <= 0 || desiredByteLength == 2) {
return fromInt(value);
}
else {
return fromLong(value, desiredByteLength);
}
}
if (HEX_PATTERN_NO_PREFIX.matcher(stringValue).matches()) {
return parseHex(stringValue, desiredByteLength);
}
throw new IllegalArgumentException("Unable to parse Identifier.");
}
private static Identifier parseHex(String identifierString, int desiredByteLength) {
String str = identifierString.length() % 2 == 0 ? "" : "0";
str += identifierString.toUpperCase();
if (desiredByteLength > 0 && desiredByteLength < str.length()/2) {
str = str.substring(str.length() - desiredByteLength * 2);
}
if (desiredByteLength > 0 && desiredByteLength > str.length()/2) {
int extraCharsToAdd = desiredByteLength*2 - str.length();
StringBuilder sb = new StringBuilder();
while (sb.length() < extraCharsToAdd) {
sb.append("0");
}
str = sb.toString()+str;
}
byte[] result = new byte[str.length() / 2];
for (int i = 0; i < result.length; i++) {
result[i] = (byte)(Integer.parseInt(str.substring(i * 2, i * 2 + 2), 16) & 0xFF);
}
return new Identifier(result);
}
/**
* Creates an Identifer 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
*/
public static Identifier fromLong(long longValue, int desiredByteLength) {
if (desiredByteLength < 0) {
throw new IllegalArgumentException("Identifier length must be > 0.");
}
byte[] newValue = new byte[desiredByteLength];
for (int i = desiredByteLength-1; i >= 0; i--) {
newValue[i] = (byte) (longValue & 0xff);
longValue = longValue >> 8;
}
return new Identifier(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
*/
public static Identifier fromInt(int intValue) {
if (intValue < 0 || intValue > MAX_INTEGER) {
throw new IllegalArgumentException("Identifiers can only be constructed from integers between 0 and " + MAX_INTEGER + " (inclusive).");
}
byte[] newValue = new byte[2];
newValue[0] = (byte) (intValue >> 8);
newValue[1] = (byte) (intValue);
return new Identifier(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 java.lang.NullPointerException <code>bytes</code> must not be <code>null</code>
* @throws java.lang.ArrayIndexOutOfBoundsException start or end are outside the bounds of the array
* @throws java.lang.IllegalArgumentException start is larger than end
*/
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
public static Identifier fromBytes(byte[] bytes, int start, int end, 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");
}
byte[] byteRange = Arrays.copyOfRange(bytes, start, end);
if (littleEndian) {
reverseArray(byteRange);
}
return new Identifier(byteRange);
}
/**
* Transforms a {@link java.util.UUID} into an Identifier.
* No mangling with strings, only the underlying bytes of the
* UUID are used so this is fast and stable.
*/
public static Identifier fromUuid(UUID uuid) {
ByteBuffer buf = ByteBuffer.allocate(16);
buf.putLong(uuid.getMostSignificantBits());
buf.putLong(uuid.getLeastSignificantBits());
return new Identifier(buf.array());
}
/**
* Creates a new copy of the specified Identifier.
* @param identifier identifier to copy
* @deprecated objects of this class are immutable and therefore don't have to be cloned when
* used from multiple threads
*/
@Deprecated
public Identifier(Identifier identifier) {
if (identifier == null) {
throw new NullPointerException("Identifiers cannot be constructed from null pointers but \"identifier\" is null.");
}
mValue = identifier.mValue;
}
/**
* Creates a new instance of Identifier
* @param value value to use. This value isn't copied, so don't change the value after using it to create an instance!
*/
protected Identifier(byte[] value) {
if (value == null) {
throw new NullPointerException("Identifiers cannot be constructed from null pointers but \"value\" is null.");
}
this.mValue = value;
}
/**
* 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
*/
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 (mValue.length == 2) {
return Integer.toString(toInt());
}
if (mValue.length == 16) {
return toUuid().toString();
}
return toHexString();
}
/**
* Represents the value as an <code>int</code>.
* @return value represented as int
* @throws java.lang.UnsupportedOperationException when value length is longer than 2
*/
public int toInt() {
if (mValue.length > 2) {
throw new UnsupportedOperationException("Only supported for Identifiers with max byte length of 2");
}
int result = 0;
for (int i = 0; i < mValue.length; i++) {
result |= (mValue[i] & 0xFF) << ((mValue.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
*/
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
public byte[] toByteArrayOfSpecifiedEndianness(boolean bigEndian) {
byte[] copy = Arrays.copyOf(mValue, mValue.length);
if (!bigEndian) {
reverseArray(copy);
}
return copy;
}
private static void reverseArray(byte[] bytes) {
for (int i = 0; i < bytes.length / 2; i++) {
int mirroredIndex = bytes.length - i - 1;
byte tmp = bytes[i];
bytes[i] = bytes[mirroredIndex];
bytes[mirroredIndex] = tmp;
}
}
/**
* Returns the byte length of this identifier.
* @return length of identifier
*/
public int getByteCount() {
return mValue.length;
}
@Override
public int hashCode() {
return Arrays.hashCode(mValue);
}
/**
* 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(Object that) {
if (!(that instanceof Identifier)) {
return false;
}
Identifier thatIdentifier = (Identifier) that;
return Arrays.equals(mValue, thatIdentifier.mValue);
}
/**
* 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() {
StringBuilder sb = new StringBuilder(2 + 2 * mValue.length);
sb.append("0x");
for (byte item : mValue) {
sb.append(String.format("%02x", item));
}
return sb.toString();
}
/**
* Returns the value of this Identifier in UUID format. For example 2f234454-cf6d-4a0f-adf2-f4911ba9ffa6
* @deprecated Replaced by stronger typed variant.
* This mathod returns a string, therefore does not offer type safety on
* the UUID per se. It was replaced by {@link #toUuid()}.
* @return value in UUID format
* @throws UnsupportedOperationException when value length is not 16 bytes
*/
@Deprecated
public String toUuidString() {
return toUuid().toString();
}
/**
* Gives you the Identifier as a UUID if possible.
*
* @throws UnsupportedOperationException if the byte array backing this Identifier is not exactly
* 16 bytes long.
*/
public UUID toUuid() {
if (mValue.length != 16) {
throw new UnsupportedOperationException("Only Identifiers backed by a byte array with length of exactly 16 can be UUIDs.");
}
LongBuffer buf = ByteBuffer.wrap(mValue).asLongBuffer();
return new UUID(buf.get(), buf.get());
}
/**
* Gives you the byte array backing this Identifier. Note that Identifiers are immutable,
* so changing the the returned array will not result in a changed Identifier.
*
* @return a deep copy of the data backing this Identifier.
*/
public byte[] toByteArray() {
return mValue.clone();
}
/**
* 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 the other.
* @see Comparable#compareTo
*/
@Override
public int compareTo(Identifier that) {
if (mValue.length != that.mValue.length) {
return mValue.length < that.mValue.length ? -1 : 1;
}
for (int i = 0; i < mValue.length; i++) {
if (mValue[i] != that.mValue[i]) {
return mValue[i] < that.mValue[i] ? -1 : 1;
}
}
return 0;
}
}