org.sputnikdev.bluetooth.URL Maven / Gradle / Ivy
package org.sputnikdev.bluetooth;
/*-
* #%L
* org.sputnikdev:bluetooth-utils
* %%
* Copyright (C) 2017 Sputnik Dev
* %%
* 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.
* #L%
*/
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Class {@code URL} represents a Uniform Resource Locator for bluetooth resources,
* e.g. bluetooth adapters, bluetooth devices, GATT services, GATT characteristics and GATT fields.
* For example, if you have an adapter with MAC address B8:27:EB:60:0C:43,
* a device with MAC address 54:60:09:95:86:01 is connected to the adapter,
* the device has a GATT service with UUID 0000180f-0000-1000-8000-00805f9b34fb,
* the service has a characteristic with UUID 00002a19-0000-1000-8000-00805f9b34fb
* and the characteristic has a field called "Level", then a URL for the field can be:
* /B8:27:EB:60:0C:43/54:60:09:95:86:01/0000180f-0000-1000-8000-00805f9b34fb/00002a19-0000-1000-8000-00805f9b34fb/Level
* Or a URL for the service can be:
* /B8:27:EB:60:0C:43/54:60:09:95:86:01/0000180f-0000-1000-8000-00805f9b34fb
* Similarly, it is easy to define a URL for other components, e.g. adapters, devices and characteristics.
*
* If there are more than one protocol used to access Bluetooth devices (e.g. DBus, serial interface etc),
* then it is possible to define protocol as well:
* tinyb://B8:27:EB:60:0C:43/54:60:09:95:86:01/0000180f-0000-1000-8000-00805f9b34fb/
* 00002a19-0000-1000-8000-00805f9b34fb/Level
*
* @author Vlad Kolotov
*/
public class URL implements Comparable {
private static final Pattern URL_PATTERN =
Pattern.compile("^((?\\w*):)?/(?(\\w\\w:){5}\\w\\w)?"
+ "(/(?(\\w\\w:){5}\\w\\w)?(\\[name=(?[\\w\\s'-]+)\\])?)?"
+ "(/(?[0-9A-Fa-f]{4,8}(-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12})?))?"
+ "(/(?[0-9A-Fa-f]{4,8}(-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12})?))?"
+ "(/(?\\w+))?$");
public static final URL ROOT = new URL("/");
private final String protocol;
private final String adapterAddress;
private final String deviceAddress;
private final Map deviceAttributes = new HashMap<>();
private final String serviceUUID;
private final String characteristicUUID;
private final String fieldName;
/**
* Constructor to build a URL object from its text representation.
* E.g. / bluetooth adapter / bluetooth device / GATT service / GATT characteristic / Characteristic Field Name
*
* The following are some examples of a valid URL:
*
*
URL pointing to a field:
* /B8:27:EB:60:0C:43/54:60:09:95:86:01/0000180f-0000-1000-8000-00805f9b34fb/
* 00002a19-0000-1000-8000-00805f9b34fb/Level
*
*
URL pointing to a characteristic:
* /B8:27:EB:60:0C:43/54:60:09:95:86:01/0000180f-0000-1000-8000-00805f9b34fb/00002a19-0000-1000-8000-00805f9b34fb
*
*
URL pointing to a service:
* /B8:27:EB:60:0C:43/54:60:09:95:86:01/0000180f-0000-1000-8000-00805f9b34fb
*
*
URL pointing to a bluetooth device:
* /B8:27:EB:60:0C:43/54:60:09:95:86:01
*
*
URL point to a bluetooth adapter:
* /B8:27:EB:60:0C:43
*
*
URL pointing to the "root":
* /
*
*
Services and characteristic UUIDs can be in a short form:
* /B8:27:EB:60:0C:43/54:60:09:95:86:01/0000180f/00002a19/Level
* /B8:27:EB:60:0C:43/54:60:09:95:86:01/180f/2a19/Level
*
*
* @param url text representation of a URL
*/
public URL(String url) {
Matcher matcher = URL_PATTERN.matcher(url);
if (matcher.find()) {
protocol = toLowerCase(matcher.group("protocol"));
adapterAddress = toUpperCase(matcher.group("adapter"));
deviceAddress = toUpperCase(matcher.group("device"));
String deviceName = matcher.group("devicename");
if (deviceName != null) {
deviceAttributes.put("name", deviceName);
}
serviceUUID = toLowerCase(matcher.group("service"));
characteristicUUID = toLowerCase(matcher.group("charact"));
fieldName = matcher.group("field");
validate();
} else {
throw new IllegalArgumentException("Invalid URL: " + url);
}
}
/**
* Constructs the "root" URL object. All components of the such URL are nulls.
* A text representation of the "root" URL is "/"
*/
public URL() {
this(null, null);
}
/**
* Constructs a URL pointing to a bluetooth device.
* @param adapterAddress bluetooth adapter MAC address
* @param deviceAddress bluetooth device MAC address
*/
public URL(String adapterAddress, String deviceAddress) {
this(adapterAddress, deviceAddress, null, null, null);
}
/**
* Constructs a URL pointing to a GATT service.
* @param protocol protocol name
* @param adapterAddress bluetooth adapter MAC address
* @param deviceAddress bluetooth device MAC address
*/
public URL(String protocol, String adapterAddress, String deviceAddress) {
this(protocol, adapterAddress, deviceAddress, null, null, null);
}
/**
* Constructs a URL pointing to a GATT characteristic.
* @param adapterAddress bluetooth adapter MAC address
* @param deviceAddress bluetooth device MAC address
* @param serviceUUID UUID of a GATT service
* @param characteristicUUID UUID of a GATT characteristic
*/
public URL(String adapterAddress, String deviceAddress, String serviceUUID, String characteristicUUID) {
this(adapterAddress, deviceAddress, serviceUUID, characteristicUUID, null);
}
/**
* Constructs a URL pointing to a GATT field.
* @param adapterAddress bluetooth adapter MAC address
* @param deviceAddress bluetooth device MAC address
* @param serviceUUID UUID of a GATT service
* @param characteristicUUID UUID of a GATT characteristic
* @param fieldName name of a field of the characteristic
*/
public URL(String adapterAddress, String deviceAddress, String serviceUUID, String characteristicUUID,
String fieldName) {
this(null, adapterAddress, deviceAddress, serviceUUID, characteristicUUID, fieldName);
}
/**
* Constructs a URL pointing to a GATT field.
* @param protocol protocol name
* @param adapterAddress bluetooth adapter MAC address
* @param deviceAddress bluetooth device MAC address
* @param serviceUUID UUID of a GATT service
* @param characteristicUUID UUID of a GATT characteristic
* @param fieldName name of a field of the characteristic
*/
public URL(String protocol, String adapterAddress, String deviceAddress, String serviceUUID,
String characteristicUUID, String fieldName) {
this.protocol = toLowerCase(protocol);
this.adapterAddress = toUpperCase(adapterAddress);
this.deviceAddress = toUpperCase(deviceAddress);
this.serviceUUID = toLowerCase(serviceUUID);
this.characteristicUUID = toLowerCase(characteristicUUID);
this.fieldName = fieldName;
validate();
}
/**
* Copy constructor.
* @param protocol protocol name
* @param adapterAddress bluetooth adapter MAC address
* @param deviceAttributes bluetooth device attributes
* @param serviceUUID UUID of a GATT service
* @param characteristicUUID UUID of a GATT characteristic
* @param fieldName name of a field of the characteristic
*/
public URL(String protocol, String adapterAddress, Map deviceAttributes,
String serviceUUID, String characteristicUUID, String fieldName) {
this.protocol = toLowerCase(protocol);
this.adapterAddress = toUpperCase(adapterAddress);
this.deviceAddress = null;
if (deviceAttributes != null && !deviceAttributes.isEmpty()) {
this.deviceAttributes.putAll(deviceAttributes);
}
this.serviceUUID = toLowerCase(serviceUUID);
this.characteristicUUID = toLowerCase(characteristicUUID);
this.fieldName = fieldName;
validate();
}
URL(String protocol, String adapterAddress, String deviceAddress, Map deviceAttributes,
String serviceUUID, String characteristicUUID, String fieldName) {
this.protocol = toLowerCase(protocol);
this.adapterAddress = toUpperCase(adapterAddress);
this.deviceAddress = toUpperCase(deviceAddress);
if (deviceAttributes != null && !deviceAttributes.isEmpty()) {
this.deviceAttributes.putAll(deviceAttributes);
}
this.serviceUUID = toLowerCase(serviceUUID);
this.characteristicUUID = toLowerCase(characteristicUUID);
this.fieldName = fieldName;
validate();
}
/**
* Makes a copy of a given URL with some additional components.
* @param protocol protocol name
* @return a copy of a given URL with some additional components
*/
public URL copyWithProtocol(String protocol) {
return new URL(protocol, this.adapterAddress, this.deviceAddress, this.deviceAttributes, this.serviceUUID,
this.characteristicUUID, this.fieldName);
}
/**
* Makes a copy of a given URL with some additional components.
* @param adapterAddress bluetooth adapter MAC address
* @return a copy of a given URL with some additional components
*/
public URL copyWithAdapter(String adapterAddress) {
return new URL(this.protocol, adapterAddress, this.deviceAddress, this.deviceAttributes, this.serviceUUID,
this.characteristicUUID, this.fieldName);
}
/**
* Makes a copy of a given URL with some additional components.
* @param deviceAddress bluetooth device MAC address
* @return a copy of a given URL with some additional components
*/
public URL copyWithDevice(String deviceAddress) {
return new URL(this.protocol, this.adapterAddress, deviceAddress, this.deviceAttributes, this.serviceUUID,
this.characteristicUUID, this.fieldName);
}
/**
* Makes a copy of a given URL with some additional components.
* @param deviceAddress bluetooth device MAC address
* @param attr device attributes
* @return a copy of a given URL with some additional components
*/
public URL copyWithDevice(String deviceAddress, Map attr) {
return new URL(this.protocol, this.adapterAddress, deviceAddress, attr, this.serviceUUID,
this.characteristicUUID, this.fieldName);
}
/**
* Makes a copy of a given URL with a new device name and a single attribute.
* @param deviceAddress bluetooth device MAC address
* @param attrName attribute name
* @param attrValue attribute value
* @return a copy of a given URL with some additional components
*/
public URL copyWithDevice(String deviceAddress, String attrName, String attrValue) {
return new URL(this.protocol, this.adapterAddress, deviceAddress, Collections.singletonMap(attrName, attrValue),
this.serviceUUID, this.characteristicUUID, this.fieldName);
}
/**
* Makes a copy of a given URL with some additional components.
* @param serviceUUID UUID of a GATT service
* @return a copy of a given URL with some additional components
*/
public URL copyWithService(String serviceUUID) {
return new URL(this.protocol, this.adapterAddress, this.deviceAddress, this.deviceAttributes,
serviceUUID, null, null);
}
/**
* Makes a copy of a given URL with some additional components.
* @param characteristicUUID UUID of a GATT characteristic
* @return a copy of a given URL with some additional components
*/
public URL copyWithCharacteristic(String characteristicUUID) {
return new URL(this.protocol, this.adapterAddress, this.deviceAddress, this.deviceAttributes,
serviceUUID, characteristicUUID, null);
}
/**
* Makes a copy of a given URL with some additional components.
* @param serviceUUID UUID of a GATT service
* @param characteristicUUID UUID of a GATT characteristic
* @return a copy of a given URL with some additional components
*/
public URL copyWith(String serviceUUID, String characteristicUUID) {
return new URL(this.protocol, this.adapterAddress, this.deviceAddress, this.deviceAttributes,
serviceUUID, characteristicUUID, null);
}
/**
* Makes a copy of a given URL with some additional components.
* @param serviceUUID UUID of a GATT service
* @param characteristicUUID UUID of a GATT characteristic
* @param fieldName name of a field of the characteristic
* @return a copy of a given URL with some additional components
*/
public URL copyWith(String serviceUUID, String characteristicUUID, String fieldName) {
return new URL(this.protocol, this.adapterAddress, this.deviceAddress, this.deviceAttributes, serviceUUID,
characteristicUUID, fieldName);
}
/**
* Makes a copy of a given URL with some additional components.
* @param fieldName name of a field of the characteristic
* @return a copy of a given URL with some additional components
*/
public URL copyWithField(String fieldName) {
return new URL(this.protocol, this.adapterAddress, this.deviceAddress, this.deviceAttributes, this.serviceUUID,
this.characteristicUUID, fieldName);
}
/**
* Returns a copy of a given URL truncated to its device component.
* Service, characteristic and field components will be null.
* @return a copy of a given URL truncated to the device component
*/
public URL getDeviceURL() {
return new URL(this.protocol, this.adapterAddress, this.deviceAddress, this.deviceAttributes, null, null, null);
}
public Map getDeviceAttributes() {
return Collections.unmodifiableMap(deviceAttributes);
}
public String getDeviceName() {
return deviceAttributes.get("name");
}
/**
* Returns a copy of a given URL truncated to its service component.
* Characteristic and field components will be null.
* @return a copy of a given URL truncated to the service component
*/
public URL getServiceURL() {
return new URL(this.protocol, this.adapterAddress, this.deviceAddress, this.deviceAttributes,
this.serviceUUID, null, null);
}
/**
* Returns a copy of a given URL truncated to its characteristic component.
* The field component will be null.
* @return a copy of a given URL truncated to the service component
*/
public URL getCharacteristicURL() {
return new URL(this.protocol, this.adapterAddress, this.deviceAddress, this.deviceAttributes, this.serviceUUID,
this.characteristicUUID, null);
}
/**
* Returns a copy of a given URL truncated to its adapter component.
* Device, Service, characteristic and field components will be null.
* @return a copy of a given URL truncated to the adapter component
*/
public URL getAdapterURL() {
return new URL(this.protocol, this.adapterAddress, null);
}
/**
* Returns a copy of a given URL truncated to its protocol component.
* Adapter, Device, Service, characteristic and field components will be null.
* @return a copy of a given URL truncated to the adapter component
*/
public URL getProtocolURL() {
return new URL(this.protocol, null, null);
}
/**
* Returns bluetooth protocol.
* @return bluetooth protocol
*/
public String getProtocol() {
return protocol;
}
/**
* Returns bluetooth adapter address (MAC address).
* @return adapter address (MAC address)
*/
public String getAdapterAddress() {
return adapterAddress;
}
/**
* Returns bluetooth device address.
* @return bluetooth device address
*/
public String getDeviceAddress() {
return deviceAddress;
}
/**
* Returns bluetooth device composite address (a combination of the address and its attributes).
* @return bluetooth device composite address
*/
public String getDeviceCompositeAddress() {
String device = deviceAddress;
if (!deviceAttributes.isEmpty()) {
device = (device != null ? device : "") + deviceAttributes.entrySet().stream()
.map(Map.Entry::toString).collect(Collectors.joining(",", "[", "]"));
}
return device;
}
/**
* Returns service UUID.
* @return service UUID
*/
public String getServiceUUID() {
return serviceUUID;
}
/**
* Return characteristic UUID.
* @return characteristic UUID
*/
public String getCharacteristicUUID() {
return characteristicUUID;
}
/**
* Returns field name.
* @return field name
*/
public String getFieldName() {
return fieldName;
}
/**
* Checks whether a given URL is the "root" URL object.
* @return true if it is the "root" URL, false otherwise
*/
public boolean isProtocol() {
return protocol != null && adapterAddress == null;
}
/**
* Checks whether a given URL is the "root" URL object.
* @return true if it is the "root" URL, false otherwise
*/
public boolean isRoot() {
return adapterAddress == null;
}
/**
* Checks whether a given URL is pointing to a bluetooth adapter.
* @return true if a given URL is pointing to a bluetooth adapter, false otherwise
*/
public boolean isAdapter() {
return adapterAddress != null && deviceAddress == null && deviceAttributes.isEmpty();
}
/**
* Checks whether a given URL is pointing to a bluetooth device.
* @return true if a given URL is pointing to a bluetooth device, false otherwise
*/
public boolean isDevice() {
return (deviceAddress != null || !deviceAttributes.isEmpty()) && serviceUUID == null;
}
/**
* Checks whether a given URL is pointing to a GATT service.
* @return true if a given URL is pointing to a GATT service, false otherwise
*/
public boolean isService() {
return serviceUUID != null && characteristicUUID == null;
}
/**
* Checks whether a given URL is pointing to a GATT characteristic.
* @return true if a given URL is pointing to a GATT characteristic, false otherwise
*/
public boolean isCharacteristic() {
return characteristicUUID != null && fieldName == null;
}
/**
* Checks whether a given URL is pointing to a field of a characteristic.
* @return true if a given URL is pointing to a field of a characteristic, false otherwise
*/
public boolean isField() {
return fieldName != null;
}
/**
* Returns a new URL representing its "parent" component.
* @return a new URL representing its "parent" component
*/
public URL getParent() {
if (isField()) {
return getCharacteristicURL();
} else if (isCharacteristic()) {
return getServiceURL();
} else if (isService()) {
return getDeviceURL();
} else if (isDevice()) {
return getAdapterURL();
} else if (isAdapter()) {
return getProtocolURL();
} else {
return null;
}
}
/**
* Checks whether the URL is a descendant of a URL provided as an argument.
* If the provided URL does not specify protocol, then it will match any protocol of the URL
* @param url a bluetooth URL
* @return true if the URL is a descendant of a provided URL
*/
public boolean isDescendant(URL url) {
if (url.protocol != null && protocol != null && !protocol.equals(url.protocol)) {
return false;
}
if (adapterAddress != null && url.adapterAddress == null) {
return true;
}
if (adapterAddress != null && !adapterAddress.equals(url.adapterAddress)) {
return false;
}
if (deviceAddress != null && url.deviceAddress == null) {
return true;
}
if (deviceAddress != null && !deviceAddress.equals(url.deviceAddress)) {
return false;
}
if (serviceUUID != null && url.serviceUUID == null) {
return true;
}
if (serviceUUID != null ? !serviceUUID.equals(url.serviceUUID) : url.serviceUUID != null) {
return false;
}
if (characteristicUUID != null && url.characteristicUUID == null) {
return true;
}
if (characteristicUUID != null
? !characteristicUUID.equals(url.characteristicUUID) :
url.characteristicUUID != null) {
return false;
}
return fieldName != null && url.fieldName == null;
}
@Override
public String toString() {
LinkedList fields = new LinkedList<>();
String deviceAddress = getDeviceCompositeAddress();
if (adapterAddress == null && deviceAddress != null) {
fields.add("");
} else {
fields.add(adapterAddress);
}
fields.add(deviceAddress);
fields.add(serviceUUID);
fields.add(characteristicUUID);
fields.add(fieldName);
while (!fields.isEmpty() && fields.getLast() == null) {
fields.removeLast();
}
return (protocol != null ? protocol + ":" : "") + "/" + String.join("/", fields);
}
@Override
public boolean equals(Object that) {
if (this == that) {
return true;
}
if (that == null || getClass() != that.getClass()) {
return false;
}
URL url = (URL) that;
if (protocol != null ? !protocol.equals(url.protocol) : url.protocol != null) {
return false;
}
if (adapterAddress != null ? !adapterAddress.equals(url.adapterAddress) : url.adapterAddress != null) {
return false;
}
if (deviceAddress != null ? !deviceAddress.equals(url.deviceAddress) : url.deviceAddress != null) {
return false;
}
if (!deviceAttributes.equals(url.deviceAttributes)) {
return false;
}
if (serviceUUID != null ? !serviceUUID.equals(url.serviceUUID) : url.serviceUUID != null) {
return false;
}
if (characteristicUUID != null
? !characteristicUUID.equals(url.characteristicUUID) :
url.characteristicUUID != null) {
return false;
}
return fieldName != null && url.fieldName != null
? fieldName.toLowerCase().equals(url.fieldName.toLowerCase()) : fieldName == url.fieldName;
}
@Override
public int hashCode() {
int result = protocol != null ? protocol.hashCode() : 0;
result = 31 * result + (adapterAddress != null ? adapterAddress.hashCode() : 0);
result = 31 * result + (deviceAddress != null ? deviceAddress.hashCode() : 0);
result = 31 * result + deviceAttributes.hashCode();
result = 31 * result + (serviceUUID != null ? serviceUUID.hashCode() : 0);
result = 31 * result + (characteristicUUID != null ? characteristicUUID.hashCode() : 0);
result = 31 * result + (fieldName != null ? fieldName.toLowerCase().hashCode() : 0);
return result;
}
@Override
public int compareTo(URL that) {
int result = compareFields(adapterAddress, that.adapterAddress);
if (result != 0) {
return result;
}
result = compareFields(deviceAddress, that.deviceAddress);
if (result != 0) {
return result;
}
//TODO add device attributes comparison
result = compareFields(serviceUUID, that.serviceUUID);
if (result != 0) {
return result;
}
result = compareFields(characteristicUUID, that.characteristicUUID);
if (result != 0) {
return result;
}
return compareFields(fieldName, that.fieldName);
}
private void validate() {
if (fieldName != null && characteristicUUID == null
|| characteristicUUID != null && serviceUUID == null
|| serviceUUID != null && (deviceAddress == null && deviceAttributes.isEmpty())) {
throw new IllegalArgumentException("Invalid url: " + toString());
}
}
private int compareFields(String field1, String field2) {
if (field1 == null && field2 == null) {
return 0;
} else if (field2 == null) {
return 1;
} else if (field1 == null) {
return -1;
} else {
return field1.compareTo(field2);
}
}
private static String toUpperCase(String str) {
return Optional.ofNullable(str).map(String::toUpperCase).orElse(null);
}
private static String toLowerCase(String str) {
return Optional.ofNullable(str).map(String::toLowerCase).orElse(null);
}
}