View Javadoc
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 }