Java Tooling for Working with Aspect Models

General Considerations

In this section, the Java APIs for working with Aspect Models are described. All of the components described in the following subsections can either be included in your project using a dedicated dependency (as described in the respective subsection), or you can use the aspect-model-starter artifact that aggregates all necessary dependencies:

Maven
<dependency>
    <groupId>org.eclipse.esmf</groupId>
    <artifactId>esmf-aspect-model-starter</artifactId>
    <version>2.9.7</version>
</dependency>
Gradle Groovy DSL
implementation 'org.eclipse.esmf:esmf-aspect-model-starter:2.9.7'
Gradle Kotlin DSL
implementation("org.eclipse.esmf:esmf-aspect-model-starter:2.9.7")

Getting the right version

The tooling can be made available in two different versions: release and milestone. The release version represents a stable version of the product and can be referenced in your projects in the usual way from the Maven Central repository. There is also the possibility to have the intermediate builds made available, these are called milestone builds. Instead of Maven Central, these are released via GitHub Packages mechanism.
To be able to use the artifacts released in this way in your projects, first the right repository has to be added to your pom.xml file:

<repositories>
  <repository>
    <id>github</id>
    <name>ESMF SDK</name>
    <url>https://maven.pkg.github.com/eclipse-esmf/esmf-sdk</url>
    <releases><enabled>true</enabled></releases>
    <snapshots><enabled>true</enabled></snapshots>
  </repository>
</repositories>

Then the desired dependencies can be referenced in the usual way. For an example, please refer to Github - Installing a package.

JDK requirements

The esmf-sdk components are built with Java 17 and require a JDK >= 17 such as Adoptium Temurin.

The esmf-sdk can also be used with a Java 17-compatible GraalVM JDK. With GraalVM you need to make the Graal JavaScript component available, as parts of the SDK such as the Aspect Model validation component require embedded JavaScript execution:

  • For using the esmf-sdk in the GraalVM itself, install the JS component using the GraalVM Updater: gu install js

  • For using the esmf-sdk with GraalVM native-image, be sure to use the --language:js option when building the native image; for more information see polyglot programming.

Parsing Aspect Model URNs

The aspect-model-urn artifact provides a way to parse and validate Aspect model element URNs as described in the specification.

Show used imports
import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn;
final AspectModelUrn urn = AspectModelUrn.fromUrn( "urn:samm:com.example:1.0.0#Example" );
final String namespace = urn.getNamespaceMainPart(); // com.example
final String name = urn.getName();           // Example
final String version = urn.getVersion();     // 1.0.0
final String urnPrefix = urn.getUrnPrefix(); // urn:samm:com.example:1.0.0#

To include the artifact, use the following dependencies:

Maven
<dependency>
    <groupId>org.eclipse.esmf</groupId>
    <artifactId>esmf-aspect-model-urn</artifactId>
    <version>2.9.7</version>
</dependency>
Gradle Groovy DSL
implementation 'org.eclipse.esmf:esmf-aspect-model-urn:2.9.7'
Gradle Kotlin DSL
implementation("org.eclipse.esmf:esmf-aspect-model-urn:2.9.7")

Loading and Saving Aspect Models

Aspect models are, like the Semantic Aspect Meta Model, described using the Resource Description Format (RDF, [rdf11]) and the Terse RDF Triple Language syntax (TTL, [turtle]). When an Aspect Model is loaded, there are two ways of working with it: Either on the abstraction level of the underlying RDF/Turtle serialization, or on the native Java Aspect Model level, where the model is represented as a Java object graph. Both approaches have different use cases and advantages and both are supported by the Aspect Model Java tooling:

Working on the RDF level Using the Java Aspect model
  • Low level, focus on power and flexibility

  • Flexibly navigate and filter the model on the RDF statements level

  • Work with models that are valid RDF, but incomplete Aspect Models, e.g. in Aspect model editors

  • Use SPARQL [sparql] to execute complex queries on Aspect Models

  • High level, focus on convenience and type-safety

  • Use Aspect Model-specific interfaces for type-safe code

  • Use Java native data types (e.g. java.math.BigInteger for xsd:integer)

  • Easily traverse the model on the abstraction level of Aspect Model elements

As a rule of thumb, if your use case mostly consists of consuming Aspect models, you should prefer the Java Aspect model, if you create or edit Aspect models, or build code interfacing with other RDF vocabularies, this is better done using the RDF API.

Loading an Aspect Model to work on the Java Aspect Model Level

To load an AspectModel, you use the org.eclipse.esmf.aspectmodel.loader.AspectModelLoader class. An instance of AspectModelLoader provides load() methods to load an Aspect Model from an InputStream, one or multiple Files, or a number of AspectModelUrn (where you can delegate finding out where a model element is defined to the AspectModelLoader - details on that are explained in the next section).

The AspectModelLoader loads Aspect Models based on the most recent version of the Semantic Aspect Meta Model and previous versions: Models based on older meta model versions are automatically translated to models corresponding to the latest meta model version.

The resulting AspectModel object contains information about the loaded model elements, their namespaces and the files they were defined in. Details about the structure of these objects can be found in the Decision Record 0007. The AspectModel provides the methods elements(), files() and namespaces() as well as the convenience methods aspects() (which returns all Aspect elements in the model) and aspect(), which returns the single Aspect element if one exists.

Show used imports
import java.io.File;

import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
import org.eclipse.esmf.metamodel.AspectModel;
import org.eclipse.esmf.metamodel.ModelElement;
      final AspectModel aspectModel = new AspectModelLoader().load(
            // a File, InputStream or AspectModelUrn
            new File( "aspect-models/org.eclipse.esmf.examples.movement/1.0.0/Movement.ttl" )
      );

      // Do something with the elements
      for ( final ModelElement element : aspectModel.elements() ) {
         System.out.printf( "Model element: %s has URN %s%n", element.getName(), element.urn() );
      }

      // Directly work with the Aspect, if the AspectModel happens to contain
      // exactly one Aspect. If there are 0 or >1 Aspects, this will throw an exception.
      System.out.println( "Aspect URN: " + aspectModel.aspect().urn() );
   }
}

To include the Java Aspect Model artifact, use the following dependency:

Maven
<dependency>
    <groupId>org.eclipse.esmf</groupId>
    <artifactId>esmf-aspect-meta-model-java</artifactId>
    <version>2.9.7</version>
</dependency>
Gradle Groovy DSL
implementation 'org.eclipse.esmf:esmf-aspect-meta-model-java:2.9.7'
Gradle Kotlin DSL
implementation("org.eclipse.esmf:esmf-aspect-meta-model-java:2.9.7")

Understanding Model Resolution

The example in the last section showed how a self-contained Aspect Model file can be loaded (i.e., a file that does not refer to model element definitions in other files or namespaces). Whenever models are loaded that could contain such references, or you want to load a model element by URN, the AspectModelLoader relies on so-called resolution strategies. A resolution strategy is a function that takes a model element identifier (a model element URN) as an input and returns the content of the file that contains the corresponding model element definition. Note that this is not necessarily a file in the local filesystem, but it could also be a remote file or even exist only virtually as a collection of statements in an RDF triple store. Several ResolutionStrategy​s are provided that can be instantiated and passed to the AspectModelLoader constructor to enable it to find model element definitions:

  • The FileSystemStrategy resolves elements from files in the file system which are either structured in the models directory structure or exist as flat list of files in one directory (by using FileSystemStrategy with a FlatModelsRoot).

  • The ClassPathStrategy resolves model elements from resources in the Java class path.

  • The FromLoadedFileStrategy resolves model elements from an AspectModelFile that already resides in memory.

  • The EitherStrategy can be used to chain two or more different ResolutionStrategy​s.

  • The ExternalResolverStrategy delegates resolution to an external command such as a script; it is used in the implementation of the --custom-resolver option of the samm-cli.

In addition, custom resolution strategies can be provided by implementing the ResolutionStrategy interface.

The following example demonstrates how to pass a custom instance of a ResolutionStrategy and resolve a model element by URN:

Show used imports
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;

import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
import org.eclipse.esmf.aspectmodel.resolver.FileSystemStrategy;
import org.eclipse.esmf.aspectmodel.resolver.ResolutionStrategy;
import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn;
import org.eclipse.esmf.metamodel.AspectModel;
import org.eclipse.esmf.metamodel.vocabulary.SammNs;

import org.apache.jena.vocabulary.RDF;
// The directory containing the models folder structure,
// see models directory structure
final Path modelsRoot = Paths.get( "aspect-models" );
final ResolutionStrategy fileSystemStrategy = new FileSystemStrategy( modelsRoot );
final AspectModelUrn urn = AspectModelUrn.fromUrn( "urn:samm:org.eclipse.esmf.examples.movement:1.0.0#Movement" );
final AspectModel result = new AspectModelLoader( fileSystemStrategy ).load( urn );

Loading an Aspect Model to work on the RDF Level

The following example shows how to use the AspectModelResolver to load an Aspect Model.

Show used imports
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;

import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
import org.eclipse.esmf.aspectmodel.resolver.FileSystemStrategy;
import org.eclipse.esmf.aspectmodel.resolver.ResolutionStrategy;
import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn;
import org.eclipse.esmf.metamodel.AspectModel;
import org.eclipse.esmf.metamodel.vocabulary.SammNs;

import org.apache.jena.vocabulary.RDF;
final AspectModel aspectModel = new AspectModelLoader().load(
      // a File, InputStream or AspectModelUrn
);

// Do something with the Aspect Model on the RDF level.
// Example: List the URNs of all samm:Entitys
aspectModel.mergedModel().listStatements( null, RDF.type, SammNs.SAMM.Entity() )
      .forEachRemaining( statement -> System.out.println( statement.getSubject().getURI() ) );

After loading an AspectModel using the AspectModelLoader, you can access the mergedModel() on the AspectModel, which represents the merged RDF graph of all source files that were loaded in the model and therefore contains all transitively referenced model element definitions. Alternatively, you can also access the sourceModel() of each Aspect Model file given by files(), which will return only the RDF graph of this file.

Serializing a Java Aspect Model into a RDF/Turtle Aspect Model

The serialize an Aspect into its corresponding RDF/Turtle representation, you can use the AspectSerializer, as demonstrated in the example below:

Show used imports
import java.io.File;

import org.eclipse.esmf.aspectmodel.serializer.AspectSerializer;
import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
import org.eclipse.esmf.metamodel.AspectModel;
// AspectModel as returned by the AspectModelLoader
final AspectModel aspectModel = // ...

// A String that contains the pretty printed Aspect Model
final String aspectString =
    AspectSerializer.INSTANCE.aspectToString( aspectModel.aspect() );

To include the serializer artifact, use the following dependencies:

Maven
<dependency>
    <groupId>org.eclipse.esmf</groupId>
    <artifactId>esmf-aspect-model-serializer</artifactId>
    <version>2.9.7</version>
</dependency>
Gradle Groovy DSL
implementation 'org.eclipse.esmf:esmf-aspect-model-serializer:2.9.7'
Gradle Kotlin DSL
implementation("org.eclipse.esmf:esmf-aspect-model-serializer:2.9.7")

Validating Aspect Models

Aspect Models are validated using the AspectModelValidator. Validation returns a list of Violation​s. A violation has a human-readable message and a unique error code and provides access to the EvaluationContext which contains references to the model element that caused the violation and the SHACL shape that triggered the violation.

Each possible type of violation is a subtype of the Violation interface and provides additional context information specific to this type, for example, the MinLengthViolation provides int min (the allowed length) and int actual (the length that was encountered in the model).

Consider the following example:

Show used imports
import java.io.File;
import java.util.List;

import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
import org.eclipse.esmf.aspectmodel.shacl.violation.Violation;
import org.eclipse.esmf.aspectmodel.validation.services.AspectModelValidator;
import org.eclipse.esmf.aspectmodel.validation.services.DetailedViolationFormatter;
import org.eclipse.esmf.aspectmodel.validation.services.ViolationFormatter;
import org.eclipse.esmf.metamodel.AspectModel;
// AspectModel as returned by the AspectModelLoader
final AspectModel aspectModel = // ...

final List<Violation> violations = new AspectModelValidator().validateModel( aspectModel );
if ( violations.isEmpty() ) {
   // Aspect Model is valid!
   return;
}

final String validationReport = new ViolationFormatter().apply( violations ); (1)
final String detailedReport = new DetailedViolationFormatter().apply( violations );

class MyViolationVisitor implements Violation.Visitor<String> { (2)
   // ...
}

// Turn the list of Violations into a list of custom descriptions
final Violation.Visitor<String> visitor = new MyViolationVisitor();
final List<String> result = violations.stream()
      .map( violation -> violation.accept( visitor ) )
      .toList();
1 To format the validation result into an human-readable form, use the ViolationFormatter or the DetailedViolationFormatter. Note that those are only intended for text-based interfaces such as CLIs.
2 Every application dealing with validation results that needs to transform the results into some different structure can implement the Violation.Visitor Interface with a suitable target type (String used as an example here) and use the visitor to handle the different types of Violations in a type-safe way.

The AspectModelValidator also provides the loadModel(Supplier<AspectModel>) method that can be used in conjunction with () → new AspectModeLoader().load(…​) to have exceptions that might be created during the Aspect Model loading process, for example due to model resolution failures or syntax errors in the input files, be turned into a List<Violation> that can be passed to the aforementioned violation formatters. This allows for custom structured handling of problems that occur during model loading.

To include the model validator, use the following dependencies:

Maven
<dependency>
    <groupId>org.eclipse.esmf</groupId>
    <artifactId>esmf-aspect-model-validator</artifactId>
    <version>2.9.7</version>
</dependency>
Gradle Groovy DSL
implementation 'org.eclipse.esmf:esmf-aspect-model-validator:2.9.7'
Gradle Kotlin DSL
implementation("org.eclipse.esmf:esmf-aspect-model-validator:2.9.7")

Generating Documentation for Aspect Models

Different types of artifacts can be generated from an Aspect model. All corresponding generators are included in the following dependency:

Maven
<dependency>
    <groupId>org.eclipse.esmf</groupId>
    <artifactId>esmf-aspect-model-document-generators</artifactId>
    <version>2.9.7</version>
</dependency>
Gradle Groovy DSL
implementation 'org.eclipse.esmf:esmf-aspect-model-document-generators:2.9.7'
Gradle Kotlin DSL
implementation("org.eclipse.esmf:esmf-aspect-model-document-generators:2.9.7")

The documentation generation APIs provide methods that take as an argument a Function<String, java.io.OutputStream>. This is a mapping function that takes a file name as an input (which is determined by the respective generator) and returns a corresponding OutputStream, for example (but not necessarily) a FileOutputStream. By providing this function when calling the generator method, you can control where the output is written to, even when the generator creates multiple files. For the code examples in the following subsections, we assume that the following method is defined for calling the generators:

OutputStream outputStreamForName( final String aspectFileName ) {
   // Create an OutputStream for the file name, e.g. a FileOutputStream
}

Generating SVG or PNG Diagrams

Using the AspectModelDiagramGenerator, automatically layouted diagrams can be created for Aspect models in the formats PNG and SVG.

Show used imports
import java.io.OutputStream;
import java.util.Locale;
import java.util.Set;

import org.eclipse.esmf.aspectmodel.generator.diagram.AspectModelDiagramGenerator;
import org.eclipse.esmf.aspectmodel.generator.diagram.AspectModelDiagramGenerator.Format;
import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
import org.eclipse.esmf.metamodel.AspectModel;
// AspectModel as returned by the AspectModelLoader
final AspectModel aspectModel = // ...

final AspectModelDiagramGenerator generator = new AspectModelDiagramGenerator( aspectModel.aspect() ); (1)

// Variant 1: Generate a diagram in SVG format using @en descriptions and preferredNames from the model
final OutputStream output = outputStreamForName( "diagram.svg" );
generator.generateDiagram( Format.SVG, Locale.ENGLISH, output ); (2)
output.close();

// Variant 2: Generate diagrams in multiple formats, for all languages that are present in the model.
generator.generateDiagrams( Set.of( Format.PNG, Format.SVG ), this::outputStreamForName ); (3)
1 The diagram generator is initialized with the loaded model.
2 The simple call for one output format and one language (i.e., descriptions and preferredNames of one locale) takes one output stream to write the image to.
3 It is also possible to generate multiple diagrams, one for each combination of output format and language. For that, the set of target formats is given as well as a mapping function.

Generating HTML Documentation

A HTML reference documentation for an Aspect model can be generated as shown in the following code sample. The documentation contains an overview diagram and describes the model elements as specified in the Aspect model. Preferred names and descriptions in the respective language from the Aspect model are shown in the resulting document as part of each model element.

Show used imports
import java.io.File;
import java.io.IOException;
import java.util.Map;

import org.eclipse.esmf.aspectmodel.generator.docu.AspectModelDocumentationGenerator;
import org.eclipse.esmf.aspectmodel.generator.docu.AspectModelDocumentationGenerator.HtmlGenerationOption;
import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
import org.eclipse.esmf.metamodel.AspectModel;
// end:imports[]

import org.junit.jupiter.api.Test;

public class GenerateHtml extends AbstractGenerator {
   @Test
   public void generate() throws IOException {
      // AspectModel as returned by the AspectModelLoader
      final AspectModel aspectModel = // ...
            new AspectModelLoader().load(
                  new File( "aspect-models/org.eclipse.esmf.examples.movement/1.0.0/Movement.ttl" ) );
      final AspectModelDocumentationGenerator generator = (1)
            new AspectModelDocumentationGenerator( aspectModel.aspect() );

      final Map<HtmlGenerationOption, String> options = Map.of(); (2)
      generator.generate( this::outputStreamForName, options );
   }
}
// AspectModel as returned by the AspectModelLoader
final AspectModel aspectModel = // ...
final AspectModelDocumentationGenerator generator = (1)
      new AspectModelDocumentationGenerator( aspectModel.aspect() );

final Map<HtmlGenerationOption, String> options = Map.of(); (2)
generator.generate( this::outputStreamForName, options );
1 The HTML generator is initialized with the context consisting of the loaded RDF model and the selected Aspect.
2 HTML generation can be controlled via a map of options; using an empty map implies default values.

Generating Sample JSON Payload

The sample JSON payload generator is used to create a valid JSON payload for an Aspect model as it could be returned by an Aspect that implements that Aspect model. This follows the mapping rules as defined in the Meta Model specification. The generator uses samm:exampleValue​s of Properties if present, otherwise random values corresponding to the respective data types are generated.

Show used imports
import org.eclipse.esmf.aspectmodel.generator.json.AspectModelJsonPayloadGenerator;
import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
import org.eclipse.esmf.metamodel.AspectModel;
// AspectModel as returned by the AspectModelLoader
final AspectModel aspectModel = // ...

final AspectModelJsonPayloadGenerator generator = new AspectModelJsonPayloadGenerator( aspectModel.aspect() );

// Variant 1: Direct generation into a String
final String jsonString = generator.generateJson();

// Variant 2: Generate using mapping function
generator.generateJson( this::outputStreamForName );

Generating JSON Schema

The JSON schema generator creates a JSON Schema that describes the payload for an Aspect model as it could be returned by an Aspect that implements that Aspect model.

Show used imports
import java.io.ByteArrayOutputStream;
import java.util.Locale;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.eclipse.esmf.aspectmodel.generator.jsonschema.AspectModelJsonSchemaGenerator;
import org.eclipse.esmf.aspectmodel.generator.jsonschema.JsonSchemaGenerationConfig;
import org.eclipse.esmf.aspectmodel.generator.jsonschema.JsonSchemaGenerationConfigBuilder;
import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
import org.eclipse.esmf.metamodel.AspectModel;
// AspectModel as returned by the AspectModelLoader
final AspectModel aspectModel = // ...

final JsonSchemaGenerationConfig config = JsonSchemaGenerationConfigBuilder.builder()
      .locale( Locale.ENGLISH )
      .build();
final JsonNode jsonSchema = AspectModelJsonSchemaGenerator.INSTANCE.apply( aspectModel.aspect(), config ).getContent();

// If needed, print or pretty print it into a string
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final ObjectMapper objectMapper = new ObjectMapper();

objectMapper.writerWithDefaultPrettyPrinter().writeValue( out, jsonSchema );
final String result = out.toString();

Generating OpenAPI Specification

The OpenAPI specification generator creates either a JSON Schema or a Yaml Spec that specifies an Aspect regarding to the OpenApi specification. The currently used versions corresponds Draft 4 of the JSON Schema specification, and 3.0.1.

Show used imports
import java.io.ByteArrayOutputStream;

import org.eclipse.esmf.aspectmodel.generator.openapi.AspectModelOpenApiGenerator;
import org.eclipse.esmf.aspectmodel.generator.openapi.OpenApiSchemaGenerationConfig;
import org.eclipse.esmf.aspectmodel.generator.openapi.OpenApiSchemaGenerationConfigBuilder;
import org.eclipse.esmf.aspectmodel.generator.openapi.PagingOption;
import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
import org.eclipse.esmf.metamodel.AspectModel;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
Generate OpenAPI YAML
      // AspectModel as returned by the AspectModelLoader
      final AspectModel aspectModel = // ...
            new AspectModelLoader().load(
                  new File( "aspect-models/org.eclipse.esmf.examples.movement/1.0.0/Movement.ttl" ) );

      final OpenApiSchemaGenerationConfig config = OpenApiSchemaGenerationConfigBuilder.builder()
            // Server URL
            .baseUrl( "http://www.example.com" )
            // The resource path which shall be added
            .resourcePath( "/testPath/{parameter}" )
            // Determines whether semantic versioning should be used for the API
            // i.e., true = v1.2.3, false = v1
            .useSemanticVersion( false )
            // A String containing all the information for dynamic properties mentioned in the resource path.
            // The string must be syntactically valid YAML.
            .properties( readYaml( """
                  parameter:
                    name: parameter
                    in: path
                    description: "A parameter."
                    required: true
                    schema:
                      type: string
                  """ ) )
            // Should the query API be added to the generated specification?
            .includeQueryApi( true )
            // The paging Option to be chosen. In case paging is possible:
            // The default for a time related collection is time-based paging.
            // Otherwise the default is offset-based paging.
            .pagingOption( PagingOption.OFFSET_BASED_PAGING )
            .build();

      // Generate pretty-printed YAML
      final AspectModelOpenApiGenerator generator = new AspectModelOpenApiGenerator();
      final String yaml = generator.apply( aspectModel.aspect(), config ).getContentAsYaml();
   }

   @Test
   public void generateJson() throws IOException {
      // AspectModel as returned by the AspectModelLoader
      final AspectModel aspectModel = // ...
            new AspectModelLoader().load(
                  new File( "aspect-models/org.eclipse.esmf.examples.movement/1.0.0/Movement.ttl" ) );

      final ObjectMapper objectMapper = new ObjectMapper();
      final OpenApiSchemaGenerationConfig config = OpenApiSchemaGenerationConfigBuilder.builder()
            // Server URL
            .baseUrl( "http://www.example.com" )
            // The resource path which shall be added
            .resourcePath( "/testPath/{parameter}" )
            // Determines whether semantic versioning should be used for the API
            // i.e., true = v1.2.3, false = v1
            .useSemanticVersion( false )
            // A JsonNode containing all the information for variable properties mentioned
            // in the resource path .
            .properties( readYaml( """
                     {
                       "test-Id": {
                         "name": "test-Id",
                         "in": "path",
                         "description": "The ID of the unit.",
                         "required": true,
                         "schema": {
                           "type": "string"
                         }
                       }
                     }
                  """ ) )
            // Should the query API be added to the generated specification?
            .includeQueryApi( true )
            // The paging Option to be chosen. In case paging is possible:
            // The default for a time related collection is time-based paging.
            // Otherwise the default is offset-based paging.
            .pagingOption( PagingOption.OFFSET_BASED_PAGING )
            .build();

      // Generate the JSON
      final AspectModelOpenApiGenerator generator = new AspectModelOpenApiGenerator();
      final JsonNode json = generator.apply( aspectModel.aspect(), config ).getContent();

      // If needed, print or pretty print it into a string
      final ByteArrayOutputStream out = new ByteArrayOutputStream();

      objectMapper.writerWithDefaultPrettyPrinter().writeValue( out, json );
      final String result = out.toString();
   }

   private static ObjectNode readYaml( String content ) throws JsonProcessingException {
      return (ObjectNode) new YAMLMapper().readTree( content );
   }
}
Generate OpenAPI JSON
      // AspectModel as returned by the AspectModelLoader
      final AspectModel aspectModel = // ...
            new AspectModelLoader().load(
                  new File( "aspect-models/org.eclipse.esmf.examples.movement/1.0.0/Movement.ttl" ) );

      final ObjectMapper objectMapper = new ObjectMapper();
      final OpenApiSchemaGenerationConfig config = OpenApiSchemaGenerationConfigBuilder.builder()
            // Server URL
            .baseUrl( "http://www.example.com" )
            // The resource path which shall be added
            .resourcePath( "/testPath/{parameter}" )
            // Determines whether semantic versioning should be used for the API
            // i.e., true = v1.2.3, false = v1
            .useSemanticVersion( false )
            // A JsonNode containing all the information for variable properties mentioned
            // in the resource path .
            .properties( readYaml( """
                     {
                       "test-Id": {
                         "name": "test-Id",
                         "in": "path",
                         "description": "The ID of the unit.",
                         "required": true,
                         "schema": {
                           "type": "string"
                         }
                       }
                     }
                  """ ) )
            // Should the query API be added to the generated specification?
            .includeQueryApi( true )
            // The paging Option to be chosen. In case paging is possible:
            // The default for a time related collection is time-based paging.
            // Otherwise the default is offset-based paging.
            .pagingOption( PagingOption.OFFSET_BASED_PAGING )
            .build();

      // Generate the JSON
      final AspectModelOpenApiGenerator generator = new AspectModelOpenApiGenerator();
      final JsonNode json = generator.apply( aspectModel.aspect(), config ).getContent();

      // If needed, print or pretty print it into a string
      final ByteArrayOutputStream out = new ByteArrayOutputStream();

      objectMapper.writerWithDefaultPrettyPrinter().writeValue( out, json );
      final String result = out.toString();
   }

   private static ObjectNode readYaml( String content ) throws JsonProcessingException {
      return (ObjectNode) new YAMLMapper().readTree( content );
   }
}
For Enumerations with complex data types, the values are modelled as instances of the Entity defined as the Enumeration’s data type (see Declaring Enumerations for more information). In case the Entity instances contain Properties with a sorted collection as their data type, the order of the values of said collection in the Entity instances is not preserved in the generated OpenAPI specification. Preserving this order in OpenAPI is not possible at this point.

Generating AsyncAPI Specification

The AsyncAPI specification generator creates either a JSON Schema or a Yaml Spec that specifies an Aspect regarding to the AsyncApi specification. The currently used versions correspond to Draft 4 of the JSON Schema specification, and 3.0.0 of the AsyncAPI specification.

Show used imports
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;

import org.eclipse.esmf.aspectmodel.generator.asyncapi.AspectModelAsyncApiGenerator;
import org.eclipse.esmf.aspectmodel.generator.asyncapi.AsyncApiSchemaGenerationConfig;
import org.eclipse.esmf.aspectmodel.generator.asyncapi.AsyncApiSchemaGenerationConfigBuilder;
import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
import org.eclipse.esmf.metamodel.AspectModel;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
Generate AsyncAPI YAML
      // AspectModel as returned by the AspectModelLoader
      final AspectModel aspectModel = // ...
            new AspectModelLoader().load(
                  new File( "aspect-models/org.eclipse.esmf.examples.movement/1.0.0/Movement.ttl" ) );

      final ObjectMapper yamlMapper = new YAMLMapper().enable( YAMLGenerator.Feature.MINIMIZE_QUOTES );

      final AsyncApiSchemaGenerationConfig config = AsyncApiSchemaGenerationConfigBuilder.builder()
            // i.e., true = v1.2.3, false = v1
            .useSemanticVersion( false )
            // Set Application id
            .applicationId( "test:serve" )
            .channelAddress( "/123-456/789-012/movement/0.0.1/Movement" )
            .build();

      // Generate pretty-printed YAML
      final AspectModelAsyncApiGenerator generator = new AspectModelAsyncApiGenerator();
      final JsonNode json = generator.apply( aspectModel.aspect(), config ).getContent();
      final String yaml = yamlMapper.writeValueAsString( json );

      final ByteArrayOutputStream out = new ByteArrayOutputStream();
      out.write( yaml.getBytes( StandardCharsets.UTF_8 ) );

      final String result = out.toString();
   }

   @Test
   public void generateJson() throws IOException {
      // AspectModel as returned by the AspectModelLoader
      final AspectModel aspectModel = // ...
            new AspectModelLoader().load(
                  new File( "aspect-models/org.eclipse.esmf.examples.movement/1.0.0/Movement.ttl" ) );

      final ObjectMapper objectMapper = new ObjectMapper();
      final AsyncApiSchemaGenerationConfig config = AsyncApiSchemaGenerationConfigBuilder.builder()
            // i.e., true = v1.2.3, false = v1
            .useSemanticVersion( false )
            // Set Application id
            .applicationId( "test:serve" )
            .channelAddress( "/123-456/789-012/movement/0.0.1/Movement" )
            .build();

      // Generate the JSON
      final AspectModelAsyncApiGenerator generator = new AspectModelAsyncApiGenerator();
      final JsonNode json = generator.apply( aspectModel.aspect(), config ).getContent();

      // If needed, print or pretty print it into a string
      final ByteArrayOutputStream out = new ByteArrayOutputStream();
      objectMapper.writerWithDefaultPrettyPrinter().writeValue( out, json );
      final String result = out.toString();
   }
}
Generate AsyncAPI JSON
      // AspectModel as returned by the AspectModelLoader
      final AspectModel aspectModel = // ...
            new AspectModelLoader().load(
                  new File( "aspect-models/org.eclipse.esmf.examples.movement/1.0.0/Movement.ttl" ) );

      final ObjectMapper objectMapper = new ObjectMapper();
      final AsyncApiSchemaGenerationConfig config = AsyncApiSchemaGenerationConfigBuilder.builder()
            // i.e., true = v1.2.3, false = v1
            .useSemanticVersion( false )
            // Set Application id
            .applicationId( "test:serve" )
            .channelAddress( "/123-456/789-012/movement/0.0.1/Movement" )
            .build();

      // Generate the JSON
      final AspectModelAsyncApiGenerator generator = new AspectModelAsyncApiGenerator();
      final JsonNode json = generator.apply( aspectModel.aspect(), config ).getContent();

      // If needed, print or pretty print it into a string
      final ByteArrayOutputStream out = new ByteArrayOutputStream();
      objectMapper.writerWithDefaultPrettyPrinter().writeValue( out, json );
      final String result = out.toString();
   }
}

Generating SQL for Aspect Models

Using the Aspect Model SQL generator, an SQL script can be generated that sets up a table for data corresponding to the Aspect. The current implementation provides support for the Databricks SQL dialect and a mapping strategy that uses a denormalized table, i.e., the table contains one column for each Property used in the Aspect Model (or any of its transitively referenced Entities).

Show used imports
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.Optional;

import org.eclipse.esmf.aspectmodel.generator.sql.AspectModelSqlGenerator;
import org.eclipse.esmf.aspectmodel.generator.sql.SqlGenerationConfig;
import org.eclipse.esmf.aspectmodel.generator.sql.SqlGenerationConfigBuilder;
import org.eclipse.esmf.aspectmodel.generator.sql.databricks.DatabricksColumnDefinitionBuilder;
import org.eclipse.esmf.aspectmodel.generator.sql.databricks.DatabricksSqlGenerationConfig;
import org.eclipse.esmf.aspectmodel.generator.sql.databricks.DatabricksSqlGenerationConfigBuilder;
import org.eclipse.esmf.aspectmodel.generator.sql.databricks.DatabricksType;
import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
import org.eclipse.esmf.metamodel.AspectModel;
// AspectModel as returned by the AspectModelLoader
final AspectModel aspectModel = // ...

final DatabricksSqlGenerationConfig databricksSqlGenerationConfig =
      DatabricksSqlGenerationConfigBuilder.builder()
            .commentLanguage( Locale.ENGLISH ) // optional
            .includeTableComment( true )       // optional
            .includeColumnComments( true )     // optional
            .decimalPrecision( 10 )            // optional
            .customColumns( List.of(           // optional
                  DatabricksColumnDefinitionBuilder.builder()
                        .name( "custom_column" )
                        .type( new DatabricksType.DatabricksArray( DatabricksType.STRING ) )
                        .nullable( false )
                        .comment( Optional.of( "Custom column" ) )
                        .build() ) )
            .build();
final SqlGenerationConfig sqlGenerationConfig =
      SqlGenerationConfigBuilder.builder()
            .dialect( SqlGenerationConfig.Dialect.DATABRICKS )
            .mappingStrategy( SqlGenerationConfig.MappingStrategy.DENORMALIZED )
            .dialectSpecificConfig( databricksSqlGenerationConfig )
            .build();
final String result = AspectModelSqlGenerator.INSTANCE.apply( aspectModel.aspect(), sqlGenerationConfig ).getContent();

Databricks Type Mapping

Data types in the Aspect Model are mapped to Databricks types using the following correspondences:

Aspect model type Databricks SQL type Note

xsd:string

STRING

xsd:boolean

BOOLEAN

xsd:decimal

DECIMAL

While xsd:decimal is by definition unbounded, DECIMAL’s default precision is 10 digits and can be up to 38. if we assume values larger than that can appear in the data, the Aspect Models using xsd:decimal should also use a samm-c:FixedPointConstraint accordingly.

xsd:integer

DECIMAL

As opposed to xsd:int, xsd:integer has arbitrary precision, i.e. DECIMAL is needed.

xsd:double

DOUBLE

xsd:float

FLOAT

xsd:date

STRING

DATE can not be used, because it does not retain timezone information.

xsd:time

STRING

xsd:dateTime

STRING

xsd:dateTimeStamp

TIMESTAMP

xsd:gYear

STRING

xsd:gMonth

STRING

xsd:gDay

STRING

xsd:gYearMonth

STRING

xsd:gMonthDay

STRING

xsd:duration

STRING

xsd:yearMonthDuration

STRING

xsd:dayTimeDuration

STRING

xsd:byte

TINYINT

xsd:short

SMALLINT

xsd:int

INT

xsd:long

BIGINT

xsd:unsignedByte

SMALLINT

xsd:unsignedShort

INT

xsd:unsignedInt

BIGINT

xsd:unsignedLong

DECIMAL

xsd:positiveInteger

DECIMAL

xsd:nonNegativeInteger

DECIMAL

xsd:negativeInteger

DECIMAL

xsd:nonPositiveInteger

DECIMAL

xsd:hexBinary

BINARY

xsd:base64Binary

BINARY

xsd:anyURI

STRING

samm:curie

STRING

Generating Java Code for Aspect Models

Java code can be generated from an Aspect model in two ways:

  1. The generated code represents the Aspect payload. Aspects and Entities become Java classes; their Properties become fields in the classes. Characteristics are not first-class elements, but are implicitly represented by the usage of corresponding data types (e.g. using java.util.Set as the type for the Set Characteristic of a Property) or javax.validation annotations. The generated classes can be used in a straightforward fashion, but they do not contain information about the underlying Aspect model such as its version number. Parts of the Aspect model that have no representation in its corresponding JSON payload are not part of those classes either, in particular descriptions and preferred names. These classes are called POJOs (Plain Old Java Objects), as they do not contain logic but serve mainly as data containers.

  2. The generated code represents the Aspect model itself: It is a type-safe variant of the model and includes every information that is also present in the model such as Characteristics, descriptions including language tags and original XSD data types. It is however not intended to store payload corresponding to an Aspect. Theses classes are called static meta classes, because they are created at compile time (static) and talk about the structure of the information, not the information itself (meta).

Depending on the use case, you would either use one or both of the types simultaneously.

To include the Java generator, use the following dependencies:

Maven
<dependency>
    <groupId>org.eclipse.esmf</groupId>
    <artifactId>esmf-aspect-model-java-generator</artifactId>
    <version>2.9.7</version>
</dependency>
Gradle Groovy DSL
implementation 'org.eclipse.esmf:esmf-aspect-model-java-generator:2.9.7'
Gradle Kotlin DSL
implementation("org.eclipse.esmf:esmf-aspect-model-java-generator:2.9.7")

Type Mapping

In the Java code generated from an Aspect model, the scalar Aspect model data types are mapped to native Java types. The following table lists the correspondences.

Aspect model type Java native type

xsd:string

java.lang.String

xsd:boolean

java.lang.Boolean

xsd:decimal

java.math.BigDecimal

xsd:integer

java.math.BigDecimal

xsd:double

java.lang.Double

xsd:float

java.lang.Float

xsd:date

javax.xml.datatype.XMLGregorianCalendar

xsd:time

javax.xml.datatype.XMLGregorianCalendar

xsd:dateTime

javax.xml.datatype.XMLGregorianCalendar

xsd:dateTimeStamp

javax.xml.datatype.XMLGregorianCalendar

xsd:gYear

javax.xml.datatype.XMLGregorianCalendar

xsd:gMonth

javax.xml.datatype.XMLGregorianCalendar

xsd:gDay

javax.xml.datatype.XMLGregorianCalendar

xsd:gYearMonth

javax.xml.datatype.XMLGregorianCalendar

xsd:gMonthDay

javax.xml.datatype.XMLGregorianCalendar

xsd:duration

javax.xml.datatype.Duration

xsd:yearMonthDuration

javax.xml.datatype.Duration

xsd:dayTimeDuration

javax.xml.datatype.Duration

xsd:byte

java.lang.Byte

xsd:short

java.lang.Short

xsd:int

java.lang.Integer

xsd:long

java.lang.Long

xsd:unsignedByte

java.lang.Short

xsd:unsignedShort

java.lang.Integer

xsd:unsignedInt

java.lang.Long

xsd:unsignedLong

java.math.BigInteger

xsd:positiveInteger

java.math.BigInteger

xsd:nonNegativeInteger

java.math.BigInteger

xsd:negativeInteger

java.math.BigInteger

xsd:nonPositiveInteger

java.math.BigInteger

xsd:hexBinary

byte[]

xsd:base64Binary

byte[]

xsd:anyURI

java.net.URI

samm:curie

org.eclipse.esmf.metamodel.datatypes.Curie

Generating POJOs

POJO generation is straightforward; there are two minor differences to the generation of documentation artifacts. Firstly, when instantiating the generator, you pass a flag indicating whether Jackson annotations should be generated in the class. Secondly, the name mapping function passed to the generation method takes a QualifiedName instead of a String, so that you can decide how to handle the package structure of the class.

Show used imports
import org.eclipse.esmf.aspectmodel.java.JavaCodeGenerationConfig;
import org.eclipse.esmf.aspectmodel.java.JavaCodeGenerationConfigBuilder;
import org.eclipse.esmf.aspectmodel.java.pojo.AspectModelJavaGenerator;
import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
import org.eclipse.esmf.metamodel.AspectModel;

import java.io.File;
import org.apache.commons.io.output.NullOutputStream;
// AspectModel as returned by the AspectModelLoader
final AspectModel aspectModel = // ...

final JavaCodeGenerationConfig config = JavaCodeGenerationConfigBuilder.builder()
      .enableJacksonAnnotations( true )
      .packageName( "com.example.mycompany" ) // if left out, package is taken from Aspect's namespace
      .build();
final AspectModelJavaGenerator generator = new AspectModelJavaGenerator( aspectModel.aspect(), config );
generator.generate( qualifiedName -> {
   // Create an output stream for the given qualified Java class name
} );

Generating Static Meta Classes

For the generation of static meta classes, consider the following example:

Show used imports
import org.eclipse.esmf.aspectmodel.java.JavaCodeGenerationConfig;
import org.eclipse.esmf.aspectmodel.java.JavaCodeGenerationConfigBuilder;
import org.eclipse.esmf.aspectmodel.java.metamodel.StaticMetaModelJavaGenerator;
import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
import org.eclipse.esmf.metamodel.AspectModel;
// AspectModel as returned by the AspectModelLoader
final AspectModel aspectModel = // ...

final JavaCodeGenerationConfig config = JavaCodeGenerationConfigBuilder.builder()
      .enableJacksonAnnotations( true )
      .packageName( "com.example.mycompany" ) // if left out, package is taken from Aspect's namespace
      .build();
final StaticMetaModelJavaGenerator generator = new StaticMetaModelJavaGenerator( aspectModel.aspect(), config );
generator.generate( qualifiedName -> {
   // Create an output stream for the given qualified Java class name
} );

To use the generated static meta classes, you need the following additional dependency:

Maven
<dependency>
    <groupId>org.eclipse.esmf</groupId>
    <artifactId>esmf-aspect-static-meta-model-java</artifactId>
    <version>2.9.7</version>
</dependency>
Gradle Groovy DSL
implementation 'org.eclipse.esmf:esmf-aspect-static-meta-model-java:2.9.7'
Gradle Kotlin DSL
implementation("org.eclipse.esmf:esmf-aspect-static-meta-model-java:2.9.7")

Providing Custom Macros for Code Generation

It is possible to change predefined sections of the generated classes by providing custom Velocity templates; see the Velocity User Guide for more information. The custom macros must be defined in a single template file. The path to the template file as well as its name may be passed as arguments to the code generation, e.g. using the SAMM-CLI.

Custom macros may be provided for the following sections:

Section Macro Name Default Macro Provided

Copyright Licence Header

fileHeader

When using custom macros, macros for all sections above must be provided.

Example:

#macro( fileHeader )
/*
* Copyright (c) $currentYear.getValue() Test Inc. All rights reserved.
*/
#end

Modifying and creating Aspect Models

You can use the AspectChangeManager to modify an Aspect Model. Each modifying operation performed on an Aspect Model is called a change. Instances of classes that implement the Change interface can be passed to the AspectChangeManager applyChange() method. Available Change​s include renaming Aspect Model files or Model elements, adding and removing Aspect Model files and moving Aspect Model elements to other or new files in the same or another namespace.

Show used imports
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Optional;

import org.eclipse.esmf.aspectmodel.edit.AspectChangeManager;
import org.eclipse.esmf.aspectmodel.edit.AspectChangeManagerConfig;
import org.eclipse.esmf.aspectmodel.edit.AspectChangeManagerConfigBuilder;
import org.eclipse.esmf.aspectmodel.edit.Change;
import org.eclipse.esmf.aspectmodel.edit.ChangeGroup;
import org.eclipse.esmf.aspectmodel.edit.ChangeReport;
import org.eclipse.esmf.aspectmodel.edit.ChangeReportFormatter;
import org.eclipse.esmf.aspectmodel.edit.change.MoveElementToNewFile;
import org.eclipse.esmf.aspectmodel.edit.change.RenameElement;
import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
import org.eclipse.esmf.metamodel.AspectModel;
final AspectModel aspectModel = new AspectModelLoader().load(
      // a File, InputStream or AspectModelUrn
);

// All changes to an Aspect Model are done using an AspectChangeManager.
// Optionally, you can pass a config object as first constructor argument:
final AspectChangeManagerConfig config = AspectChangeManagerConfigBuilder.builder()
      .detailedChangeReport( true )
      .defaultFileHeader( List.of( "Generated on "
            + new SimpleDateFormat( "dd-MM-yyyy" ).format( new Date() ) ) )
      .build();
final AspectChangeManager changeManager = new AspectChangeManager( config, aspectModel );

// You can create a single change, or you can combine multiple changes into a group.
// For all possible refactoring operations, see classes implementing the Change interface.
final Change refactorModel = new ChangeGroup( List.of(
      // Rename an element. This works with with samm:Aspect and all other model elements.
      new RenameElement( aspectModel.aspect(), "MyAspect" ),
      // Move an element to a new Aspect Model file in the same namespace.
      new MoveElementToNewFile(
            // The element to move.
            aspectModel.aspect().getProperties().get( 0 ),
            // If you intend writing the model to the file system, set the location
            // for the newly created file here.
            Optional.empty() )
) );

// Apply the changes and get a report that summerizes the changes.
final ChangeReport changeReport = changeManager.applyChange( refactorModel );

// If you want to display the change report, you can serialize it to a string:
ChangeReportFormatter.INSTANCE.apply( changeReport, config );

// Alternatively, you can also get views on collections containing modified/
// added/removed files for the last applied change.
changeManager.createdFiles().forEach( file -> System.out.println( "Created: " + file ) );
changeManager.modifiedFiles().forEach( file -> System.out.println( "Modified: " + file ) );
changeManager.removedFiles().forEach( file -> System.out.println( "Removed: " + file ) );

// At this point, you could use the AspectChangeManager's undo() method to revert
// the last change (refactorModel in this case); afterwards you can also redo().
// This functionality is mainly interesting when refactoring the Aspect Model
// interactively.

// If you want to write the model changes to the file system, use the AspectSerializer.
// Each AspectModelFile will be written to its respective source location.
// Alternatively, the AspectSerializer also provides a method to write an AspectModelFile
// into a String.
AspectSerializer.INSTANCE.write( aspectModel );

Accessing the SAMM programmatically

In order to access the source RDF files that describe the SAMM vocabulary, shared Characteristics and Entities as well as Units, you can add a dependency to the esmf-aspect-meta-model artifact. Note that this artifact does not provide Java classes that represent the meta model.

Maven
<dependency>
    <groupId>org.eclipse.esmf</groupId>
    <artifactId>esmf-semantic-aspect-meta-model</artifactId>
    <version>2.1.0</version>
</dependency>
Gradle Groovy DSL
implementation 'org.eclipse.esmf:esmf-semantic-aspect-meta-model:2.1.0'
Gradle Kotlin DSL
implementation("org.eclipse.esmf:esmf-semantic-aspect-meta-model:2.1.0")

In order to access the files via java.lang.Class#getResourceAsStream, you can refer to the following directory structure that is present in the artifact:

.
├── characteristic
│   └── 2.1.0
│       ├── characteristic-definitions.ttl
│       ├── characteristic-instances.ttl
│       ├── characteristic-shapes.ttl
│       └── characteristic-validations.js
├── entity
│   └── 2.1.0
│       ├── FileResource.ttl
│       ├── ThreeDimensionalPosition.ttl
│       └── TimeSeriesEntity.ttl
├── meta-model
│   └── 2.1.0
│       ├── aspect-meta-model-definitions.ttl
│       ├── aspect-meta-model-shapes.ttl
│       ├── prefix-declarations.ttl
│       └── type-conversions.ttl
└── unit
    └── 2.1.0
        └── units.ttl
You can use the MetaModelFile enumeration provided by the esmf-aspect-meta-model-java module to access object representations of the meta model files, for example:
Show used imports
import org.apache.jena.rdf.model.Model;
import org.apache.jena.vocabulary.RDF;

import org.eclipse.esmf.aspectmodel.resolver.modelfile.MetaModelFile;
import org.eclipse.esmf.metamodel.vocabulary.SammNs;
import org.eclipse.esmf.samm.KnownVersion;
final KnownVersion metaModelVersion = KnownVersion.getLatest();
final Model characteristicDefinitions = MetaModelFile.CHARACTERISTIC_DEFINITIONS.sourceModel();

// Do something with the org.apache.jena.org.rdf.model.Model
final int numberOfCharacteristicInstances =
      characteristicDefinitions.listStatements( null, RDF.type, SammNs.SAMM.Characteristic() ).toList().size();
final String result = String.format( "Meta Model Version " + metaModelVersion.toVersionString()
      + " defines " + numberOfCharacteristicInstances + " Characteristic instances" );

Mapping Aspect Models to Asset Administration Shell (AAS) Submodel Templates

The Asset Administration Shell (AAS) and its information model is a widely recognized standard developed by the Industrial Digital Twin Association (IDTA) to express and handle Digital Twins. Central element of the AAS is the concept of Submodels, which describe certain aspects of a Digital Twin.

The SAMM Aspect Meta Model allows to specify aspects of a digital twin and its semantics. The AAS Generator module provides mapping implementations to derive AAS Submodels from SAMM Aspect Models. On the one hand, this allows to integrate SAMM models in AAS environments and on the other hand it allows AAS submodels to be described with rich semantics, as it is possible with SAMM.

Details of the Mapping Concept

In the following section, the mapping rules applied by the generator are explained. The rules apply to SAMM v2.0.0 and AAS Specification Part 1 V3.0RC01.

SAMM AAS Comment

samm:Aspect

aas:Submodel with kind=Template

Empty Asset and AssetAdministrationShell entries are added to the output file

samm:name

aas:Submodel.idShort

samm:preferredName

aas:Submodel.displayName

samm:description

aas:Submodel.description

samm:property

see samm:Property

samm:operation

see samm:Operation

samm:Aspect.urn

aas:Submodel.semanticId

samm:Property

aas:Property, aas:SubmodelElementCollection

The AAS type is derived from the type of the SAMM Characteristic specifying the SAMM property. Depending on the type it is decided what the resulting AAS element will be. In case of an Entity it will result in a SubmodelElementCollection. It will also be a SubmodelElementCollection if the SAMM Characteristic is of a Collection type (see the Characteristics taxonomy). In all other cases an aas:Property will be generated

samm:Property.name

aas:Property.idShort

samm:Property.urn

aas:ConceptDescription.identification.id, aas:Property.semanticId

samm:Property.preferredName

aas:Property.displayName

samm:Property.description

aas:Property.description

Note: Also mapped to aas:DataSpecificationIEC61360.definition of the aas:ConceptDescription of this property

samm:Property.exampleValue

aas:Property.value

samm:Characteristic.dataType

aas:Property.valueType

samm:Operation

Operation

in/out parameters are not used in SAMM so the mapping only generates input variables and output variables in AAS

samm-c:Characteristic

aas:SubmodelElement, aas:ConceptDescription

Characteristics in SAMM define the semantics of a property, which includes there types as well as links to further definitions (standards, dictionaries, etc), a natural language description and name in different languages. Type and description are separated in AAS, which is why there is not a one-to-one mapping of a Characteristic to one element in AAS but rather Characteristics are used in the mapping of Properties, first, to guide the generation process and, second, to capture semantics in ConceptDescriptions of properties with data specification "DataSpecificationIEC61360" of the AAS.

samm-c:Collection

aas:SubmodelElementList, aas:ConceptDescription

The general remarks to Characteristics apply also to Collection type Characteristics. However, properties referencing Collections are mapped to SubmodelElementLists. Specific properties of collections are mapped. samm:Set is unique, samm:SortedSet is unique and sorted, samm:List is sorted.

samm-c:Quantifiable

aas:SubmodelElement, aas:ConceptDescription

The general remarks to Characteristics apply also to Quantifiable type Characteristics. Quantifiables (also Duration and Measurement) reference a unit, which is added to the ConceptDescription corresponding the mapped Characteristic.

samm-c:Either

aas:SubmodelElement, aas:ConceptDescription

The general remarks to Characteristics apply also to Either. However, the Either characteristic has two distinct entries of which one is to be selected. This concept is not present in AAS. Thus, both entries will be written to a Submodel template, where one has to be ignored.

samm-c:Trait

aas:SubmodelElement, aas:ConceptDescription

The general remarks to Characteristics apply also to Trait. However, the constraint of a trait will be ignored and only the base type will be evaluated, which will act as the characteristic of a property.

samm-c:Code

aas:SubmodelElement, aas:ConceptDescription

Similar to plain Characteristic.

samm-c:StructuredValue

aas:SubmodelElement, aas:ConceptDescription

The general remarks to Characteristics apply also to StructuredValue. However, AAS has no concpet like deconstruction rule. Thus, the deconstruction rule and the sub properties of the deconstruction entity will be ignored and only the Characteristic is mapped.

samm-c:Enumeration

aas:SubmodelElement, aas:ConceptDescription

The general remarks to Characteristics apply also to Enumerations. Additionally, the values of an Enumeration are mapped to a valueList of a DataSpecificationIEC61360.

samm-c:State

aas:SubmodelElement, aas:ConceptDescription

Same as Enumeration.

samm-c:MultiLanguageText

aas:MultiLanguageProperty

if a MultiLanguageText is used in SAMM it is mapped to the MultiLanguageProperty in AAS.

Known Limitations

The AAS Generator implements a base set of features, which are mapped from SAMM to AAS. However, there are still limitations:

  • Predefined entity mapping (FileResource would be mapped to aas:File)

  • samm-c:Either is mapped to aas:SubmodelElementCollection with two entries for left and right side

  • Recursive optional properties of SAMM model are not included in output but dropped straight away

Translate Aspect Model to AAS

The following code demonstrates how the API to translate an Aspect Model into one of the valid AAS formats (JSON, XML, AASX) is used:

Show used imports
import org.eclipse.esmf.aspectmodel.aas.AspectModelAasGenerator;
import org.eclipse.esmf.aspectmodel.aas.AasFileFormat;
import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
import org.eclipse.esmf.metamodel.Aspect;
import org.eclipse.esmf.metamodel.AspectModel;
// AspectModel as returned by the AspectModelLoader
final AspectModel aspectModel = // ...

final AspectModelAasGenerator generator = new AspectModelAasGenerator();

// Generate AAS .aasx for input Aspect
generator.generate( AasFileFormat.AASX, aspect, this::outputStreamForName );
// Generate AAS .xml for input Aspect
generator.generate( AasFileFormat.XML, aspect, this::outputStreamForName );
// Generate AAS .json for input Aspect
generator.generate( AasFileFormat.JSON, aspect, this::outputStreamForName );

To include the translator artifact, use the following dependencies:

Maven
<dependency>
    <groupId>org.eclipse.esmf</groupId>
    <artifactId>esmf-aspect-aas-generator</artifactId>
    <version>2.9.7</version>
</dependency>
Gradle Groovy DSL
implementation 'org.eclipse.esmf:esmf-aspect-model-aas-generator:2.9.7'
Gradle Kotlin DSL
implementation("org.eclipse.esmf:esmf-aspect-model-aas-generator:2.9.7")

Translate AAS to Aspect Model

It is also possible to translate AAS Submodel Templates to Aspect Models using a best-effort. The following example shows how the API to translate an AAS environment containing one or more Submodel Templates to Aspect Models:

Show used imports
import java.io.File;
import java.util.List;

import org.eclipse.esmf.aspectmodel.aas.AasFileFormat;
import org.eclipse.esmf.aspectmodel.aas.AasToAspectModelGenerator;
import org.eclipse.esmf.aspectmodel.aas.AspectModelAasGenerator;
import org.eclipse.esmf.metamodel.Aspect;
import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
import org.eclipse.esmf.metamodel.AspectModel;
final File file = // an AAS file that ends in .aasx, .xml or .json

// Multiple "from" methods are available: fromFile (which checks the file extension),
// fromAasJson, fromAasXml, fromAasx as well as fromEnvironment (for an AAS4J AAS environment)
final AasToAspectModelGenerator generator = AasToAspectModelGenerator.fromFile( file );
final List<Aspect> aspects = generator.generateAspects();
aspects.forEach( aspect -> {
   // do something with the generated aspect
} );

To include the translator artifact, use the same same dependencies as shown above in section Translate AAS to Aspect Model.