io.github.aas-core-works:aas-core3.0-java 1.0.2 API
- Install the SDK
- Programmatically create, get and set properties of an AAS model
- Iterate over a model
- Verify a model
- De/serialize a model from and to JSON
- De/serialize a model from and to XML
- Enhance model instances with your custom data
Install the SDK
First use git and clone aas-core3.0-java repository. In the the root directory of the cloned repository you will find the pom.xml used by maven to build the SDK artefact. Simply run mvn install and include the SDK as a dependency in your project by adding it to your own pom.xml:
<dependency>
<groupId>io.github.aas-core-works</groupId>
<artifactId>aas-core3.0-java</artifactId>
<version>1.0.2</version>
</dependency>
Programmatically create, get and set properties of an AAS model
The package aas_core.aas3_0 contains all the classes of the meta-model. You can simply use their constructors to create an AAS model. Usually you start bottom-up, all the way up to the Environment. Alternatively the SDK also provides you with builders for all classes of the meta-model.Example: Create an Environment with a Submodel
// We create the first element with the constructor.
IProperty someElement = new Property(DataTypeDefXsd.INT);
// We use setters to set the optional attributes.
someElement.setIdShort("someElement");
someElement.setValue("1984");
// We create the second element.
IBlob anotherElement = new Blob("application/octet-stream");
anotherElement.setIdShort("anotherElement");
anotherElement.setValue(new byte[]{0, 1, 2, 3});
// We also directly access the element attributes.
anotherElement.getValue().ifPresent(bytes -> bytes[3] = 4);
// We nest the elements in a submodel.
ISubmodel submodel = new Submodel("some-unique-global-identifier");
List<ISubmodelElement> submodelElements = new ArrayList<>();
submodelElements.add(someElement);
submodelElements.add(anotherElement);
submodel.setSubmodelElements(submodelElements);
// We now create the environment to wrap it all up.
List<ISubmodel> submodels = new ArrayList<>();
submodels.add(submodel);
IEnvironment environment = new Environment(null, submodels, null);
// We can directly access the properties from the children
// as well.
((Blob) environment
.getSubmodels().get()
.get(0)
.getSubmodelElements().get()
.get(1)).getValue().get()[3] = 3;
// Now we can do something with the environment...
Iterate over a model
The SDK provides various ways how you can loop through the elements of the model, and how these elements can be transformed. Each following section will look into one of the approaches.DescendOnce and Descend
The SDK provides two methods in the most general interface IClass, descendOnce and descend, which you can use to loop through the instances. Both descendOnce and descend iterate over model children of an IClass. DescendOnce, as it names suggests, stops after all the children has been iterated over. Descend continues recursively to grand-children etc. Here is a short example how you can iterate over all the instances of a model, and output their model types:
// We prepare the environment.
ISubmodelElement someProperty = new Property(DataTypeDefXsd.BOOLEAN);
someProperty.setIdShort("someProperty");
ISubmodelElement anotherProperty = new Property(DataTypeDefXsd.BOOLEAN);
anotherProperty.setIdShort("anotherProperty");
ISubmodelElement yetAnotherProperty = new Property(DataTypeDefXsd.BOOLEAN);
yetAnotherProperty.setIdShort("yetAnotherProperty");
ISubmodel submodel = new Submodel("some-unique-global-identifier");
List<ISubmodelElement> submodelElements = new ArrayList<>();
submodelElements.add(someProperty);
submodelElements.add(anotherProperty);
submodelElements.add(yetAnotherProperty);
submodel.setSubmodelElements(submodelElements);
List<ISubmodel> submodels = new ArrayList<>();
submodels.add(submodel);
IEnvironment environment = new Environment(null, submodels, null);
environment.descend().forEach(iClass -> System.out.println(iClass.getClass().getSimpleName()));
// Prints:
// Submodel
// Property
// Property
// Property
Filtering with descent and descentOnce
Filtering with descent and descentOnce is fairly easy, as we can write the filtering code within a for loop. Here is a short snippet, adapted from the previous example, where we filter for all properties which have int as their value type:
// We prepare the environment.
ISubmodelElement boolProperty = new Property(DataTypeDefXsd.BOOLEAN);
boolProperty.setIdShort("boolProperty");
ISubmodelElement intProperty = new Property(DataTypeDefXsd.INT);
intProperty.setIdShort("intProperty");
ISubmodelElement anotherIntProperty = new Property(DataTypeDefXsd.INT);
anotherIntProperty.setIdShort("anotherIntProperty");
ISubmodel submodel = new Submodel("some-unique-global-identifier");
List<ISubmodelElement> submodelElements = new ArrayList<>();
submodelElements.add(boolProperty);
submodelElements.add(intProperty);
submodelElements.add(anotherIntProperty);
submodel.setSubmodelElements(submodelElements);
List<ISubmodel> submodels = new ArrayList<>();
submodels.add(submodel);
IEnvironment environment = new Environment(null, submodels, null);
// We descend recursively over all the instances
// and filter for the int properties.
environment.descend().forEach(iClass -> {
if (iClass instanceof Property) {
Property property = (Property) iClass;
if (property.getValueType() == DataTypeDefXsd.INT) {
property.getIdShort().ifPresent(System.out::println);
}
}
});
// Prints:
// intProperty
// anotherIntProperty
Visitors
For loops are practical for short iterations. When you need to apply an action for each different model type, your loop body will probably become unbearably long. Such iterations are much better solved by using the visitor pattern. You specify for each model type a separate method in the visitor, and the visitor automatically dispatches the calls based on the type.IVisitor The SDK provides an abstract, purely virtual, IVisitor. If you want to steer how the children references from an instance are handled, then implement this interface.
AbstractVisitor Most of the time, you want to process each instance of a model in isolation, so you do not want to call the visiting methods on the children references. The abstract visitor AbstractVisitor will take care of the children references. You merely have to specify the operation for each model type.
VisitorThrough If you want to process only some of the model types, use the class VisitorThrough. This class will iterate recursively over all the referenced instances, and simply perform no action for instances of model types for which you did not override the corresponding visiting method. Here is an example with a VisitorThrough, which has been adapted from the previous filtering example:
// We implement our visitor.
private class MyVisitor extends VisitorThrough {
Override
public void visitProperty(IProperty that) {
if (that.getValueType() == DataTypeDefXsd.INT) {
that.getIdShort().ifPresent(System.out::println);
}
}
}
// We use it in our main routine
MyVisitor myVisitor = new MyVisitor();
// We prepare the environment.
ISubmodelElement boolProperty = new Property(DataTypeDefXsd.BOOLEAN);
boolProperty.setIdShort("boolProperty");
ISubmodelElement intProperty = new Property(DataTypeDefXsd.INT);
intProperty.setIdShort("intProperty");
ISubmodelElement anotherIntProperty = new Property(DataTypeDefXsd.INT);
anotherIntProperty.setIdShort("anotherIntProperty");
ISubmodel submodel = new Submodel("some-unique-global-identifier");
List<ISubmodelElement> submodelElements = new ArrayList<>();
submodelElements.add(boolProperty);
submodelElements.add(intProperty);
submodelElements.add(anotherIntProperty);
submodel.setSubmodelElements(submodelElements);
List<ISubmodel> submodels = new ArrayList<>();
submodels.add(submodel);
IEnvironment environment = new Environment(null, submodels, null);
myVisitor.visit(environment);
// Prints:
// intProperty
// anotherIntProperty
Verify
Our SDK allows you to verify that a model satisfies the constraints of the meta-model. The verification logic is concentrated in the static class Verification, and all it takes is a call to verify method. The method verify will check that constraints in the given model element are satisfied, including the recursion into children elements. The method returns an iterator of errors, which you can use to for further processing (e.g., report to the user). Here is a short example snippet:
// We prepare the environment.
ISubmodelElement someProperty = new Property(DataTypeDefXsd.BOOLEAN);
// We intentionally introduce a mistake.
someProperty.setIdShort("!@#$% invalid ID short (*&^_+)");
List<ISubmodelElement> submodelElements = new ArrayList<>();
submodelElements.add(someProperty);
ISubmodel submodel = new Submodel("some-unique-global-identifier");
submodel.setSubmodelElements(submodelElements);
List<ISubmodel> submodels = new ArrayList<>();
submodels.add(submodel);
IEnvironment environment = new Environment(null, submodels, null);
// We verify the environment and report the errors.
Iterable<Reporting.Error> iterable = Verification.verify(environment);
iterable.forEach(error -> {
System.out.println(Reporting.generateJsonPath(error.getPathSegments()));
System.out.println(error.getCause());
});
// Prints
// submodels[0].submodelElements[0].idShort
// Invariant violated:
// ID-short of Referables shall only feature letters, digits, underscore (``_``);
// starting mandatory with a letter. *I.e.* ``[a-zA-Z][a-zA-Z0-9_]*``.
De/serialize a model from and to JSON
Our SDK handles the de/serialization of the AAS models from and to JSON format through the Jsonization. Since Java does not support JSON de/serialization in the standard library, we use jackson as the underlying representation of JSON values. We thus do not directly de/serialize from and to strings, but rely on jackson for intermediate handling.Serialize
Here is an example code how to serialize an instance to json:
// We need to import jackson
import com.fasterxml.jackson.databind.JsonNode;
// We prepare the environment.
ISubmodelElement someProperty = new Property(DataTypeDefXsd.INT);
someProperty.setIdShort("someProperty");
List<ISubmodelElement> submodelElements = new ArrayList<>();
submodelElements.add(someProperty);
ISubmodel submodel = new Submodel("some-unique-global-identifier");
submodel.setSubmodelElements(submodelElements);
List<ISubmodel> submodels = new ArrayList<>();
submodels.add(submodel);
IEnvironment environment = new Environment(null, submodels, null);
JsonNode json = Jsonization.Serialize.toJsonObject(environment);
System.out.println(json.toString());
// Prints
// {"submodels":[{
// "id":"some-unique-global-identifier"
// ,"submodelElements":[{"idShort":"someProperty","valueType":"xs:int","modelType":"Property"}]
// ,"modelType":"Submodel"}]}
De-serialize
To translate json value to a model instance, the SDK offers deserializeXxx de-serialization functions. For example, to de-serialize an environment, callJsonization.Deserialize.deserializeEnvironment(json).
Here is an example how to de-serialize json to an instance.
// We need to import jackson
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
// We specify the JSON as text.
String text = "{\"submodels\":[{\"" +
"id\":\"some-unique-global-identifier\"," +
"\"submodelElements\":[{\"idShort\":\"someProperty\"" +
",\"valueType\":\"xs:int\",\"modelType\":\"Property\"}]," +
"\"modelType\":\"Submodel\"}]}";
// We parse text as JSON.
ObjectMapper objectMapper = new ObjectMapper();
JsonNode json = objectMapper.readTree(text);
// We parse the JSON as environment.
IEnvironment environment = Jsonization.Deserialize.deserializeEnvironment(json);
System.out.println(environment.getSubmodels().get().get(0).getId());
// Prints
// some-unique-global-identifier
Here is another example of how to report the errors:
// We specify the text which is valid JSON,
// but not a valid representation of a model.
String text = "{\"submodels\": 42}";
// We parse text as JSON.
ObjectMapper objectMapper = new ObjectMapper();
JsonNode json = objectMapper.readTree(text);
// We parse the JSON as environment.
try {
IEnvironment environment = Jsonization.Deserialize.deserializeEnvironment(json);
} catch (Jsonization.DeserializeException exception) {
System.out.println(exception.getPath().get() + ": " + exception.getReason().get());
}
// Prints
// submodels: Expected a JsonArray, but got NUMBER
De/serialize a model from and to XML
The code that de/serializes AAS models from and to XML documents lives in Xmlization.Serialization
You serialize a model use the .to(instance, writer) method of Serialize. Apart from the object, you pass it the writer which is used to serialize the object graph. Here is an example:
// We need to import
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamWriter;
import java.io.StringWriter;
// We prepare the environment.
ISubmodelElement someProperty = new Property(DataTypeDefXsd.INT);
someProperty.setIdShort("someProperty");
List<ISubmodelElement> submodelElements = new ArrayList<>();
submodelElements.add(someProperty);
ISubmodel submodel = new Submodel("some-unique-global-identifier");
submodel.setSubmodelElements(submodelElements);
List<ISubmodel> submodels = new ArrayList<>();
submodels.add(submodel);
IEnvironment environment = new Environment(null, submodels, null);
// We create the writer
StringWriter stringOut = new StringWriter();
XMLOutputFactory outputFactory = XMLOutputFactory.newFactory();
XMLStreamWriter xmlStreamWriter = outputFactory.createXMLStreamWriter(stringOut);
// We serialize the environment to
Xmlization.Serialize.to(environment, xmlStreamWriter);
System.out.println(stringOut);
// Prints (without the newlines):
// <environment xmlns="https://admin-shell.io/aas/3/0">
// <submodels>
// <submodel>
// <id>some-unique-global-identifier</id>
// <submodelElements>
// <property>
// <idShort>someProperty</idShort>
// <valueType>xs:int</valueType>
// </property>
// </submodelElements>
// </submodel>
// </submodels>
// </environment>
De-serialization
De-serialization goes in the opposite direction, with Xmlization.Deserialize.deserializeXxx(reader). Here is an example how to de-serialize xml to an instance:
// We need to import
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLEventReader;
String text = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
"<environment xmlns=\"https://admin-shell.io/aas/3/0\">" +
"<submodels><submodel>" +
"<id>some-unique-global-identifier</id>" +
"<submodelElements>" +
"<property>" +
"<idShort>someProperty</idShort>" +
"<valueType>xs:int</valueType>" +
"</property>" +
"</submodelElements>" +
"</submodel>" +
"</submodels>" +
"</environment>";
// We create the reader
final XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
final XMLEventReader outputReader = xmlInputFactory.createXMLEventReader(new StringReader(text));
// We de-serialize the environment from
// an XML text.
IEnvironment environment = Xmlization.Deserialize.deserializeEnvironment(outputReader);
System.out.println(environment.getSubmodels().get().get(0).getId());
// Prints
// some-unique-global-identifier
Here is another example how you can report the error:
// We define a valid XML, but invalid representation
// of an AAS instance. Submodels should be a list,
// not a number.
String text = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
"<environment xmlns=\"https://admin-shell.io/aas/3/0\">" +
"<submodels>" +
"42" +
"</submodels>" +
"</environment>";
// We create the reader
final XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
final XMLEventReader outputReader = xmlInputFactory.createXMLEventReader(new StringReader(text));
// We de-serialize the environment from
// an XML text.
try {
IEnvironment environment = Xmlization.Deserialize.deserializeEnvironment(outputReader);
} catch (Xmlization.DeserializeException exception) {
System.out.println(exception.getPath().get() + ": " + exception.getReason().get());
}
// Prints (without newlines):
// environment/submodels/*[0]:
// Expected a start element opening an instance of ISubmodel, but got an XML Characters
Enhance model instances with your custom data
In any complex application, creating, modifying and de/serializing AAS instances is not enough. You have to insert your custom application-specific data to the model in order for the model to be useful.
Take, for example, parent-child relationship. The current library ignores it, and there is no easy way for you to find out to which ISubmodel a particular ISubmodelElement belongs to.
We did want to keep the types as simple as possible — the parent-child relationships can get tricky very soon if you have multiple environments with shared submodels etc. Instead of overcomplicating the code and making it barely traceable, we decided to keep it simple and frugal in features.
However, that is little solace if you are developing an GUI editor where you know for sure that there will be only one environment, and where parent-child relationships are crucial for so many tasks. What is more, parent-child relationships are not the only data that need to be intertwined — you probably want history, localized caches etc.
Hasthable?
There are different ways how application-specific data can be synced with the model. One popular technique is to use Hashtable's and simply map model instances to your custom nuggets of data. This works well if the data is read-only, and you can spare the cycles for the lookups (which is often acceptable as they run on average in time complexity O(1) anyhow).
Otherwise, if you need to modify the data, maintaining the consistency between the Hashtable and your nuggets of information becomes difficult. For example, if you forget to remove the entries from the Hashtable when you remove the instances from the model, you might clog your garbage collector.
Wrapping
Hence, if you modify the data, you need to keep it close to the model instance. In dynamic languages, such as Python and JavaScript, you can simply add your custom fields to the object. This does not work in such a static language like Java.
One solution, usually called Decorator pattern, is to wrap or decorate the instances with your application-specific data. The decorated objects should satisfy both the interface of the original model and provide a way to retrieve your custom nuggets of information.
Writing wrappers for many classes in the AAS meta-model is a tedious task. We therefore pre-generated the most of the boilerplate code in package aas_core.aas3_0.enhancing.
In the context of decoration, we call your specific data enhancements. First, you need to specify how individual instances are enhanced, i.e. how to produce enhancements for each one of them. We call this an enhancement factory. Second, you need to recursively wrap your instances with the given enhancement factory.
The Enhancing is generic and can work with any form of enhancement classes. You need to specify your enhancement factory as a function which takes an instance of IClass as input and returns an optional of enhancement, or an empty optional, if you do not want to enhance the particular instance.
The wrapping and unwrapping is specified in the generic function wrap of the class Enhancer. This function takes the instance that you want to wrap (i.e., decorate) and the enhancement factory, and recursively wraps it with the produced enhancements. The wrapped instance will be finally returned.
Example: Parent-Child Enhancement
Let us now consider the aforementioned example. We want to keep track of parent-child relationships in a model.
The following code snippets first constructs an environment for illustration. Then we specify the enhancement such that each instance is initialized with the parent set to null. Finally, we modify the enhancements such that they reflect the actual parent-child relationships.
// We define the enhancement, a parent-child relationship.
private class ParentEnhancement {
public IClass parent = null;
}
// We define the enhancement factory.
private Enhancer<ParentEnhancement> produceParentEnhancement() {
Function<IClass, Optional<ParentEnhancement>> enhancementFactory = iClass -> Optional.of(new ParentEnhancement());
return new Enhancer<>(enhancementFactory);
}
// We prepare the environment.
ISubmodelElement someProperty = new Property(DataTypeDefXsd.INT);
someProperty.setIdShort("someProperty");
List<ISubmodelElement> submodelElements = new ArrayList<>();
submodelElements.add(someProperty);
ISubmodel submodel = new Submodel("some-unique-global-identifier");
submodel.setSubmodelElements(submodelElements);
List<ISubmodel> submodels = new ArrayList<>();
submodels.add(submodel);
IEnvironment environment = new Environment(null, submodels, null);
// We enhance the environment recursively.
Enhancer<ParentEnhancement> enhancer = produceParentEnhancement();
environment = (IEnvironment) enhancer.wrap(environment);
Queue<IClass> queue = new ArrayDeque<>();
queue.add(environment);
while (!queue.isEmpty()) {
IClass instance = queue.remove();
for (IClass child : instance.descendOnce()) {
enhancer.mustUnwrap(child).parent = instance;
queue.add(child);
}
}
// Retrieve the parent of the first submodel
boolean environmentIsParent = enhancer.mustUnwrap(environment.getSubmodels().get().get(0)).parent == environment;
System.out.println(environmentIsParent);
// Prints true
Note that this approach is indeed more maintainable than the one with Hashtable, but you still need to take extra care.
If you create new submodels and insert them into the environment, you have to make sure that you wrap them appropriately.
If you move a submodel from one environment to another, you have to update the parent link manually etc.
Example: Selective Enhancement
We demonstrate now how you can selectively enhance only some instances in the Environment.
For example, let us assign a unique identifier to all instances which are referable, i.e., implement IReferable. All the other instances are not enhanced.
// We define the enhancement
private class IdEnhancement {
public long Id;
public IdEnhancement(long id) {
Id = id;
}
}
// We prepare the environment.
ISubmodelElement someProperty = new Property(DataTypeDefXsd.INT);
someProperty.setIdShort("someProperty");
AdministrativeInformation administration = new AdministrativeInformation(null, "1.0", null, null, null);
List<ISubmodelElement> submodelElements = new ArrayList<>();
submodelElements.add(someProperty);
ISubmodel submodel = new Submodel("some-unique-global-identifier");
submodel.setSubmodelElements(submodelElements);
submodel.setAdministration(administration);
List<ISubmodel> submodels = new ArrayList<>();
submodels.add(submodel);
IEnvironment environment = new Environment(null, submodels, null);
// Prepare the enhancer
AtomicLong lastId = new AtomicLong();
Function<IClass, Optional<IdEnhancement>> enhancementFactory = iClass -> {
if (iClass instanceof IReferable) {
lastId.getAndIncrement();
return Optional.of(new IdEnhancement(lastId.get()));
}
return Optional.empty();
};
Enhancer<IdEnhancement> enhancer = new Enhancer<>(enhancementFactory);
// Enhance
environment = (IEnvironment) enhancer.wrap(environment);
// The submodel and property are enhanced.
IdEnhancement enhancement = enhancer.mustUnwrap(environment.getSubmodels().get().get(0));
System.out.println(enhancement.Id);
// Prints:
// 2
enhancement = enhancer.mustUnwrap(environment.getSubmodels().get().get(0).getSubmodelElements().get().get(0));
System.out.println(enhancement.Id);
// Prints:
// 1
// The administrative information is not referable, and thus not enhanced.
Optional<IdEnhancement> maybeEnhancement = enhancer.unwrap(
environment.getSubmodels().get().get(0).getAdministration().get()
);
System.out.println(maybeEnhancement.isPresent());
// Prints:
// false
No Re-wraps Allowed
We disallow re-wraps of already wrapped instances to avoid costly iterations over the object trees, and throw an exception. Additionally, we want to prevent bugs in many settings where the enhancement factory assigns unique identifiers to instances or performs non-idempotent operations.Change Log
1.0.2
Unescaped special characters in the file path verification regex caused verification of file / thumbnail elements to fail in some cases. We adjust the regex accordingly.1.0.1
We propagate the changes and fixes; please refer to:- Remove RFC8089 verification function
- Fix incomplete ID-Short docstring
- Add Missing Path_type Constraint
1.0.0
This is the first stable release. The release candidates stood the test of time, so we are now confident to publish a stable version.1.0.0-rc2
- The dataSpecification field in EmbeddedDataSpecification is made optional, according to the book.
- Add release maven build profile
- This allows execution of mvn install, mvn package, phases without execution of the gpg sign plugin for local testing and development.
- Fix camel case for the setter names in the builder types.