1 /*
2 * Copyright (C) 2018 Alberto Irurueta Carro (alberto@irurueta.com)
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package com.irurueta.navigation.indoor;
17
18 import java.io.Serializable;
19 import java.nio.ByteBuffer;
20 import java.util.Arrays;
21 import java.util.UUID;
22 import java.util.regex.Pattern;
23
24 /**
25 * Encapsulates a beacon identifier of arbitrary byte length.
26 * It can encapsulate an identifier that is a 16-byte UUID, or an integer.
27 * Based on:
28 * <a href="https://github.com/AltBeacon/android-beacon-library/blob/master/src/main/java/org/altbeacon/beacon/Identifier.java">
29 * https://github.com/AltBeacon/android-beacon-library/blob/master/src/main/java/org/altbeacon/beacon/Identifier.java
30 * </a>
31 */
32 public class BeaconIdentifier implements Comparable<BeaconIdentifier>, Serializable {
33 /**
34 * Parses beacon identifiers in hexadecimal format.
35 */
36 private static final Pattern HEX_PATTERN = Pattern.compile("^0x[0-9A-Fa-f]*$");
37
38 /**
39 * Parses beacon identifiers in hexadecimal format without prefix.
40 */
41 private static final Pattern HEX_PATTERN_NO_PREFIX = Pattern.compile("^[0-9A-Fa-f]*$");
42
43 /**
44 * Parses beacon identifiers in decimal format.
45 */
46 private static final Pattern DECIMAL_PATTERN = Pattern.compile("^(0|[1-9][0-9]*)$");
47
48 /**
49 * Parses beacon identifiers in UUID format.
50 */
51 private static final Pattern UUID_PATTERN = Pattern.compile(
52 "^[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}$");
53
54 /**
55 * Maximum allowed identifier value from an integer.
56 */
57 private static final int MAX_INTEGER = 65535;
58
59 /**
60 * Contains digits to represent this instance in hexadecimal format.
61 */
62 private static final char[] HEX_DIGITS =
63 {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
64
65 /**
66 * Internal value holding a beacon identifier as a byte array.
67 */
68 private byte[] value;
69
70
71 /**
72 * Empty constructor to prevent deserialization issues.
73 */
74 protected BeaconIdentifier() {
75 }
76
77 /**
78 * Creates a nw instance of a beacon identifier.
79 *
80 * @param value value to use.
81 * @throws NullPointerException if provided value is null.
82 */
83 protected BeaconIdentifier(final byte[] value) {
84 if (value == null) {
85 throw new NullPointerException(
86 "Identifiers cannot be constructed from null pointers but \"value\" is null.");
87 }
88
89 this.value = value;
90 }
91
92 /**
93 * Takes the passed string and tries to figure out what format it is in.
94 * Then turns the string into plain bytes and constructs an identifier.
95 * <p>
96 * This method parses UUIDs without dashes for compatibility (although this is not a standard behaviour).
97 * <p>
98 * Allowed formats:
99 * <ul>
100 * <li>UUID: 2F234454-CF6D-4A0F-ADF2-F4911BA9FFA6 (16 bytes)</li>
101 * <li>Hexadecimal: 0x000000000003 (variable length)</li>
102 * <li>Decimal: 1337 (2 bytes)</li>
103 * </ul>
104 *
105 * @param stringValue string to be parsed.
106 * @return an identifier representing the specified value.
107 * @throws NullPointerException if string value is null.
108 * @throws IllegalArgumentException if parsing fails for some other reason (invalid format, etc.).
109 * @see <a href="https://www.ietf.org/rfc/rfc4122.txt">RFC 4122 on UUIDs</a>
110 */
111 public static BeaconIdentifier parse(final String stringValue) {
112 return parse(stringValue, -1);
113 }
114
115 /**
116 * Variant of the parse method that allows specifying the byte length of the identifier.
117 *
118 * @param stringValue value to be parsed.
119 * @param desiredByteLength requested number of bytes to hold the identifier or -1 if not specified.
120 * @return the parsed identifier.
121 * @throws NullPointerException if string value is null.
122 * @throws IllegalArgumentException if parsing fails for some other reason (invalid format, etc.).
123 */
124 public static BeaconIdentifier parse(final String stringValue, final int desiredByteLength) {
125 if (stringValue == null) {
126 throw new NullPointerException(
127 "Identifiers cannot be constructed from null pointers but \"stringValue\" is null.");
128 }
129
130 if (HEX_PATTERN.matcher(stringValue).matches()) {
131 // parse hexadecimal format
132 return parseHex(stringValue.substring(2), desiredByteLength);
133 }
134
135 if (UUID_PATTERN.matcher(stringValue).matches()) {
136 // parse UUID format
137 return parseHex(stringValue.replace("-", ""), desiredByteLength);
138 }
139
140 if (DECIMAL_PATTERN.matcher(stringValue).matches()) {
141 // parse decimal format
142 var value = Integer.parseInt(stringValue);
143 if (desiredByteLength <= 0 || desiredByteLength == 2) {
144 return fromInt(value);
145 } else {
146 return fromLong(value, desiredByteLength);
147 }
148 }
149
150 if (HEX_PATTERN_NO_PREFIX.matcher(stringValue).matches()) {
151 // parse hexadecimal format without prefix
152 return parseHex(stringValue, desiredByteLength);
153 }
154
155 throw new IllegalArgumentException("Unable to parse identifier");
156 }
157
158 /**
159 * Creates an identifier backed by an array of length desiredByteLength.
160 *
161 * @param longValue a long to put into the identifier.
162 * @param desiredByteLength how many bytes to make the identifier.
163 * @return the parsed identifier.
164 * @throws IllegalArgumentException if desired number of bytes is negative.
165 */
166 public static BeaconIdentifier fromLong(long longValue, final int desiredByteLength) {
167 if (desiredByteLength < 0) {
168 throw new IllegalArgumentException("identifier length must be > 0");
169 }
170 final var newValue = new byte[desiredByteLength];
171 for (var i = desiredByteLength - 1; i >= 0; i--) {
172 newValue[i] = (byte) (longValue & 0xff);
173 longValue = longValue >> 8;
174 }
175 return new BeaconIdentifier(newValue);
176 }
177
178 /**
179 * Creates an identifier backed by a two byte array (big endian).
180 *
181 * @param intValue an integer between 0 and 65535 (inclusive).
182 * @return an identifier with the specified value.
183 * @throws IllegalArgumentException if provided value is out of valid range (from 0 to 65535).
184 */
185 public static BeaconIdentifier fromInt(final int intValue) {
186 if (intValue < 0 || intValue > MAX_INTEGER) {
187 throw new IllegalArgumentException(
188 "Identifiers can only be constructed from integers between 0 and " + MAX_INTEGER + " (inclusive).");
189 }
190
191 final var newValue = new byte[2];
192
193 newValue[0] = (byte) (intValue >> 8);
194 newValue[1] = (byte) (intValue);
195
196 return new BeaconIdentifier(newValue);
197 }
198
199 /**
200 * Creates an identifier from the specified byte array.
201 *
202 * @param bytes array to copy from.
203 * @param start the start index, inclusive.
204 * @param end the end index, exclusive.
205 * @param littleEndian whether the bytes are ordered in little endian.
206 * @return a new identifier.
207 * @throws NullPointerException if bytes is null.
208 * @throws ArrayIndexOutOfBoundsException if start or end are outside the bounds of the array.
209 * @throws IllegalArgumentException start is larger than end.
210 */
211 public static BeaconIdentifier fromBytes(
212 final byte[] bytes, final int start, final int end, final boolean littleEndian) {
213 if (bytes == null) {
214 throw new NullPointerException(
215 "Identifiers cannot be constructed from null pointers but \"bytes\" is null.");
216 }
217 if (start < 0 || start > bytes.length) {
218 throw new ArrayIndexOutOfBoundsException("start < 0 || start > bytes.length");
219 }
220 if (end > bytes.length) {
221 throw new ArrayIndexOutOfBoundsException("end > bytes.length");
222 }
223 if (start > end) {
224 throw new IllegalArgumentException("start > end");
225 }
226
227 final var byteRange = Arrays.copyOfRange(bytes, start, end);
228 if (littleEndian) {
229 reverseArray(byteRange);
230 }
231 return new BeaconIdentifier(byteRange);
232 }
233
234 /**
235 * Transforms a {@link UUID} into an identifier.
236 * No mangling with strings, only the underlying bytes of the
237 * UUID are used so this is fast and stable.
238 *
239 * @param uuid UUID to create identifier from.
240 * @return a new identifier.
241 */
242 public static BeaconIdentifier fromUuid(final UUID uuid) {
243 final var buf = ByteBuffer.allocate(16);
244 buf.putLong(uuid.getMostSignificantBits());
245 buf.putLong(uuid.getLeastSignificantBits());
246 return new BeaconIdentifier(buf.array());
247 }
248
249 /**
250 * Represents the value as a String. The output varies based on the length of the value.
251 * <ul><li>When the value is 2 bytes long: decimal, for example 6536.
252 * <li>When the value is 16 bytes long: uuid, for example 2f234454-cf6d-4a0f-adf2-f4911ba9ffa6
253 * <li>Else: hexadecimal prefixed with <code>0x</code>, for example 0x0012ab</ul>
254 *
255 * @return string representation of the current value.
256 */
257 @Override
258 public String toString() {
259 // Note: the toString() method is also used for serialization and deserialization. So
260 // toString() and parse() must always return objects that return true when you call equals()
261 if (value == null) {
262 return super.toString();
263 }
264
265 if (value.length == 2) {
266 return Integer.toString(toInt());
267 }
268 if (value.length == 16) {
269 return toUuid().toString();
270 }
271 return toHexString();
272 }
273
274 /**
275 * Represents the value as an <code>int</code>.
276 *
277 * @return value represented as int.
278 * @throws UnsupportedOperationException when value length is longer than 2.
279 */
280 public int toInt() {
281 if (value == null) {
282 return 0;
283 }
284
285 if (value.length > 2) {
286 throw new UnsupportedOperationException("Only supported for Identifiers with max byte length of 2");
287 }
288
289 var result = 0;
290 for (var i = 0; i < value.length; i++) {
291 result |= (value[i] & 0xFF) << ((value.length - i - 1) * 8);
292 }
293
294 return result;
295 }
296
297 /**
298 * Converts identifier to a byte array.
299 *
300 * @param bigEndian true if bytes are MSB first.
301 * @return a new byte array with a copy of the value.
302 */
303 public byte[] toByteArrayOfSpecifiedEndianness(final boolean bigEndian) {
304 if (value == null) {
305 return null;
306 }
307
308 final var copy = Arrays.copyOf(value, value.length);
309
310 if (!bigEndian) {
311 reverseArray(copy);
312 }
313
314 return copy;
315 }
316
317 /**
318 * Returns the byte length of this identifier.
319 *
320 * @return length of identifier.
321 */
322 public int getByteCount() {
323 return value != null ? value.length : 0;
324 }
325
326 /**
327 * Represents the value as a hexadecimal String. The String is prefixed with <code>0x</code>. For example
328 * 0x0034ab.
329 *
330 * @return value as hexadecimal String.
331 */
332 public String toHexString() {
333 if (value == null) {
334 return null;
335 }
336
337 final var l = value.length;
338 final var out = new char[l * 2 + 2];
339 out[0] = '0';
340 out[1] = 'x';
341 for (int i = 0, j = 2; i < l; i++) {
342 out[j] = HEX_DIGITS[(0xF0 & value[i]) >>> 4];
343 j++;
344 out[j] = HEX_DIGITS[0x0F & value[i]];
345 j++;
346 }
347 return new String(out);
348 }
349
350 /**
351 * Gives you the identifier as a UUID if possible.
352 *
353 * @return the identifier as a UUID.
354 * @throws UnsupportedOperationException if conversion to UUID fails.
355 */
356 public UUID toUuid() {
357 if (value == null) {
358 return null;
359 }
360
361 if (value.length != 16) {
362 throw new UnsupportedOperationException("Only Identifiers backed by a byte array with length of exactly 16 can be UUIDs.");
363 }
364 final var buf = ByteBuffer.wrap(value).asLongBuffer();
365 return new UUID(buf.get(), buf.get());
366 }
367
368 /**
369 * Gives you the byte array backing this identifier. Note that identifiers are immutable,
370 * so changing that the returned array will not result in a changed identifier.
371 *
372 * @return a deep copy of the data backing this identifier.
373 */
374 public byte[] toByteArray() {
375 return value != null ? value.clone() : null;
376 }
377
378 /**
379 * Computes hash code for this instance.
380 *
381 * @return this instance hash code.
382 */
383 @Override
384 public int hashCode() {
385 return value != null ? Arrays.hashCode(value) : 0;
386 }
387
388 /**
389 * Returns whether both identifiers contain equal value.
390 * This is the case when the value is the same and has the same length.
391 *
392 * @param that object to compare to.
393 * @return whether that equals this.
394 */
395 @Override
396 public boolean equals(final Object that) {
397 if (!(that instanceof BeaconIdentifier thatIdentifier)) {
398 return false;
399 }
400 return Arrays.equals(value, thatIdentifier.value);
401 }
402
403 /**
404 * Compares two identifiers.
405 * When the identifiers don't have the same length, the identifier having the shortest
406 * array is considered smaller than the other.
407 *
408 * @param that the other identifier.
409 * @return 0 if both identifiers are equal. Otherwise, returns -1 or 1 depending on
410 * which is bigger than th other.
411 * @see Comparable#compareTo(Object)
412 */
413 @Override
414 public int compareTo(final BeaconIdentifier that) {
415 if (value.length != that.value.length) {
416 return value.length < that.value.length ? -1 : 1;
417 }
418 for (var i = 0; i < value.length; i++) {
419 if (value[i] != that.value[i]) {
420 return value[i] < that.value[i] ? -1 : 1;
421 }
422 }
423 return 0;
424 }
425
426 /**
427 * Reverses provided array.
428 *
429 * @param bytes array to be reversed.
430 */
431 private static void reverseArray(final byte[] bytes) {
432 for (var i = 0; i < bytes.length / 2; i++) {
433 final var mirroredIndex = bytes.length - i - 1;
434 final var tmp = bytes[i];
435 bytes[i] = bytes[mirroredIndex];
436 bytes[mirroredIndex] = tmp;
437 }
438 }
439
440 /**
441 * Parses a string containing a beacon identifier in hexadecimal format.
442 *
443 * @param identifierString string to be parsed.
444 * @param desiredByteLength length of byte array to create to hold provided value.
445 * @return the parsed identifier.
446 */
447 private static BeaconIdentifier parseHex(final String identifierString, final int desiredByteLength) {
448 var str = identifierString.length() % 2 == 0 ? "" : "0";
449 str += identifierString.toUpperCase();
450 var len = str.length();
451
452 if (desiredByteLength > 0 && desiredByteLength < len / 2) {
453 str = str.substring(len - desiredByteLength * 2);
454 len = str.length();
455 }
456 if (desiredByteLength > 0 && desiredByteLength > len / 2) {
457 final var extraCharsToAdd = desiredByteLength * 2 - len;
458 final var sb = new StringBuilder();
459 while (sb.length() < extraCharsToAdd) {
460 sb.append("0");
461 }
462 str = sb + str;
463 len = str.length();
464 }
465
466 final var result = new byte[len / 2];
467 for (var i = 0; i < result.length; i++) {
468 result[i] = (byte) (Integer.parseInt(str.substring(i * 2, i * 2 + 2), 16) & 0xFF);
469 }
470 return new BeaconIdentifier(result);
471 }
472 }