Skip to content

Commit

Permalink
Provide a generic submodel service component
Browse files Browse the repository at this point in the history
  • Loading branch information
geso02 committed Sep 11, 2024
1 parent 21897ed commit a229ff1
Show file tree
Hide file tree
Showing 40 changed files with 2,130 additions and 0 deletions.
14 changes: 14 additions & 0 deletions basyx.submodelservice/basyx.submodelservice.component/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM amazoncorretto:17
USER nobody
WORKDIR /application
ARG JAR_FILE=target/*-exec.jar
COPY ${JAR_FILE} basyxExecutable.jar
ARG AAS4J_JAR_FILE=target/libs/aas4j-model-1.0.2.jar
COPY ${AAS4J_JAR_FILE} libs/aas4j-model-1.0.2.jar
ARG PORT=8081
ENV SERVER_PORT=${PORT}
ARG CONTEXT_PATH=/
ENV SERVER_SERVLET_CONTEXT_PATH=${CONTEXT_PATH}
EXPOSE ${SERVER_PORT}
HEALTHCHECK --interval=10s --timeout=3s --retries=10 --start-period=5s CMD curl --fail http://localhost:${SERVER_PORT}${SERVER_SERVLET_CONTEXT_PATH%/}/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "basyxExecutable.jar"]
91 changes: 91 additions & 0 deletions basyx.submodelservice/basyx.submodelservice.component/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Eclipse BaSyx - Standalone Submodel Service Component

This project provides a generic Submodel Service component. With this component, you can deploy your own Submodel Services without the need to build a Spring Boot application or create and deploy your own Dockerfiles.

*Invoke* calls to the Submodel element *Operation* are delegated in the generic component to Java classes that can either be precompiled or provided as source code.

These methods can be provided as simple Java classes and do not require additional dependencies for the project. At runtime, the classes from the *aas4j-model* module are already available. Additional libraries can be included via an environment variable.

## Configuration

The [example folder](./example/) contains sample settings. For assigning `idShortPaths` to executable Java classes, configuration should be done using Properties in the form of a Properties file or YAML. Environment variables are not processed correctly by Spring when mapping.

When running the container, the execution folder is /application.

**Performance Note**: The startup time of the service can be significantly reduced if the sources are already compiled and placed as JAR files or class files in the /application directory. Pre-compiling your code before building the Docker image can improve performance and reduce initialization time.

### System Properties

Below are the individual properties used when starting the application. Except for `basyx.submodel.file`, all properties are optional.

| Property | Example | Explanation |
|-------------------------------------------------------|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
| `basyx.submodel.file` | `submodel.json` | Path to the file describing the Submodel. |
| `basyx.operation.java.sourcesPath` | `sources` | Source directory where the Java classes to be compiled are located. These classes can be specified to be loaded at runtime to perform operations. |
| `basyx.operation.java.classesPath` | `classes` | Target directory for compilation. Used in the classpath when loading classes. |
| `basyx.operation.java.additionalClasspath` | `jars/HelloWorld.jar,jars/` | Comma-separated list of additional libraries used during source compilation and loading of executable classes. |
| `basyx.operation.invokation.mappings[SquareOperation]` | `org.example.SquareOp` | Example of a mapping assignment. The `idShortPath` of an operation is assigned to the class `org.example.SquareOp`. |
| `basyx.operation.invokation.mappings[BasicOperations.AddOperation]` | `org.basic.AddOperation` | Another example of mapping an operation to a Java class. |
| `basyx.operation.invokation.defaultMapping` | `org.example.MyOperation` | Specifies the operation to be called if no mapping is found. |

The example project contains an [application.yml](./example/application.yml) that demonstrates how configuration can be specified clearly using YAML.

### Structure of Java Classes

The Java classes that execute operations do not require dependencies and do not need to extend interfaces or implement classes. It is important that they include a method with the following signature:

```java
public OperationVariable[] invoke(String path, Operation op, OperationVariable[] in)
```

Alternatively, the following method can be used if not all arguments are needed:

```java
public OperationVariable[] invoke(OperationVariable[] in)
```

Here is a simple example:

```java
package org.basic;

import org.eclipse.digitaltwin.aas4j.v3.model.DataTypeDefXsd;
import org.eclipse.digitaltwin.aas4j.v3.model.Operation;
import org.eclipse.digitaltwin.aas4j.v3.model.OperationVariable;
import org.eclipse.digitaltwin.aas4j.v3.model.Property;
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultOperationVariable;
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty;

public class AddOperation {

public OperationVariable[] invoke(String path, Operation op, OperationVariable[] in) {
Property first = (Property) in[0].getValue();
Property second = (Property) in[1].getValue();
int iFirst = Integer.parseInt(first.getValue());
int iSecond = Integer.parseInt(second.getValue());
int result = iFirst + iSecond;
Property prop = new DefaultProperty.Builder()
.value(String.valueOf(result))
.valueType(DataTypeDefXsd.INT)
.build();
return new OperationVariable[] { new DefaultOperationVariable.Builder().value(prop).build() };
}
}
```

The execution is stateless. A new instance is created each time.

### Creating a Custom Image

This setup allows for quick deployment of Submodel Services. However, if you want to avoid configuration and Docker volume binding, you can also create your own images quickly:

```dockerfile
FROM eclipsebasyx/submodel-service:0.2.0-SNAPSHOT
COPY sources/ /application/sources
COPY jars/ /application/jars
COPY submodel.json /application/submodel.json
COPY application.yml /application/config/application.yml
```

The operations can also be precompiled and placed as JARs in the `/application/jars` folder, which is referenced in the `application.yml`. Alternatively, you can place Java classes of the correct syntax in the source folder and reference them in the properties via a mapping.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM eclipsebasyx/submodel-service:0.2.0-SNAPSHOT
COPY sources/ /application/sources
COPY jars/ /application/jars
COPY submodel.json /application/submodel.json
COPY application.yml /application/config/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## Submodel Service Component Example

This example describes a potential setup for the Submodel Service using Docker Compose.

## Configuration

The [docker-compose.yml](docker-compose.yml) file provides a basic setup for starting the service. The Docker image used is initially built based on the [Dockerfile located in the parent directory](../Dockerfile).

Volumes are used to provide the Submodel and the executable source code to the container, which are referenced in the environment section. The mapping of `idShortPath` to Java classes is also referenced there and loaded via [application-mappings.yml](application-mappings.yml). Alternatively, you can simplify the setup by configuring everything directly in [application.yml](application.yml).

**Performance Note:** For faster startup times, it's recommended to pre-compile your code and provide it as JAR files or class files. The [aas4j](https://github.com/eclipse-aas4j/aas4j) model classes are available at runtime and do not need to be added to the classpath.

### Test Script

Please review the [start-container.sh](start-container.sh) shell script and execute it. The script first builds the executable JAR using Maven, if necessary, which is then copied into the Docker image. After that, the Docker Compose stack is started, and test cases are executed.

## Standalone Image

The [Dockerfile.standalone-example](Dockerfile.standalone-example) provides an example of how a Dockerfile for a standalone service might look. When using standalone images, mounting additional volumes or setting environment variables is not strictly necessary. For standalone images, it's recommended to provide precompiled classes or a JAR file to expedite the service startup. The `aas4j` model classes are available at runtime and do not need to be added to the classpath.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
basyx:
operation:
invokation:
mappings:
SquareOperation: SquareOperation
BasicOperations.AddOperation: basic.ops.AddOperation
BasicOperations.HelloOperation: HelloOperation
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
basyx:
submodel:
file: submodel.json
operation:
java:
sourcesPath: sources
classesPath: classes
additionalClasspath:
- jars/HelloWorld.jar
invokation:
mappings:
SquareOperation: SquareOperation
"BasicOperations.AddOperation": basic.ops.AddOperation
BasicOperations.HelloOperation: HelloOperation
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
version: "3.9"
services:
submodel-service:
image: eclipsebasyx/submodel-service:test
build:
context: ..
container_name: submodel-service
environment:
BASYX_SUBMODEL_FILE: submodel.json
BASYX_OPERATION_JAVA_SOURCESPATH: sources
BASYX_OPERATION_JAVA_CLASSESPATH: classes
BASYX_OPERATION_JAVA_ADDITIONALCLASSPATH: jars/HelloWorld.jar
# we should use properties or yml files for operation mappings
# Environment variables could not be parsed properly
# dots of the idShortPath are not handled properly by spring and also the lower-case conversion of spring is problematic
# so either we use config file for mappings or define everything in a .properties or .yml file
SPRING_PROFILES_ACTIVE: mappings
ports:
- 8111:8081
volumes:
- ./submodel.json:/application/submodel.json:ro
- ./sources/:/application/sources/:ro
- ./jars/:/application/jars/:ro
- ./application-mappings.yml:/application/config/application-mappings.yml/:ro
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import org.eclipse.digitaltwin.aas4j.v3.model.DataTypeDefXsd;
import org.eclipse.digitaltwin.aas4j.v3.model.OperationVariable;
import org.eclipse.digitaltwin.aas4j.v3.model.Property;
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultOperationVariable;
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty;

public class HelloOperation {

public OperationVariable[] invoke(OperationVariable[] in) {
String value = new HelloWorld().sayHello();
Property prop = new DefaultProperty.Builder().value(value).valueType(DataTypeDefXsd.STRING).build();
return new OperationVariable[] {new DefaultOperationVariable.Builder().value(prop).build()};
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import org.eclipse.digitaltwin.aas4j.v3.model.DataTypeDefXsd;
import org.eclipse.digitaltwin.aas4j.v3.model.OperationVariable;
import org.eclipse.digitaltwin.aas4j.v3.model.Property;
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultOperationVariable;
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty;

public class SquareOperation {

public OperationVariable[] invoke(OperationVariable[] vars) {
Property prop = (Property) vars[0].getValue();
int value = Integer.parseInt(prop.getValue());
int result = value * value;
Property toReturn = new DefaultProperty.Builder().value(String.valueOf(result)).valueType(DataTypeDefXsd.INT).build();
return new OperationVariable[] {new DefaultOperationVariable.Builder().value(toReturn).build()};
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package basic.ops;

import java.util.List;

import org.eclipse.digitaltwin.aas4j.v3.model.DataTypeDefXsd;
import org.eclipse.digitaltwin.aas4j.v3.model.Operation;
import org.eclipse.digitaltwin.aas4j.v3.model.OperationVariable;
import org.eclipse.digitaltwin.aas4j.v3.model.Property;
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultOperationVariable;
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty;

public class AddOperation {

public OperationVariable[] invoke(String path, Operation op, OperationVariable[] in) {
Property first = (Property) in[0].getValue();
Property second = (Property) in[1].getValue();
int iFirst = Integer.parseInt(first.getValue());
int iSecond = Integer.parseInt(second.getValue());
int result = iFirst + iSecond;
Property prop = new DefaultProperty.Builder().value(String.valueOf(result)).valueType(DataTypeDefXsd.INT).build();
return new OperationVariable[] {new DefaultOperationVariable.Builder().value(prop).build()};
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/bin/bash

project_was_built() {
if find ../target -type f -name "basyx.submodelservice.component*-exec.jar" | grep -q "^"; then
return 0
else
return 1
fi
}

build_maven() {
echo building maven artifacts ...
mvn -f ../../../pom.xml clean install -DskipTests
}

run_square() {
echo "Please enter an integer value:"
read -r int_value
response=$(curl http://localhost:8111/submodel/submodel-elements/SquareOperation/invoke \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d "{ \"inputArguments\" : [{ \"value\" : { \"modelType\" : \"Property\", \"value\" : \"${int_value}\" }}]}" \
-s | jq '.outputArguments[0].value.value')
echo result: $response
}


run_add() {
echo "Please enter the first integer value:"
read -r int_value1
echo "Please enter the second integer value:"
read -r int_value2
response=$(curl http://localhost:8111/submodel/submodel-elements/BasicOperations.AddOperation/invoke \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d "{ \"inputArguments\" : [{ \"value\" : { \"modelType\" : \"Property\", \"value\" : \"${int_value1}\" }},{ \"value\" : { \"modelType\" : \"Property\", \"value\" : \"${int_value2}\" }}]}" \
-s | jq '.outputArguments[0].value.value')
echo result: $response
}

run_hello() {
response=$(curl http://localhost:8111/submodel/submodel-elements/BasicOperations.HelloOperation/invoke \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d "{ }" \
-s | jq '.outputArguments[0].value.value')
echo result: $response
}

run_tests() {
echo We are now running tests with curl

echo The following operations can be invoked:
echo "'s': square operation"
echo "'h': hello world operation"
echo "'a': add operation"
echo "'e': exit"
while true; do
# Nutzer nach Eingabe fragen
echo "Please enter 's', 'h', 'a' or 'e' to exit:"
read -r user_input

# Prüfen, welche Eingabe gemacht wurde
if [[ "$user_input" == "s" ]]; then
run_square
elif [[ "$user_input" == "h" ]]; then
run_hello
elif [[ "$user_input" == "a" ]]; then
run_add
elif [[ "$user_input" == "e" ]]; then
echo Exit...
break
else
echo "Input not valid. Please enter 's', 'h', 'a' or 'e'."
fi
done
}

if ! project_was_built; then
read -p "The maven artifact does not exist. Do you want to build all maven artifacts now? [Y/n]: " build_maven
build_maven=${build_maven:-Y}
if [[ "$build_maven" =~ ^[Yy]$ ]]; then
build_maven;
else
echo abort
return;
fi
fi

docker-compose up --build --force-recreate --detach --wait
echo service started: http://localhost:8111/submodel

run_tests;

echo "Shutting down containers"
docker-compose down
Loading

0 comments on commit a229ff1

Please sign in to comment.