WoT-TD-Java is a Java library for the W3C Web of Things (WoT) Thing Description (TD).
What you can do with the current version:
- read/write TDs in RDF; this library uses RDF4J and supports primariy Turtle
- use property and action affordances with the data schemas defined by the W3C Recommendation
- JSON Schema keywords are mapped to IRIs using the JSON Schema in RDF vocabulary
- note: not all terms (and not all default values) are currently supported
- you can use composite data schemas (e.g., arrays of nested objects with semantic annotations)
- create HTTP and CoAP requests from a given TD, and parse HTTP and CoAP responses based on a given TD
Table of Contents
- Getting Started
- Retrieving and Parsing WoT TDs
- Creating and Writing WoT TDs
- Executing HTTP Requests
- Executing CoAP Requests
- Working with Semantic Payloads
You can easily add WoT-TD-Java to your project with JitPack.
Gradle:
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
Maven:
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
Gradle:
implementation 'com.github.Interactions-HSG:wot-td-java:v0.1.2'
Maven:
<dependency>
<groupId>com.github.Interactions-HSG</groupId>
<artifactId>wot-td-java</artifactId>
<version>v0.1.2</version>
</dependency>
To retrieve and parse a TD from a URL:
ThingDescription td = TDGraphReader.readFromURL(TDFormat.RDF_TURTLE, url);
Or from a local file:
ThingDescription td = TDGraphReader.readFromFile(TDFormat.RDF_TURTLE, filePath);
Or just parse it from a string:
ThingDescription td = TDGraphReader.readFromString(TDFormat.RDF_TURTLE, description);
The library provides a fluent API for constructing TDs programmatically. This feature can be useful, for instance, to expose TDs for resources hosted on origin servers, or to expose TDs via intermediaries in order to integrate legacy devices.
We can construct a TD for a lamp as follows:
ThingDescription td = (new ThingDescription.Builder("My Lamp Thing"))
.addThingURI("http://example.org/lamp123")
.addSemanticType("https://saref.etsi.org/core/LightSwitch")
.addAction(toggle)
.build();
The above code snippet creates a ThingDescription
for a lamp with the title My Lamp Thing
(mandatory property) and the semantic type saref:LightSwitch
(see SAREF ontology). The lamp exposes a toggle
action, which can be defined in a similar manner:
ActionAffordance toggle = new ActionAffordance.Builder("toggle", toggleForm)
.addTitle("Toggle")
.addSemanticType("https://saref.etsi.org/core/ToggleCommand")
.addInputSchema(new ObjectSchema.Builder()
.addSemanticType("https://saref.etsi.org/core/OnOffState")
.addProperty("status", new BooleanSchema.Builder()
.build())
.addRequiredProperties("status")
.build())
.build();
Our toggle
action has the semantic type saref:ToggleCommand
and takes as input an ObjectSchema that represents a saref:OnOffState
. The object schema requires a status
property of type BooleanSchema.
The toggle
action is exposed via a Form, which
is a type of hypermedia control. To create a form, we have to specify a target URI, the method to be
used, etc.:
Form toggleForm = new Form.Builder("http://mylamp.example.org/toggle")
.setMethodName("PUT")
.build();
We can serialize our TD in Turtle like so (support for other formats is to be added):
String description = new TDGraphWriter(td)
.setNamespace("td", "https://www.w3.org/2019/wot/td#")
.setNamespace("htv", "http://www.w3.org/2011/http#")
.setNamespace("hctl", "https://www.w3.org/2019/wot/hypermedia#")
.setNamespace("wotsec", "https://www.w3.org/2019/wot/security#")
.setNamespace("dct", "http://purl.org/dc/terms/")
.setNamespace("js", "https://www.w3.org/2019/wot/json-schema#")
.setNamespace("saref", "https://saref.etsi.org/core/")
.write();
The generated TD is:
@prefix td: <https://www.w3.org/2019/wot/td#> .
@prefix htv: <http://www.w3.org/2011/http#> .
@prefix hctl: <https://www.w3.org/2019/wot/hypermedia#> .
@prefix wotsec: <https://www.w3.org/2019/wot/security#> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix js: <https://www.w3.org/2019/wot/json-schema#> .
@prefix saref: <https://saref.etsi.org/core/> .
<http://example.org/lamp123> a td:Thing, saref:LightSwitch;
td:title "My Lamp Thing";
td:hasSecurityConfiguration [ a wotsec:NoSecurityScheme
];
td:hasActionAffordance [ a td:ActionAffordance, saref:ToggleCommand;
td:name "toggle";
td:title "Toggle";
td:hasForm [
htv:methodName "PUT";
hctl:hasTarget <http://mylamp.example.org/toggle>;
hctl:forContentType "application/json";
hctl:hasOperationType td:invokeAction
];
td:hasInputSchema [ a js:ObjectSchema, saref:OnOffState;
js:properties [ a js:BooleanSchema;
js:propertyName "status"
];
js:required "status"
]
] .
In the above listing, notice the TDGraphWriter
added for us a number of default values specified by the W3C WoT TD recommendation (e.g., application/json
for our form's content type).
WoT-TD-Java comes with a built-in HTTP client that can be used to execute HTTP requests based on a given TD. This feature is currently supported for property and action affordances.
First, we need to retrieve an affordance from the TD. For instance, we can retrieve an action affordance based on a semantic type:
String affordanceType = "https://saref.etsi.org/core/ToggleCommand";
Optional<ActionAffordance> action = td.getFirstActionBySemanticType(affordanceType);
We can also retrieve an action by name:
Optional<ActionAffordance> action = td.getActionByName("toggle");
Or we can retrive the list of all available actions:
List<ActionAffordance> actions = td.getActions();
Retrieving property affordances works in a similar manner.
Once an affordance is found, we then need to retrieve a form for exploiting the affordance. We can retrieve the first available form like so:
if (affordanceOpt.isPresent()) {
Optional<Form> form = action.get().getFirstForm();
(...)
}
Or we can retrieve a form for a given operation type as defined by the W3C WoT TD Recommendation:
Optional<Form> form = action.get().getFirstFormForOperationType(TD.invokeAction);
If a form is found, we can use it to create and execute an HTTP request for a given operation type:
if (form.isPresent()) {
TDHttpRequest request = new TDHttpRequest(form.get(),TD.invokeAction);
TDHttpResponse response = request.execute();
System.out.println("Received response with status code: " + response.getStatusCode());
}
In our above TD for a lamp, the toggle
action affordance does not use a payload. To construct
requests with payloads, see Working with Semantic Payloads.
At the moment, WoT-TD-Java only supports the APIKeySecurityScheme
(see Security Vocabulary Definitions).
To set an API key on a request, we first need to retrieve the API key security scheme from the TD. This step is necessary because the security scheme will typically contain information specific to the interface described by the TD (e.g., a specific HTTP header field).
Optional<SecurityScheme> securityScheme = td.getFirstSecuritySchemeByType(WoTSec.APIKeySecurityScheme);
If the security scheme is found, we can use it to set the security token:
if (securityScheme.isPresent()) {
request.setAPIKey((APIKeySecurityScheme) securityScheme.get(), token);
}
WoT-TD-Java will use the security scheme to add the security token in the right place (e.g., using the correct HTTP header field).
Executing CoAP requests is similar to executing HTTP requests. First we need to retrieve a form for a given affordance in a TD, and then we can create and execute a CoAP request.
For instance:
Optional<PropertyAffordance> humidity = td.getPropertyByName("humidity");
if (humidity.isPresent()) {
Optional<Form> form = humidity.get().getFirstFormForOperationType(TD.readProperty);
if(form.isPresent()) {
TDCoapRequest request = new TDCoapRequest(form.get(),TD.readProperty);
TDCoapResponse response=request.execute();
System.out.println("Received response with status code (raw code in decimal): " + response.getStatusCode());
System.out.println("Received response with status code (code name): " + response.getStatusCodeName());
}
}
Similar to HTTP requests and responses, CoAP requests and responses can also carry payloads.
Authentication is not currently supported for CoAP requests.
WoT-TD-Java provides support for adding payloads to HTTP and CoAP requests, and for extracting payloads from HTTP and CoAP responses. Furthermore, if the payloads carry semantic annotations, we can work with semantic data to decouple from interface-specific details (e.g., JSON schemas).
WoT-TD-Java implements all Data Schema Vocabulary Definitions specified in the W3C WoT TD Recommendation.
A payload can be an object (ObjectSchema
), an array (ArraySchema
), a
primitive data type (BooleanSchema
, NumberSchema
, IntegerSchema
, StringSchema
), or null (NullSchema
).
For instance, a device may provide an action affordance to register its user, where the action affordance is specified by the device's TD as follows:
[ a td:ActionAffordance, onto:LogIn;
td:name "logIn";
td:title "Log In";
td:hasForm [
htv:methodName "POST";
hctl:hasTarget <https://api.interactions.ics.unisg.ch/xarm/user>;
hctl:forContentType "application/json";
hctl:hasOperationType td:invokeAction
];
td:hasInputSchema [
a js:ObjectSchema, foaf:Agent;
js:properties
[ a js:StringSchema, foaf:Name;
js:propertyName "name"
],
[ a js:StringSchema, foaf:Mbox;
js:propertyName "email"
];
js:required "name", "email"
]
] ;
This action affordance has an input schema of type js:ObjectSchema
. The object contains two properties
(name
and email
) to identify the user. Each of these properties carries two semantic annotations:
js:StringSchema
, which reflects the primitive data types of these properties;- annotations using the FOAF vocabulary (
foaf:Name
andfoaf:Mbox
, respectively).
Such semantic descriptions allow us to program clients against semantic models rather than interface-specific data formats. We can use the semantic annotations to tell WoT-TD-Java how to construct a payload that follows the specified schema. To do so, we will need to create a map between the annotations and our data.
Map<String, Object> payload = new HashMap<>();
payload.put("http://xmlns.com/foaf/0.1/Name", "Andrei Ciortea");
payload.put("http://xmlns.com/foaf/0.1/Mbox", "[email protected]");
Note here that:
- we need to use the fully qualified URIs for the semantic annotations
- we are using the FOAF annotations, which allow WoT-TD-Java to discriminate between the two properties; if instead
we would use
js:StringSchema
, we would have no guarantee on the order in which the values are inserted into the constructed payload
After we construct our payload, we need to set the payload on the request with the data schema definition from the TD:
// Retrieve the input data schema from the action affordance
Optional<DataSchema> inputSchema = action.getInputSchema();
if (inputSchema.isPresent()){
request.setObjectPayload((ObjectSchema) inputSchema.get(), payload);
}
WoT-TD-Java will use the input data schema definition and the provided data to construct the request payload.
Setting payloads with other data schema definitions and/or setting payloads for CoAP requests works in a similar manner.
We can extract payloads from responses similar to how we set payloads for requests.
Given the following property affordance for reading a humidity value using a CoAP device:
[ a td:PropertyAffordance, miro:Humidity, js:ObjectSchema;
td:name "humidity";
td:title "Humidity";
td:hasForm [
cov:methodName "GET";
hctl:hasTarget <coap://130.82.171.10:5683/humidity>;
hctl:forContentType "application/json";
hctl:hasOperationType td:observeProperty;
hctl:forSubProtocol cov:observe
],
[
cov:methodName "GET";
hctl:hasTarget <coap://130.82.171.10:5683/humidity>;
hctl:forContentType "application/json";
hctl:hasOperationType td:readProperty
];
td:isObservable true;
js:properties
[ a js:NumberSchema, miro:HumidityValue;
js:propertyName "value";
js:minimum 1.5E1;
js:maximum 4.0E1
]
] ;
We first extract a form for reading the property:
Optional<Form> form = humidity.getFirstFormForOperationType(TD.readProperty);
We then use the form to execute a CoAP request:
if (form.isPresent()) {
TDCoapRequest request=new TDCoapRequest(form.get(),TD.readProperty);
TDCoapResponse response = request.execute();
(...)
}
After receiving the CoAP response, we can extract the ObjectSchema
payload as follows:
Map<String, Object> payload = response.getPayloadAsObject((ObjectSchema) humidity.getDataSchema());
payload.get("https://interactions.ics.unisg.ch/mirogate#HumidityValue");
The values for primitive data types are wrapped in equivalent Java objects. In this example, the humidity
value is retrieved as a Double
.