diff --git a/basyx.aasenvironment/basyx.aasenvironment.component/pom.xml b/basyx.aasenvironment/basyx.aasenvironment.component/pom.xml
index e4f8db930..458bb4553 100644
--- a/basyx.aasenvironment/basyx.aasenvironment.component/pom.xml
+++ b/basyx.aasenvironment/basyx.aasenvironment.component/pom.xml
@@ -83,6 +83,32 @@
org.eclipse.digitaltwin.basyx
basyx.aasenvironment-feature-authorization
+
+ org.eclipse.digitaltwin.basyx
+ basyx.aasrepository-feature-kafka
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelrepository-feature-kafka
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.aasrepository-feature-kafka
+ tests
+ test
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelservice-feature-kafka
+ tests
+ test
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelrepository-feature-kafka
+ tests
+ test
+
org.eclipse.digitaltwin.basyx
basyx.http
@@ -100,8 +126,14 @@
test
- org.eclipse.digitaltwin.basyx
- basyx.mongodbcore
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.junit.vintage
+ junit-vintage-engine
+ test
org.springframework.boot
diff --git a/basyx.aasenvironment/basyx.aasenvironment.component/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/component/KafkaEventsInMemoryStorageIntegrationTest.java b/basyx.aasenvironment/basyx.aasenvironment.component/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/component/KafkaEventsInMemoryStorageIntegrationTest.java
new file mode 100644
index 000000000..259db165e
--- /dev/null
+++ b/basyx.aasenvironment/basyx.aasenvironment.component/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/component/KafkaEventsInMemoryStorageIntegrationTest.java
@@ -0,0 +1,155 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.aasenvironment.component;
+
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonSerializer;
+import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell;
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.eclipse.digitaltwin.basyx.aasrepository.AasRepository;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.AasEventKafkaListener;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.KafkaAasRepositoryFeature;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.TestShells;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events.model.AasEvent;
+import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo;
+import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository;
+import org.eclipse.digitaltwin.basyx.submodelrepository.feature.kafka.KafkaSubmodelRepositoryFeature;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.SubmodelEventKafkaListener;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.TestSubmodels;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.SubmodelEvent;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.MediaType;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.annotation.DirtiesContext.ClassMode;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
+
+/**
+ * @author sonnenberg (DFKI GmbH)
+ */
+@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
+@ComponentScan(basePackages = { "org.eclipse.digitaltwin.basyx"})
+@RunWith(SpringRunner.class)
+@TestPropertySource(properties = {
+ "basyx.environment=",
+ "basyx.feature.kafka.enabled=true",
+ "spring.kafka.bootstrap-servers=PLAINTEXT_HOST://localhost:9092"
+})
+@AutoConfigureMockMvc
+@Import({ SubmodelEventKafkaListener.class, AasEventKafkaListener.class})
+public class KafkaEventsInMemoryStorageIntegrationTest {
+
+ @Autowired
+ private AasEventKafkaListener aasEventListener;
+
+ @Autowired
+ private SubmodelEventKafkaListener submodelEventListener;
+
+ @Autowired
+ private KafkaAasRepositoryFeature aasFeature;
+
+ @Autowired
+ private KafkaSubmodelRepositoryFeature submodelFeature;
+
+ @Autowired
+ private MockMvc mvc;
+
+ @Autowired
+ private JsonSerializer serializer;
+
+ @Autowired
+ private SubmodelRepository smRepo;
+
+ @Autowired
+ private AasRepository aasRepo;
+
+ @Before
+ public void awaitAssignment() throws InterruptedException {
+ aasEventListener.awaitTopicAssignment();
+ submodelEventListener.awaitTopicAssignment();
+
+ cleanup();
+ }
+
+ @Test
+ public void testCreateAas() throws Exception {
+ AssetAdministrationShell shell = TestShells.shell();
+ String body = serializer.write(shell);
+
+ mvc.perform(MockMvcRequestBuilders.post("/shells").contentType(MediaType.APPLICATION_JSON).content(body).accept(MediaType.APPLICATION_JSON))
+ .andExpect(MockMvcResultMatchers.status().isCreated())
+ .andExpect(MockMvcResultMatchers.content().json(body));
+ AasEvent aasEvt = aasEventListener.next();
+ Assert.assertEquals(shell, aasEvt.getAas());
+ Assert.assertEquals(shell.getId(), aasEvt.getId());
+ Assert.assertNull(aasEvt.getSubmodelId());
+ Assert.assertNull(aasEvt.getAssetInformation());
+ Assert.assertNull(aasEvt.getReference());
+
+ Submodel sm = TestSubmodels.createSubmodel("http://submodels/123", "123", "hello");
+ body = serializer.write(sm);
+ mvc.perform(MockMvcRequestBuilders.post("/submodels").contentType(MediaType.APPLICATION_JSON).content(body).accept(MediaType.APPLICATION_JSON))
+ .andExpect(MockMvcResultMatchers.status().isCreated());
+ SubmodelEvent smEvt = submodelEventListener.next();
+ Assert.assertEquals(sm, smEvt.getSubmodel());
+ Assert.assertEquals(sm.getId(), smEvt.getId());
+ Assert.assertNull(smEvt.getSmElement());
+ Assert.assertNull(smEvt.getSmElementPath());
+ }
+
+
+ @Test
+ public void testFeatureIsEnabled() {
+ Assert.assertTrue(aasFeature.isEnabled());
+ Assert.assertTrue(submodelFeature.isEnabled());
+ }
+
+ @After
+ public void cleanup() throws InterruptedException {
+ for (AssetAdministrationShell aas : aasRepo.getAllAas(new PaginationInfo(null, null)).getResult()) {
+ aasRepo.deleteAas(aas.getId());
+ }
+
+ for (Submodel sm : smRepo.getAllSubmodels(new PaginationInfo(null, null)).getResult()) {
+ smRepo.deleteSubmodel(sm.getId());
+ }
+ while(submodelEventListener.next(300, TimeUnit.MICROSECONDS) != null);
+ }
+}
diff --git a/basyx.aasregistry/basyx.aasregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/integration/BaseIntegrationTest.java b/basyx.aasregistry/basyx.aasregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/integration/BaseIntegrationTest.java
index da381d92b..bdb796516 100644
--- a/basyx.aasregistry/basyx.aasregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/integration/BaseIntegrationTest.java
+++ b/basyx.aasregistry/basyx.aasregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/integration/BaseIntegrationTest.java
@@ -139,20 +139,23 @@ public abstract class BaseIntegrationTest {
protected RegistryAndDiscoveryInterfaceApi api;
@Before
- public void initClient() throws ApiException {
+ public void setUp() throws Exception {
+ initClient();
+ cleanup();
+ }
+
+ protected void initClient() throws Exception {
api = new RegistryAndDiscoveryInterfaceApi("http", "127.0.0.1", port);
- api.deleteAllShellDescriptors();
- queue().assertNoAdditionalMessage();
}
- @After
- public void cleanup() throws ApiException {
- queue().assertNoAdditionalMessage();
+ protected void cleanup() throws ApiException, InterruptedException {
+ queue().pullAdditionalMessages();
GetAssetAdministrationShellDescriptorsResult result = api.getAllAssetAdministrationShellDescriptors(null, null, null, null);
for (AssetAdministrationShellDescriptor eachDescriptor : result.getResult()) {
api.deleteAssetAdministrationShellDescriptorById(eachDescriptor.getId());
assertThatEventWasSend(RegistryEvent.builder().id(eachDescriptor.getId()).type(EventType.AAS_UNREGISTERED).build());
}
+ queue().pullAdditionalMessages();
}
@Test
@@ -230,7 +233,6 @@ public void whenDeleteAll_thenAllDescriptorsAreRemoved() throws ApiException {
assertThat(events.remove(RegistryEvent.builder().id("id_" + i).type(EventType.AAS_UNREGISTERED).build())).isTrue();
}
assertThat(events.isEmpty());
- queue().assertNoAdditionalMessage();
}
@Test
@@ -246,7 +248,7 @@ public void whenCreateAndDeleteDescriptors_thenAllDescriptorsAreRemoved() throws
all = api.getAllAssetAdministrationShellDescriptors(null, null, null, null).getResult();
assertThat(all).isEmpty();
- queue().assertNoAdditionalMessage();
+ queue().pullAdditionalMessages();
}
@Test
@@ -279,7 +281,7 @@ public void whenRegisterAndUnregisterSubmodel_thenSubmodelIsCreatedAndDeleted()
aasDescriptor = api.getAssetAdministrationShellDescriptorById(aasId);
assertThat(aasDescriptor.getSubmodelDescriptors()).doesNotContain(toRegister);
- queue().assertNoAdditionalMessage();
+ queue().pullAdditionalMessages();
}
@Test
diff --git a/basyx.aasregistry/basyx.aasregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/integration/EventQueue.java b/basyx.aasregistry/basyx.aasregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/integration/EventQueue.java
index 57cdb8a7f..c5c71dd45 100644
--- a/basyx.aasregistry/basyx.aasregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/integration/EventQueue.java
+++ b/basyx.aasregistry/basyx.aasregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/integration/EventQueue.java
@@ -55,16 +55,8 @@ public void reset() {
}
}
- public void assertNoAdditionalMessage() {
- try {
- String message = messageQueue.poll(1, TimeUnit.SECONDS);
- if (message != null) {
- throw new EventListenerException("Got additional message: " + message);
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new EventListenerException(e);
- }
+ public void pullAdditionalMessages() throws InterruptedException {
+ while(messageQueue.poll(100, TimeUnit.MILLISECONDS) != null);
}
public RegistryEvent poll() {
@@ -81,11 +73,11 @@ public RegistryEvent poll() {
throw new EventListenerException(e);
}
}
-
+
public static final class EventListenerException extends RuntimeException {
private static final long serialVersionUID = 1L;
-
+
public EventListenerException(Throwable e) {
super(e);
}
diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/memory/KafkaEventsInMemoryStorageIntegrationTest.java b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/memory/KafkaEventsInMemoryStorageIntegrationTest.java
index 182f8fbc8..c4933703a 100644
--- a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/memory/KafkaEventsInMemoryStorageIntegrationTest.java
+++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/memory/KafkaEventsInMemoryStorageIntegrationTest.java
@@ -24,7 +24,6 @@
******************************************************************************/
package org.eclipse.digitaltwin.basyx.aasregistry.service.storage.memory;
-import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -32,9 +31,7 @@
import org.apache.kafka.common.TopicPartition;
import org.eclipse.digitaltwin.basyx.aasregistry.service.tests.integration.BaseIntegrationTest;
import org.eclipse.digitaltwin.basyx.aasregistry.service.tests.integration.EventQueue;
-import org.junit.Before;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
import org.springframework.kafka.annotation.KafkaHandler;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.listener.ConsumerSeekAware;
@@ -51,10 +48,12 @@ public class KafkaEventsInMemoryStorageIntegrationTest extends BaseIntegrationTe
@Autowired
private RegistrationEventKafkaListener listener;
- @Before
- public void awaitAssignment() throws InterruptedException {
+ @Override
+ public void setUp() throws Exception {
listener.awaitTopicAssignment();
+ super.setUp();
}
+
@Override
public EventQueue queue() {
@@ -68,9 +67,6 @@ private static class RegistrationEventKafkaListener implements ConsumerSeekAware
private final EventQueue queue;
private final CountDownLatch latch = new CountDownLatch(1);
- @Value("${spring.kafka.template.default-topic}")
- private String topicName;
-
@SuppressWarnings("unused")
public RegistrationEventKafkaListener(ObjectMapper mapper) {
this.queue = new EventQueue(mapper);
@@ -85,8 +81,7 @@ public void receiveMessage(String content) {
public void onPartitionsAssigned(Map assignments,
ConsumerSeekCallback callback) {
for (TopicPartition eachPartition : assignments.keySet()) {
- if (topicName.equals(eachPartition.topic())) {
- callback.seekToEnd(List.of(eachPartition));
+ if ("aas-registry".equals(eachPartition.topic())) {
latch.countDown();
}
}
diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/AuthorizedClientTest.java b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/AuthorizedClientTest.java
index 0fb2192e1..8e78b4521 100644
--- a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/AuthorizedClientTest.java
+++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/AuthorizedClientTest.java
@@ -61,9 +61,10 @@ public class AuthorizedClientTest extends BaseIntegrationTest {
@Value("${local.server.port}")
private int port;
- @Before
- public void awaitAssignment() throws InterruptedException {
+ @Override
+ public void setUp() throws Exception {
listener.awaitTopicAssignment();
+ super.setUp();
}
@Override
@@ -71,13 +72,12 @@ public EventQueue queue() {
return listener.getQueue();
}
- @Before
@Override
- public void initClient() throws ApiException {
+ public void initClient() throws Exception {
api = new AuthorizedConnectedAasRegistry("http://127.0.0.1:" + port, new TokenManager("http://localhost:9096/realms/BaSyx/protocol/openid-connect/token", new ClientCredentialAccessTokenProvider(new ClientCredential("workstation-1", "nY0mjyECF60DGzNmQUjL81XurSl8etom"))));
api.deleteAllShellDescriptors();
- queue().assertNoAdditionalMessage();
+ queue().pullAdditionalMessages();
}
@Test
@@ -110,4 +110,4 @@ public void whenPostShellDescriptor_LocationIsReturned() throws ApiException, IO
// TODO: It uses normal GET unauthorized request, need to override and refactor
}
-}
+}
\ No newline at end of file
diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/KafkaEventsMongoDbStorageIntegrationTest.java b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/KafkaEventsMongoDbStorageIntegrationTest.java
index 0edaf75c5..b268db742 100644
--- a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/KafkaEventsMongoDbStorageIntegrationTest.java
+++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/KafkaEventsMongoDbStorageIntegrationTest.java
@@ -32,7 +32,6 @@
import org.apache.kafka.common.TopicPartition;
import org.eclipse.digitaltwin.basyx.aasregistry.service.tests.integration.BaseIntegrationTest;
import org.eclipse.digitaltwin.basyx.aasregistry.service.tests.integration.EventQueue;
-import org.junit.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.kafka.annotation.KafkaHandler;
@@ -51,9 +50,10 @@ public class KafkaEventsMongoDbStorageIntegrationTest extends BaseIntegrationTes
@Autowired
private RegistrationEventKafkaListener listener;
- @Before
- public void awaitAssignment() throws InterruptedException {
+ @Override
+ public void setUp() throws Exception {
listener.awaitTopicAssignment();
+ super.setUp();
}
@Override
@@ -71,7 +71,6 @@ public static class RegistrationEventKafkaListener implements ConsumerSeekAware
@Value("${spring.kafka.template.default-topic}")
private String topicName;
- @SuppressWarnings("unused")
public RegistrationEventKafkaListener(ObjectMapper mapper) {
this.queue = new EventQueue(mapper);
}
@@ -89,7 +88,6 @@ public void receiveMessage(String content) {
public void onPartitionsAssigned(Map assignments, ConsumerSeekCallback callback) {
for (TopicPartition eachPartition : assignments.keySet()) {
if (topicName.equals(eachPartition.topic())) {
- callback.seekToEnd(List.of(eachPartition));
latch.countDown();
}
}
diff --git a/basyx.aasrepository/basyx.aasrepository-feature-kafka/README.md b/basyx.aasrepository/basyx.aasrepository-feature-kafka/README.md
new file mode 100644
index 000000000..3e2326622
--- /dev/null
+++ b/basyx.aasrepository/basyx.aasrepository-feature-kafka/README.md
@@ -0,0 +1,46 @@
+# AssetAdministrationShell Repository - KAFKA Eventing
+
+This feature provides KAFKA eventing. Messages are sent whenever a resource in the repository is created, updated, or deleted.
+
+A key concern is that the insertion order is preserved for each Shell event. Events for creating, updating, and deleting must not overtake each other. For this reason, only one topic is used, and all messages related to a specific Shell are placed in one partition, by using the Shell ID as the message key. Only within a partition is the order of messages guaranteed to be maintained when consumed.
+
+## Configuration of the Feature
+
+The feature is configured through the following Spring properties:
+
+| Property | Default | Description |
+|-----------------------------------------------|-----------------|----------------------------------------------------------------------------------------------|
+| basyx.aasrepository.feature.kafka.enabled | false | Specifies whether the feature is enabled for AAS repository |
+| basyx.feature.kafka.enabled | false | Specifies whether the feature is enabled for both the AAS repository and Submodel repository |
+| basyx.aasrepository.feature.kafka.topic.name | aas-events | The name of the topic where events are sent |
+| spring.kafka.bootstrap-servers | - | The address of the Kafka brokers, e.g., PLAINTEXT_HOST://localhost:9092 |
+
+## Structure of the Messages
+
+The values are transferred as strings in JSON format:
+
+```json
+{
+ "type": "AAS_CREATED",
+ "id" : "http://aas.ids.org/1",
+ "aas" : {
+ "modelType": "AssetAdministrationShell",
+ "id" : "http://aas.ids.org/1",
+ "idShort" : "1"
+ },
+ "submodelId" : null,
+ "reference" : null,
+ "assetInformation" : null
+}
+```
+
+Depending on the event type, the fields of the JSON message are set. The following event types are available:
+
+* AAS_CREATED
+* AAS_UPDATED
+* AAS_DELETED
+* SM_REF_ADDED
+* SM_REF_DELETED
+* ASSET_INFORMATION_SET
+
+
diff --git a/basyx.aasrepository/basyx.aasrepository-feature-kafka/pom.xml b/basyx.aasrepository/basyx.aasrepository-feature-kafka/pom.xml
new file mode 100644
index 000000000..52404644b
--- /dev/null
+++ b/basyx.aasrepository/basyx.aasrepository-feature-kafka/pom.xml
@@ -0,0 +1,62 @@
+
+ 4.0.0
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.aasrepository
+ ${revision}
+
+
+ basyx.aasrepository-feature-kafka
+ BaSyx AAS Repository feature-kafka
+ BaSyx AAS Repository feature-kafka
+
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.aasrepository-core
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.aasrepository-core
+ tests
+ test
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.aasrepository-backend-inmemory
+ test
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.aasservice-backend-inmemory
+ test
+
+
+ org.eclipse.digitaltwin.aas4j
+ aas4j-dataformat-json
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.junit.vintage
+ junit-vintage-engine
+ test
+
+
+ org.hamcrest
+ hamcrest-core
+
+
+
+
+
\ No newline at end of file
diff --git a/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/KafkaAasRepository.java b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/KafkaAasRepository.java
new file mode 100644
index 000000000..0c9258afb
--- /dev/null
+++ b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/KafkaAasRepository.java
@@ -0,0 +1,130 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.List;
+
+import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell;
+import org.eclipse.digitaltwin.aas4j.v3.model.AssetInformation;
+import org.eclipse.digitaltwin.aas4j.v3.model.Reference;
+import org.eclipse.digitaltwin.basyx.aasrepository.AasRepository;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events.AasEventHandler;
+import org.eclipse.digitaltwin.basyx.core.exceptions.CollidingIdentifierException;
+import org.eclipse.digitaltwin.basyx.core.exceptions.ElementDoesNotExistException;
+import org.eclipse.digitaltwin.basyx.core.pagination.CursorResult;
+import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public class KafkaAasRepository implements AasRepository {
+
+ private AasRepository decorated;
+ private AasEventHandler eventHandler;
+
+ public KafkaAasRepository(AasRepository decorated, AasEventHandler handler) {
+ this.decorated = decorated;
+ this.eventHandler = handler;
+ }
+
+ @Override
+ public CursorResult> getAllAas(PaginationInfo pInfo) {
+ return decorated.getAllAas(pInfo);
+ }
+
+ @Override
+ public AssetAdministrationShell getAas(String aasId) throws ElementDoesNotExistException {
+ return decorated.getAas(aasId);
+ }
+
+ @Override
+ public void createAas(AssetAdministrationShell aas) throws CollidingIdentifierException {
+ decorated.createAas(aas);
+ eventHandler.onAasCreated(aas);
+ }
+
+ @Override
+ public void updateAas(String aasId, AssetAdministrationShell aas) {
+ decorated.updateAas(aasId, aas);
+ eventHandler.onAasUpdated(aasId, aas);
+ }
+
+ @Override
+ public void deleteAas(String aasId) {
+ decorated.deleteAas(aasId);
+ eventHandler.onAasDeleted(aasId);
+ }
+
+ @Override
+ public String getName() {
+ return decorated.getName();
+ }
+
+ @Override
+ public CursorResult> getSubmodelReferences(String aasId, PaginationInfo pInfo) {
+ return decorated.getSubmodelReferences(aasId, pInfo);
+ }
+
+ @Override
+ public void addSubmodelReference(String aasId, Reference submodelReference) {
+ decorated.addSubmodelReference(aasId, submodelReference);
+ eventHandler.onSubmodelRefAdded(aasId, submodelReference);
+ }
+
+ @Override
+ public void removeSubmodelReference(String aasId, String submodelId) {
+ decorated.removeSubmodelReference(aasId, submodelId);
+ eventHandler.onSubmodelRefDeleted(aasId, submodelId);
+ }
+
+ @Override
+ public void setAssetInformation(String aasId, AssetInformation aasInfo) throws ElementDoesNotExistException {
+ decorated.setAssetInformation(aasId, aasInfo);
+ eventHandler.onAssetInformationSet(aasId, aasInfo);
+ }
+
+ @Override
+ public AssetInformation getAssetInformation(String aasId) throws ElementDoesNotExistException {
+ return decorated.getAssetInformation(aasId);
+ }
+
+ @Override
+ public File getThumbnail(String aasId) {
+ return decorated.getThumbnail(aasId);
+ }
+
+ @Override
+ public void setThumbnail(String aasId, String fileName, String contentType, InputStream inputStream) {
+ decorated.setThumbnail(aasId, fileName, contentType, inputStream);
+ }
+
+ @Override
+ public void deleteThumbnail(String aasId) {
+ decorated.deleteThumbnail(aasId);
+ }
+}
diff --git a/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/KafkaAasRepositoryConfiguration.java b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/KafkaAasRepositoryConfiguration.java
new file mode 100644
index 000000000..2a544966b
--- /dev/null
+++ b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/KafkaAasRepositoryConfiguration.java
@@ -0,0 +1,66 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+
+package org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka;
+
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonSerializer;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events.AasEventDistributer;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events.AasEventHandler;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events.DistributingAasEventHandler;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events.KafkaAasEventDistributer;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.core.KafkaTemplate;
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+@ConditionalOnExpression(KafkaAasRepositoryFeature.FEATURE_ENABLED_EXPRESSION)
+@Configuration
+public class KafkaAasRepositoryConfiguration {
+
+ @ConditionalOnMissingBean
+ @Bean
+ public JsonSerializer aas4jSerializer() {
+ return new JsonSerializer();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public AasEventDistributer aasEventDistributer(JsonSerializer serializer,
+ KafkaTemplate template,
+ @Value("${" + KafkaAasRepositoryFeature.FEATURENAME + ".topic.name:aas-events}") String topicName) {
+ return new KafkaAasEventDistributer(serializer, template, topicName);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public AasEventHandler aasEventHandler(AasEventDistributer distributer) {
+ return new DistributingAasEventHandler(distributer);
+ }
+}
diff --git a/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/KafkaAasRepositoryFactory.java b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/KafkaAasRepositoryFactory.java
new file mode 100644
index 000000000..092525d6a
--- /dev/null
+++ b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/KafkaAasRepositoryFactory.java
@@ -0,0 +1,50 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka;
+
+import org.eclipse.digitaltwin.basyx.aasrepository.AasRepository;
+import org.eclipse.digitaltwin.basyx.aasrepository.AasRepositoryFactory;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events.AasEventHandler;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public class KafkaAasRepositoryFactory implements AasRepositoryFactory {
+
+ private final AasRepositoryFactory decorated;
+ private final AasEventHandler evtHandler;
+
+ public KafkaAasRepositoryFactory(AasRepositoryFactory decorated, AasEventHandler evtHandler) {
+ this.decorated = decorated;
+ this.evtHandler = evtHandler;
+ }
+
+ @Override
+ public AasRepository create() {
+ return new KafkaAasRepository(decorated.create(), evtHandler);
+ }
+
+}
diff --git a/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/KafkaAasRepositoryFeature.java b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/KafkaAasRepositoryFeature.java
new file mode 100644
index 000000000..a30a9aa68
--- /dev/null
+++ b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/KafkaAasRepositoryFeature.java
@@ -0,0 +1,75 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka;
+
+import org.eclipse.digitaltwin.basyx.aasrepository.AasRepositoryFactory;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.AasRepositoryFeature;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events.AasEventHandler;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.stereotype.Component;
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+@ConditionalOnExpression("#{${" + KafkaAasRepositoryFeature.FEATURENAME + ".enabled:false} or ${basyx.feature.kafka.enabled:false}}")
+@Component
+public class KafkaAasRepositoryFeature implements AasRepositoryFeature {
+
+ public final static String FEATURENAME = "basyx.aasrepository.feature.kafka";
+
+ public final static String FEATURE_ENABLED_EXPRESSION = "#{${" + FEATURENAME + ".enabled:false} or ${basyx.feature.kafka.enabled:false}}";
+
+ private final AasEventHandler evtHandler;
+
+ @Autowired
+ public KafkaAasRepositoryFeature(AasEventHandler evtHandler) {
+ this.evtHandler = evtHandler;
+ }
+
+ @Override
+ public AasRepositoryFactory decorate(AasRepositoryFactory aasServiceFactory) {
+ return new KafkaAasRepositoryFactory(aasServiceFactory, evtHandler);
+ }
+
+ @Override
+ public void initialize() {
+ }
+
+ @Override
+ public void cleanUp() {
+
+ }
+
+ @Override
+ public String getName() {
+ return "AasRepository KAFKA";
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+}
diff --git a/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/events/AasEventDistributer.java b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/events/AasEventDistributer.java
new file mode 100644
index 000000000..3a5154b24
--- /dev/null
+++ b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/events/AasEventDistributer.java
@@ -0,0 +1,36 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events;
+
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events.model.AasEvent;
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public interface AasEventDistributer {
+
+ void distribute(AasEvent event);
+
+}
diff --git a/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/events/AasEventHandler.java b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/events/AasEventHandler.java
new file mode 100644
index 000000000..57d4eb18a
--- /dev/null
+++ b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/events/AasEventHandler.java
@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events;
+
+import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell;
+import org.eclipse.digitaltwin.aas4j.v3.model.AssetInformation;
+import org.eclipse.digitaltwin.aas4j.v3.model.Reference;
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public interface AasEventHandler {
+
+ void onAasCreated(AssetAdministrationShell aas);
+
+ void onAasUpdated(String aasId, AssetAdministrationShell aas);
+
+ void onAasDeleted(String aasId);
+
+ void onSubmodelRefAdded(String aasId, Reference submodelReference);
+
+ void onSubmodelRefDeleted(String aasId, String submodelId);
+
+ void onAssetInformationSet(String aasId, AssetInformation aasInfo);
+
+}
diff --git a/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/events/DistributingAasEventHandler.java b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/events/DistributingAasEventHandler.java
new file mode 100644
index 000000000..b1382368f
--- /dev/null
+++ b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/events/DistributingAasEventHandler.java
@@ -0,0 +1,117 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events;
+
+import java.util.List;
+
+import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell;
+import org.eclipse.digitaltwin.aas4j.v3.model.AssetInformation;
+import org.eclipse.digitaltwin.aas4j.v3.model.Key;
+import org.eclipse.digitaltwin.aas4j.v3.model.Reference;
+import org.eclipse.digitaltwin.aas4j.v3.model.ReferenceTypes;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events.model.AasEvent;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events.model.AasEventType;
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public class DistributingAasEventHandler implements AasEventHandler {
+
+ private final AasEventDistributer evtDistributer;
+
+
+ public DistributingAasEventHandler(AasEventDistributer evtDistributer) {
+ this.evtDistributer = evtDistributer;
+ }
+
+ @Override
+ public void onAasCreated(AssetAdministrationShell aas) {
+ AasEvent event = new AasEvent();
+ event.setType(AasEventType.AAS_CREATED);
+ event.setId(aas.getId());
+ event.setAas(aas);
+ evtDistributer.distribute(event);
+ }
+
+ @Override
+ public void onAasUpdated(String aasId, AssetAdministrationShell aas) {
+ AasEvent event = new AasEvent();
+ event.setType(AasEventType.AAS_UPDATED);
+ event.setId(aasId);
+ event.setAas(aas);
+ evtDistributer.distribute(event);
+ }
+
+ @Override
+ public void onAasDeleted(String aasId) {
+ AasEvent event = new AasEvent();
+ event.setType(AasEventType.AAS_DELETED);
+ event.setId(aasId);
+ evtDistributer.distribute(event);
+ }
+
+ @Override
+ public void onSubmodelRefAdded(String aasId, Reference submodelReference) {
+ AasEvent event = new AasEvent();
+ event.setType(AasEventType.SM_REF_ADDED);
+ event.setId(aasId);
+ event.setSubmodelId(resolveSubmodelId(submodelReference));
+ event.setReference(submodelReference);
+ evtDistributer.distribute(event);
+ }
+
+ @Override
+ public void onSubmodelRefDeleted(String aasId, String submodelId) {
+ AasEvent event = new AasEvent();
+ event.setType(AasEventType.SM_REF_DELETED);
+ event.setId(aasId);
+ event.setSubmodelId(submodelId);
+ evtDistributer.distribute(event);
+ }
+
+ @Override
+ public void onAssetInformationSet(String aasId, AssetInformation aasInfo) {
+ AasEvent event = new AasEvent();
+ event.setType(AasEventType.ASSET_INFORMATION_SET);
+ event.setId(aasId);
+ event.setAssetInformation(aasInfo);
+ evtDistributer.distribute(event);
+ }
+
+ private String resolveSubmodelId(Reference submodelReference) {
+ if (submodelReference == null) {
+ return null;
+ }
+ if (submodelReference.getType() != ReferenceTypes.MODEL_REFERENCE) {
+ return null;
+ }
+ List keys = submodelReference.getKeys();
+ if (keys == null || keys.size() != 1) {
+ return null;
+ }
+ Key key = keys.get(0);
+ return key.getValue();
+ }
+}
diff --git a/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/events/KafkaAasEventDistributer.java b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/events/KafkaAasEventDistributer.java
new file mode 100644
index 000000000..001e421c8
--- /dev/null
+++ b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/events/KafkaAasEventDistributer.java
@@ -0,0 +1,67 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.SerializationException;
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonSerializer;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events.model.AasEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.kafka.core.KafkaTemplate;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public class KafkaAasEventDistributer implements AasEventDistributer {
+
+ private static Logger LOGGER = LoggerFactory.getLogger(KafkaAasEventDistributer.class);
+
+ private final JsonSerializer serializer;
+ private final KafkaTemplate template;
+ private String topicName;
+
+ public KafkaAasEventDistributer(JsonSerializer serializer, KafkaTemplate template, String topicName) {
+ this.serializer = serializer;
+ this.template = template;
+ this.topicName = topicName;
+ }
+
+ @Override
+ public void distribute(AasEvent evt) {
+ try {
+ String payload = serializer.write(evt);
+ LOGGER.debug("Send kafka message to " + topicName + ".");
+ template.send(topicName, evt.getId(), payload).get(3, TimeUnit.SECONDS);
+ } catch (InterruptedException | ExecutionException | TimeoutException | SerializationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/events/model/AasEvent.java b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/events/model/AasEvent.java
new file mode 100644
index 000000000..767a037e2
--- /dev/null
+++ b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/events/model/AasEvent.java
@@ -0,0 +1,117 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events.model;
+
+import java.util.Objects;
+
+import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell;
+import org.eclipse.digitaltwin.aas4j.v3.model.AssetInformation;
+import org.eclipse.digitaltwin.aas4j.v3.model.Reference;
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public class AasEvent {
+
+ private AasEventType type;
+ private String id;
+ private String submodelId;
+ private AssetAdministrationShell aas;
+ private Reference reference;
+ private AssetInformation assetInformation;
+
+ public AasEventType getType() {
+ return type;
+ }
+
+ public void setType(AasEventType type) {
+ this.type = type;
+ }
+
+ public void setAas(AssetAdministrationShell shell) {
+ this.aas = shell;
+ }
+
+ public AssetAdministrationShell getAas() {
+ return aas;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setReference(Reference reference) {
+ this.reference = reference;
+ }
+
+ public Reference getReference() {
+ return reference;
+ }
+
+ public void setSubmodelId(String submodelId) {
+ this.submodelId = submodelId;
+ }
+
+ public String getSubmodelId() {
+ return submodelId;
+ }
+
+ public void setAssetInformation(AssetInformation aasInfo) {
+ this.assetInformation = aasInfo;
+ }
+
+ public AssetInformation getAssetInformation() {
+ return assetInformation;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(aas, assetInformation, id, reference, submodelId, type);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ AasEvent other = (AasEvent) obj;
+ return Objects.equals(aas, other.aas) && Objects.equals(assetInformation, other.assetInformation)
+ && Objects.equals(id, other.id) && Objects.equals(reference, other.reference)
+ && Objects.equals(submodelId, other.submodelId) && type == other.type;
+ }
+
+ @Override
+ public String toString() {
+ return "AasEvent [type=" + type + ", id=" + id + ", submodelId=" + submodelId + ", aas=" + aas + ", reference="
+ + reference + ", assetInformation=" + assetInformation + "]";
+ }
+}
diff --git a/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/events/model/AasEventType.java b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/events/model/AasEventType.java
new file mode 100644
index 000000000..5bd05792e
--- /dev/null
+++ b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/events/model/AasEventType.java
@@ -0,0 +1,34 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events.model;
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public enum AasEventType {
+
+ AAS_CREATED, AAS_UPDATED, AAS_DELETED, SM_REF_ADDED, SM_REF_DELETED, ASSET_INFORMATION_SET;
+
+}
diff --git a/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/AasEventKafkaListener.java b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/AasEventKafkaListener.java
new file mode 100644
index 000000000..e8f7a6252
--- /dev/null
+++ b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/AasEventKafkaListener.java
@@ -0,0 +1,91 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.kafka.common.TopicPartition;
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.DeserializationException;
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonDeserializer;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events.model.AasEvent;
+import org.springframework.kafka.annotation.KafkaHandler;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.kafka.listener.ConsumerSeekAware;
+import org.springframework.stereotype.Component;
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+@KafkaListener(topics = AasEventKafkaListener.TOPIC_NAME, batch = "false", groupId = "kafka-test-aas", autoStartup = "true")
+@Component
+public class AasEventKafkaListener implements ConsumerSeekAware {
+
+ public static final String TOPIC_NAME = "aas-events";
+
+ private final LinkedBlockingDeque evt = new LinkedBlockingDeque();
+ private final JsonDeserializer deserializer;
+ private final CountDownLatch latch = new CountDownLatch(1);
+
+ public AasEventKafkaListener(JsonDeserializer deserializer) {
+ this.deserializer = deserializer;
+ }
+
+ @KafkaHandler
+ public void receiveMessage(String content) {
+ try {
+ AasEvent event = deserializer.read(content, AasEvent.class);
+ evt.offerFirst(event);
+ } catch (DeserializationException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public AasEvent next(int value, TimeUnit unit) throws InterruptedException {
+ return evt.pollLast(value, unit);
+ }
+
+ public AasEvent next() throws InterruptedException {
+ return next(1, TimeUnit.MINUTES);
+ }
+
+ @Override
+ public void onPartitionsAssigned(Map assignments, ConsumerSeekCallback callback) {
+ for (TopicPartition eachPartition : assignments.keySet()) {
+ if (TOPIC_NAME.equals(eachPartition.topic())) {
+ latch.countDown();
+ }
+ }
+ }
+
+ public void awaitTopicAssignment() throws InterruptedException {
+ if (!latch.await(1, TimeUnit.MINUTES)) {
+ throw new RuntimeException("Timeout occured while waiting for partition assignment. Is kafka running?");
+ }
+ }
+}
diff --git a/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/KafkaEventsInMemoryStorageIntegrationTest.java b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/KafkaEventsInMemoryStorageIntegrationTest.java
new file mode 100644
index 000000000..a48ad29d6
--- /dev/null
+++ b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/KafkaEventsInMemoryStorageIntegrationTest.java
@@ -0,0 +1,249 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka;
+
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell;
+import org.eclipse.digitaltwin.aas4j.v3.model.AssetInformation;
+import org.eclipse.digitaltwin.aas4j.v3.model.AssetKind;
+import org.eclipse.digitaltwin.aas4j.v3.model.KeyTypes;
+import org.eclipse.digitaltwin.aas4j.v3.model.Reference;
+import org.eclipse.digitaltwin.aas4j.v3.model.ReferenceTypes;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultAssetInformation;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultKey;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultReference;
+import org.eclipse.digitaltwin.basyx.aasrepository.AasRepository;
+import org.eclipse.digitaltwin.basyx.aasrepository.backend.CrudAasRepositoryFactory;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events.model.AasEvent;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events.model.AasEventType;
+import org.eclipse.digitaltwin.basyx.aasservice.AasServiceFactory;
+import org.eclipse.digitaltwin.basyx.aasservice.backend.AasBackend;
+import org.eclipse.digitaltwin.basyx.aasservice.backend.CrudAasServiceFactory;
+import org.eclipse.digitaltwin.basyx.aasservice.backend.InMemoryAasBackend;
+import org.eclipse.digitaltwin.basyx.core.filerepository.FileRepository;
+import org.eclipse.digitaltwin.basyx.core.filerepository.InMemoryFileRepository;
+import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.annotation.DirtiesContext.ClassMode;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit4.SpringRunner;
+
+/**
+ * @author sonnenberg (DFKI GmbH)
+ */
+@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
+@ComponentScan(basePackages = { "org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka"})
+@ContextConfiguration(classes = {TestApplication.class})
+@RunWith(SpringRunner.class)
+@TestPropertySource(properties = { KafkaAasRepositoryFeature.FEATURENAME + ".enabled=true",
+ "spring.kafka.bootstrap-servers=PLAINTEXT_HOST://localhost:9092",
+ KafkaAasRepositoryFeature.FEATURENAME + ".topic.name=aas-events"
+
+})
+public class KafkaEventsInMemoryStorageIntegrationTest {
+
+ @Autowired
+ private AasEventKafkaListener listener;
+
+ @Autowired
+ private KafkaAasRepositoryFeature feature;
+
+ private AasRepository repo;
+
+ @Before
+ public void awaitAssignment() throws InterruptedException {
+ listener.awaitTopicAssignment();
+ FileRepository fileRepo = new InMemoryFileRepository();
+ AasBackend aasRepositoryBackend = new InMemoryAasBackend();
+ AasServiceFactory sf = new CrudAasServiceFactory(aasRepositoryBackend, fileRepo);
+ CrudAasRepositoryFactory factory = new CrudAasRepositoryFactory(aasRepositoryBackend, sf, "test");
+ repo = feature.decorate(factory).create();
+
+ cleanup();
+ }
+
+
+ @Test
+ public void testCreateAas() throws InterruptedException {
+ AssetAdministrationShell shell = TestShells.shell();
+ repo.createAas(shell);
+
+ AasEvent evt = listener.next();
+ Assert.assertEquals(AasEventType.AAS_CREATED, evt.getType());
+ Assert.assertEquals(TestShells.ID_AAS, evt.getId());
+ Assert.assertNull(evt.getSubmodelId());
+ Assert.assertNull(evt.getAssetInformation());
+ Assert.assertNull(evt.getReference());
+ Assert.assertEquals(shell, evt.getAas());
+ }
+
+ @Test
+ public void testUpdateAas() throws InterruptedException {
+ AssetAdministrationShell shell = TestShells.shell();
+ repo.createAas(shell);
+
+ AasEvent evtCreated = listener.next();
+ Assert.assertEquals(AasEventType.AAS_CREATED, evtCreated.getType());
+ Assert.assertEquals(shell, evtCreated.getAas());
+
+ AssetAdministrationShell newShell = TestShells.shell();
+ newShell.setIdShort("newIdShort");
+ repo.updateAas(newShell.getId(), newShell);
+
+ AasEvent evtUpdated = listener.next();
+ Assert.assertEquals(AasEventType.AAS_UPDATED, evtUpdated.getType());
+
+ Assert.assertEquals(TestShells.ID_AAS, evtUpdated.getId());
+ Assert.assertNull(evtUpdated.getSubmodelId());
+ Assert.assertNull(evtUpdated.getAssetInformation());
+ Assert.assertNull(evtUpdated.getReference());
+ Assert.assertEquals(newShell, evtUpdated.getAas());
+ }
+
+ @Test
+ public void testDelete() throws InterruptedException {
+ AssetAdministrationShell shell = TestShells.shell();
+ repo.createAas(shell);
+
+ AasEvent evtCreated = listener.next();
+ Assert.assertEquals(AasEventType.AAS_CREATED, evtCreated.getType());
+ Assert.assertEquals(shell, evtCreated.getAas());
+
+ repo.deleteAas(shell.getId());
+
+ AasEvent evtDeleted = listener.next();
+ Assert.assertEquals(AasEventType.AAS_DELETED, evtDeleted.getType());
+
+ Assert.assertEquals(TestShells.ID_AAS, evtDeleted.getId());
+ Assert.assertNull(evtDeleted.getSubmodelId());
+ Assert.assertNull(evtDeleted.getAssetInformation());
+ Assert.assertNull(evtDeleted.getReference());
+ Assert.assertNull(evtDeleted.getAas());
+ }
+
+ @Test
+ public void testAssetInformation() throws InterruptedException {
+ AssetAdministrationShell shell = TestShells.shell();
+ repo.createAas(shell);
+
+ AasEvent evtCreated = listener.next();
+ Assert.assertEquals(AasEventType.AAS_CREATED, evtCreated.getType());
+ Assert.assertEquals(shell, evtCreated.getAas());
+
+ AssetInformation assetInfo = new DefaultAssetInformation.Builder().assetKind(AssetKind.TYPE).assetType("robot").globalAssetId("aas:robot:id").build();
+ repo.setAssetInformation(shell.getId(), assetInfo);
+
+ AasEvent evtAasIdSet = listener.next();
+ Assert.assertEquals(AasEventType.ASSET_INFORMATION_SET, evtAasIdSet.getType());
+
+ Assert.assertEquals(TestShells.ID_AAS, evtAasIdSet.getId());
+ Assert.assertNull(evtAasIdSet.getSubmodelId());
+ Assert.assertEquals(assetInfo, evtAasIdSet.getAssetInformation());
+ Assert.assertNull(evtAasIdSet.getReference());
+ Assert.assertNull(evtAasIdSet.getAas());
+ }
+
+ @Test
+ public void testSubmodelReferenceAdded() throws InterruptedException {
+ AssetAdministrationShell shell = TestShells.shell();
+ repo.createAas(shell);
+
+ AasEvent evtCreated = listener.next();
+ Assert.assertEquals(AasEventType.AAS_CREATED, evtCreated.getType());
+ Assert.assertEquals(shell, evtCreated.getAas());
+
+ String smId = "http://sm.id/1";
+ Reference ref = new DefaultReference.Builder().type(ReferenceTypes.MODEL_REFERENCE).keys(new DefaultKey.Builder().type(KeyTypes.SUBMODEL).value(smId).build()).build();
+ repo.addSubmodelReference(TestShells.ID_AAS, ref);
+
+ AasEvent evtRefAdded = listener.next();
+ Assert.assertEquals(AasEventType.SM_REF_ADDED, evtRefAdded.getType());
+
+ Assert.assertEquals(TestShells.ID_AAS, evtRefAdded.getId());
+ Assert.assertEquals(smId, evtRefAdded.getSubmodelId());
+ Assert.assertNull(evtRefAdded.getAssetInformation());
+ Assert.assertEquals(ref , evtRefAdded.getReference());
+ Assert.assertNull(evtRefAdded.getAas());
+ }
+
+ @Test
+ public void testSubmodelReferenceRemoved() throws InterruptedException {
+ AssetAdministrationShell shell = TestShells.shell();
+ repo.createAas(shell);
+
+ AasEvent evtCreated = listener.next();
+ Assert.assertEquals(AasEventType.AAS_CREATED, evtCreated.getType());
+ Assert.assertEquals(shell, evtCreated.getAas());
+
+ repo.removeSubmodelReference(TestShells.ID_AAS, TestShells.ID_SM);
+ AasEvent evtRefRemoved = listener.next();
+ Assert.assertEquals(AasEventType.SM_REF_DELETED, evtRefRemoved.getType());
+
+ Assert.assertEquals(TestShells.ID_AAS, evtRefRemoved.getId());
+ Assert.assertEquals(TestShells.ID_SM, evtRefRemoved.getSubmodelId());
+ Assert.assertNull(evtRefRemoved.getAssetInformation());
+ Assert.assertNull(evtRefRemoved.getReference());
+ Assert.assertNull(evtRefRemoved.getAas());
+ }
+
+ @Test
+ public void testGetterAreWorking() throws InterruptedException {
+ AssetAdministrationShell shell = TestShells.shell();
+ repo.createAas(shell);
+ AasEvent evtCreated = listener.next();
+ Assert.assertEquals(AasEventType.AAS_CREATED, evtCreated.getType());
+
+ Assert.assertEquals(1, repo.getSubmodelReferences(TestShells.ID_AAS, new PaginationInfo(null, null)).getResult().size());
+ Assert.assertEquals(shell, repo.getAas(TestShells.ID_AAS));
+ Assert.assertEquals(1, repo.getAllAas(new PaginationInfo(null, null)).getResult().size());
+ Assert.assertEquals(shell.getAssetInformation(), repo.getAssetInformation(TestShells.ID_AAS));
+ }
+
+ @Test
+ public void testFeatureIsEnabled() {
+ Assert.assertTrue(feature.isEnabled());
+ }
+
+ @After
+ public void cleanup() throws InterruptedException {
+ for (AssetAdministrationShell aas : repo.getAllAas(new PaginationInfo(null, null)).getResult()) {
+ repo.deleteAas(aas.getId());
+ }
+ while(listener.next(100, TimeUnit.MICROSECONDS) != null);
+ }
+
+}
diff --git a/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/TestApplication.java b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/TestApplication.java
new file mode 100644
index 000000000..27d1d91f9
--- /dev/null
+++ b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/TestApplication.java
@@ -0,0 +1,41 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka;
+
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonDeserializer;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+@SpringBootApplication
+public class TestApplication {
+
+ @Bean
+ public JsonDeserializer getDeserializer() {
+ return new JsonDeserializer();
+ }
+}
diff --git a/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/TestShells.java b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/TestShells.java
new file mode 100644
index 000000000..ba040fc5a
--- /dev/null
+++ b/basyx.aasrepository/basyx.aasrepository-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/kafka/TestShells.java
@@ -0,0 +1,55 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka;
+
+import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell;
+import org.eclipse.digitaltwin.aas4j.v3.model.KeyTypes;
+import org.eclipse.digitaltwin.aas4j.v3.model.ReferenceTypes;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultAssetAdministrationShell;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultKey;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultReference;
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public class TestShells {
+
+ public static final String ID_AAS = "http://aas.id/0";
+ public static final String IDSHORT_AAS = "aas_0";
+ public static final String ID_SM = "http://sm.id/0";
+
+ private TestShells() {
+
+ }
+
+ public static AssetAdministrationShell shell() {
+ return new DefaultAssetAdministrationShell.Builder().id(ID_AAS).idShort(IDSHORT_AAS)
+ .submodels(new DefaultReference.Builder().type(ReferenceTypes.MODEL_REFERENCE)
+ .keys(new DefaultKey.Builder().type(KeyTypes.SUBMODEL).value(ID_SM).build()).build())
+ .build();
+
+ }
+
+}
diff --git a/basyx.aasrepository/basyx.aasrepository.component/pom.xml b/basyx.aasrepository/basyx.aasrepository.component/pom.xml
index 7a8d74fed..6a6b5f10f 100644
--- a/basyx.aasrepository/basyx.aasrepository.component/pom.xml
+++ b/basyx.aasrepository/basyx.aasrepository.component/pom.xml
@@ -59,6 +59,16 @@
org.eclipse.digitaltwin.basyx
basyx.aasrepository-feature-mqtt
+
+ org.eclipse.digitaltwin.basyx
+ basyx.aasrepository-feature-kafka
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.aasrepository-feature-kafka
+ tests
+ test
+
org.eclipse.digitaltwin.basyx
basyx.aasrepository-feature-registry-integration
diff --git a/basyx.aasrepository/basyx.aasrepository.component/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/component/KafkaFeatureEnabledSmokeTest.java b/basyx.aasrepository/basyx.aasrepository.component/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/component/KafkaFeatureEnabledSmokeTest.java
new file mode 100644
index 000000000..77b5f1297
--- /dev/null
+++ b/basyx.aasrepository/basyx.aasrepository.component/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/component/KafkaFeatureEnabledSmokeTest.java
@@ -0,0 +1,122 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.aasrepository.component;
+
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.SerializationException;
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonSerializer;
+import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell;
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultAssetAdministrationShell;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.KafkaAasRepositoryFeature;
+import org.eclipse.digitaltwin.basyx.aasrepository.AasRepository;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.AasEventKafkaListener;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events.model.AasEvent;
+import org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka.events.model.AasEventType;
+import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.annotation.DirtiesContext.ClassMode;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit4.SpringRunner;
+
+/**
+ * @author sonnenberg (DFKI GmbH)
+ */
+@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@ComponentScan(basePackages = { "org.eclipse.digitaltwin.basyx.aasrepository.feature.kafka" })
+@RunWith(SpringRunner.class)
+@TestPropertySource(properties = { "basyx.feature.kafka.enabled=true",
+ "spring.kafka.bootstrap-servers=PLAINTEXT_HOST://localhost:9092",
+ KafkaAasRepositoryFeature.FEATURENAME + "kafka.enabled=true",
+ KafkaAasRepositoryFeature.FEATURENAME + ".topic.name=" + AasEventKafkaListener.TOPIC_NAME })
+public class KafkaFeatureEnabledSmokeTest {
+
+ @LocalServerPort
+ private int port;
+
+ @Autowired
+ private TestRestTemplate restTemplate;
+
+ @Autowired
+ private JsonSerializer serializer;
+
+ @Autowired
+ private AasEventKafkaListener listener;
+
+ @Autowired
+ private AasRepository aasRepo;
+
+ @Before
+ public void provideAas() throws InterruptedException {
+ listener.awaitTopicAssignment();
+ cleanup();
+ }
+
+ @After
+ public void cleanup() throws InterruptedException {
+ for (AssetAdministrationShell aas : aasRepo.getAllAas(new PaginationInfo(null, null)).getResult()) {
+ aasRepo.deleteAas(aas.getId());
+ }
+ while(listener.next(100, TimeUnit.MICROSECONDS) != null);
+ }
+
+ @Test
+ public void testAasCreatedEvent() throws InterruptedException, SerializationException {
+ AssetAdministrationShell shell = new DefaultAssetAdministrationShell.Builder().id("http://aas.id/1")
+ .idShort("1").build();
+ HttpEntity entity = createHttpEntity(shell);
+ restTemplate.exchange(createEndpointUrl(), HttpMethod.POST, entity, String.class);
+ AasEvent event = listener.next();
+ Assert.assertEquals(AasEventType.AAS_CREATED, event.getType());
+ Assert.assertEquals(shell.getId(), event.getId());
+ Assert.assertEquals(shell, event.getAas());
+ }
+
+ private String createEndpointUrl() {
+ return "http://localhost:" + port + "/shells";
+ }
+
+ private HttpEntity createHttpEntity(AssetAdministrationShell shell) throws SerializationException {
+ HttpHeaders headers = new HttpHeaders();
+ headers.set(HttpHeaders.CONTENT_TYPE, "application/json");
+ return new HttpEntity<>(serializer.write(shell), headers);
+ }
+}
diff --git a/basyx.aasrepository/pom.xml b/basyx.aasrepository/pom.xml
index aeb480c2a..ae8f067e6 100644
--- a/basyx.aasrepository/pom.xml
+++ b/basyx.aasrepository/pom.xml
@@ -20,6 +20,7 @@
basyx.aasrepository-backend-mongodb
basyx.aasrepository-feature-aasxupload
basyx.aasrepository-feature-mqtt
+ basyx.aasrepository-feature-kafka
basyx.aasrepository-feature-registry-integration
basyx.aasrepository-feature-authorization
basyx.aasrepository-tck
diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/integration/BaseIntegrationTest.java b/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/integration/BaseIntegrationTest.java
index fc25049b6..0aed1e675 100644
--- a/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/integration/BaseIntegrationTest.java
+++ b/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/integration/BaseIntegrationTest.java
@@ -114,18 +114,24 @@ public abstract class BaseIntegrationTest {
public TestResourcesLoader resourceLoader = new TestResourcesLoader(BaseIntegrationTest.class.getPackageName(), mapper);
protected SubmodelRegistryApi api;
-
+
@Before
- public void initClient() throws ApiException {
- api = new SubmodelRegistryApi("http", "127.0.0.1", port);
- // there should be no descriptor
- api.deleteAllSubmodelDescriptors();
- queue().assertNoAdditionalMessage();
+ public void setUp() throws ApiException, InterruptedException {
+ initClient();
+ cleanup();
}
-
+
@After
- public void cleanup() throws ApiException {
- queue().assertNoAdditionalMessage();
+ public void tearDown() throws ApiException, InterruptedException {
+ cleanup();
+ }
+
+ protected void initClient() throws ApiException, InterruptedException {
+ api = new SubmodelRegistryApi("http", "127.0.0.1", port);
+ }
+
+ protected void cleanup() throws ApiException, InterruptedException {
+ queue().noAdditionalMessage();
GetSubmodelDescriptorsResult result = api.getAllSubmodelDescriptors(null, null);
for (SubmodelDescriptor eachDescriptor : result.getResult()) {
api.deleteSubmodelDescriptorById(eachDescriptor.getId());
@@ -161,7 +167,7 @@ private void postSubmodel(int id) {
}
@Test
- public void whenDeleteAll_thenAllDescriptorsAreRemoved() throws ApiException {
+ public void whenDeleteAll_thenAllDescriptorsAreRemoved() throws ApiException, InterruptedException {
for (int i = 0; i < DELETE_ALL_TEST_INSTANCE_COUNT; i++) {
SubmodelDescriptor descr = new SubmodelDescriptor();
String id = "id_" + i;
@@ -189,7 +195,7 @@ public void whenDeleteAll_thenAllDescriptorsAreRemoved() throws ApiException {
for (int i = 0; i < DELETE_ALL_TEST_INSTANCE_COUNT; i++) {
assertThat(events.remove(RegistryEvent.builder().id("id_" + i).type(EventType.SUBMODEL_UNREGISTERED).build())).isTrue();
}
- queue().assertNoAdditionalMessage();
+ queue().noAdditionalMessage();
}
@Test
@@ -205,7 +211,7 @@ public void whenCreateAndDeleteDescriptors_thenAllDescriptorsAreRemoved() throws
all = api.getAllSubmodelDescriptors(null, null).getResult();
assertThat(all).isEmpty();
- queue().assertNoAdditionalMessage();
+ queue().noAdditionalMessage();
}
@Test
diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/integration/EventQueue.java b/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/integration/EventQueue.java
index c76f20a2d..4ff9a3cd7 100644
--- a/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/integration/EventQueue.java
+++ b/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/integration/EventQueue.java
@@ -55,16 +55,8 @@ public void reset() {
}
}
- public void assertNoAdditionalMessage() {
- try {
- String message = messageQueue.poll(1, TimeUnit.SECONDS);
- if (message != null) {
- throw new EventListenerException("Got additional message: " + message);
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new EventListenerException(e);
- }
+ public void noAdditionalMessage() throws InterruptedException {
+ while(messageQueue.poll(100, TimeUnit.MILLISECONDS) != null);
}
public RegistryEvent poll() {
diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/memory/KafkaEventsInMemoryStorageIntegrationTest.java b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/memory/KafkaEventsInMemoryStorageIntegrationTest.java
index 70d99f08a..afb353d0b 100644
--- a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/memory/KafkaEventsInMemoryStorageIntegrationTest.java
+++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/memory/KafkaEventsInMemoryStorageIntegrationTest.java
@@ -30,11 +30,10 @@
import java.util.concurrent.TimeUnit;
import org.apache.kafka.common.TopicPartition;
+import org.eclipse.digitaltwin.basyx.submodelregistry.client.ApiException;
import org.eclipse.digitaltwin.basyx.submodelregistry.service.tests.integration.BaseIntegrationTest;
import org.eclipse.digitaltwin.basyx.submodelregistry.service.tests.integration.EventQueue;
-import org.junit.Before;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
import org.springframework.kafka.annotation.KafkaHandler;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.listener.ConsumerSeekAware;
@@ -53,11 +52,13 @@ public class KafkaEventsInMemoryStorageIntegrationTest extends BaseIntegrationTe
@Autowired
private RegistrationEventKafkaListener listener;
- @Before
- public void awaitAssignment() throws InterruptedException {
+ @Override
+ public void setUp() throws ApiException, InterruptedException {
listener.awaitTopicAssignment();
+ super.setUp();
}
+
@Override
public EventQueue queue() {
return listener.queue;
@@ -67,12 +68,9 @@ public EventQueue queue() {
@Component
private static class RegistrationEventKafkaListener implements ConsumerSeekAware {
-
private final EventQueue queue;
private final CountDownLatch latch = new CountDownLatch(1);
- @Value("${spring.kafka.template.default-topic}")
- private String topicName;
@SuppressWarnings("unused")
public RegistrationEventKafkaListener(ObjectMapper mapper) {
@@ -88,8 +86,7 @@ public void receiveMessage(String content) {
public void onPartitionsAssigned(Map assignments,
ConsumerSeekCallback callback) {
for (TopicPartition eachPartition : assignments.keySet()) {
- if (topicName.equals(eachPartition.topic())) {
- callback.seekToEnd(List.of(eachPartition));
+ if ("submodel-registry".equals(eachPartition.topic())) {
latch.countDown();
}
}
diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/AuthorizedClientTest.java b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/AuthorizedClientTest.java
index a904ca815..ddacea0bf 100644
--- a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/AuthorizedClientTest.java
+++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/AuthorizedClientTest.java
@@ -62,22 +62,20 @@ public class AuthorizedClientTest extends BaseIntegrationTest {
@Value("${local.server.port}")
private int port;
- @Before
- public void awaitAssignment() throws InterruptedException {
+ @Override
+ public void setUp() throws ApiException, InterruptedException {
listener.awaitTopicAssignment();
+ super.setUp();
}
-
+
@Override
public EventQueue queue() {
return listener.getQueue();
}
- @Before
@Override
- public void initClient() throws ApiException {
+ public void initClient() throws ApiException, InterruptedException {
api = new AuthorizedConnectedSubmodelRegistry("http://127.0.0.1:" + port, new TokenManager("http://localhost:9096/realms/BaSyx/protocol/openid-connect/token", new ClientCredentialAccessTokenProvider(new ClientCredential("workstation-1", "nY0mjyECF60DGzNmQUjL81XurSl8etom"))));
- api.deleteAllSubmodelDescriptors();
- queue().assertNoAdditionalMessage();
}
@Test
diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/KafkaEventsMongoDbStorageIntegrationTest.java b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/KafkaEventsMongoDbStorageIntegrationTest.java
index 0bb275662..21bd7df90 100644
--- a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/KafkaEventsMongoDbStorageIntegrationTest.java
+++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/KafkaEventsMongoDbStorageIntegrationTest.java
@@ -30,6 +30,7 @@
import java.util.concurrent.TimeUnit;
import org.apache.kafka.common.TopicPartition;
+import org.eclipse.digitaltwin.basyx.submodelregistry.client.ApiException;
import org.eclipse.digitaltwin.basyx.submodelregistry.service.tests.integration.BaseIntegrationTest;
import org.eclipse.digitaltwin.basyx.submodelregistry.service.tests.integration.EventQueue;
import org.junit.Before;
@@ -52,9 +53,10 @@ public class KafkaEventsMongoDbStorageIntegrationTest extends BaseIntegrationTes
@Autowired
private RegistrationEventKafkaListener listener;
- @Before
- public void awaitAssignment() throws InterruptedException {
+ @Override
+ public void setUp() throws ApiException, InterruptedException {
listener.awaitTopicAssignment();
+ super.setUp();
}
@Override
@@ -72,7 +74,6 @@ public static class RegistrationEventKafkaListener implements ConsumerSeekAware
@Value("${spring.kafka.template.default-topic}")
private String topicName;
- @SuppressWarnings("unused")
public RegistrationEventKafkaListener(ObjectMapper mapper) {
this.queue = new EventQueue(mapper);
}
@@ -91,7 +92,6 @@ public void onPartitionsAssigned(Map assignments,
ConsumerSeekCallback callback) {
for (TopicPartition eachPartition : assignments.keySet()) {
if (topicName.equals(eachPartition.topic())) {
- callback.seekToEnd(List.of(eachPartition));
latch.countDown();
}
}
diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/README.md b/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/README.md
new file mode 100644
index 000000000..47cb4075b
--- /dev/null
+++ b/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/README.md
@@ -0,0 +1,44 @@
+# Submodel Repository - KAFKA Eventing
+
+This feature provides KAFKA eventing. Messages are sent whenever a resource in the submodel repository is created, modified, or deleted.
+
+It is essential to maintain the insertion order of events per submodel. Events for creating, updating, and deleting must not overtake one another. For this reason, only one topic is used, and all messages related to a submodel are stored in a single partition, as the submodel ID is used as the message key. Only within a partition is the order guaranteed when consuming messages.
+
+## Feature Configuration
+
+The feature is configured using the following Spring properties:
+
+| Property | Default | Description |
+|---------------------------------------------------|----------------|--------------------------------------------------------------------------------------------|
+| basyx.submodelrepository.feature.kafka.enabled | false | Specifies whether the feature is enabled |
+| basyx.feature.kafka.enabled | false | Specifies whether the feature is enabled (for both aas-repository and submodel-repository) |
+| basyx.submodelrepository.feature.kafka.topic.name | submodel-events aas-events for [submodel component](../basyx.submodelservice.component/), otherwise no default | The name of the topic where events are sent |
+| spring.kafka.bootstrap-servers | - | The address of the Kafka brokers, e.g., `PLAINTEXT_HOST://localhost:9092` |
+
+## Message Structure
+
+The messages are transmitted as strings in JSON format:
+
+```json
+{
+ "type": "SM_CREATED",
+ "id": "http://sm.ids.org/1",
+ "submodel": {
+ "modelType": "Submodel",
+ "id": "http://sm.ids.org/1",
+ "idShort": "1"
+ },
+ "smElement": null,
+ "smElementPath": null
+}
+```
+
+Depending on the event type, the fields of the JSON message are populated. The following event types are available:
+
+
+* SM_CREATED
+* SM_UPDATED
+* SM_DELETED
+* SME_UPDATED
+* SME_CREATED
+* SME_DELETED
\ No newline at end of file
diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/pom.xml b/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/pom.xml
new file mode 100644
index 000000000..6d2f8cd1a
--- /dev/null
+++ b/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/pom.xml
@@ -0,0 +1,67 @@
+
+ 4.0.0
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelrepository
+ ${revision}
+
+ basyx.submodelrepository-feature-kafka
+ BaSyx submodelrepository-feature-kafka
+ BaSyx submodelrepository-feature-kafka
+
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelrepository-core
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelrepository-core
+ tests
+ test
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelrepository-backend-inmemory
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelservice-backend-inmemory
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelservice-feature-kafka
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelservice-feature-kafka
+ tests
+ test
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+ org.junit.vintage
+ junit-vintage-engine
+ test
+
+
+ org.hamcrest
+ hamcrest-core
+
+
+
+
+
\ No newline at end of file
diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/kafka/KafkaSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/kafka/KafkaSubmodelRepository.java
new file mode 100644
index 000000000..35ad4aee6
--- /dev/null
+++ b/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/kafka/KafkaSubmodelRepository.java
@@ -0,0 +1,188 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelrepository.feature.kafka;
+
+import java.io.InputStream;
+import java.util.List;
+
+import org.eclipse.digitaltwin.aas4j.v3.model.OperationVariable;
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
+import org.eclipse.digitaltwin.basyx.core.exceptions.CollidingIdentifierException;
+import org.eclipse.digitaltwin.basyx.core.exceptions.ElementDoesNotExistException;
+import org.eclipse.digitaltwin.basyx.core.pagination.CursorResult;
+import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo;
+import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.SubmodelEventHandler;
+import org.eclipse.digitaltwin.basyx.submodelservice.value.SubmodelElementValue;
+import org.eclipse.digitaltwin.basyx.submodelservice.value.SubmodelValueOnly;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public class KafkaSubmodelRepository implements SubmodelRepository {
+
+ private final SubmodelRepository decorated;
+ private final SubmodelEventHandler eventHandler;
+
+ public KafkaSubmodelRepository(SubmodelRepository decorated, SubmodelEventHandler eventHandler) {
+ this.decorated = decorated;
+ this.eventHandler = eventHandler;
+ }
+
+ @Override
+ public CursorResult> getAllSubmodels(PaginationInfo pInfo) {
+ return decorated.getAllSubmodels(pInfo);
+ }
+
+ @Override
+ public Submodel getSubmodel(String submodelId) throws ElementDoesNotExistException {
+ return decorated.getSubmodel(submodelId);
+ }
+
+ @Override
+ public void updateSubmodel(String submodelId, Submodel submodel) throws ElementDoesNotExistException {
+ decorated.updateSubmodel(submodelId, submodel);
+ eventHandler.onSubmodelUpdated(submodel);
+ }
+
+ @Override
+ public void createSubmodel(Submodel submodel) throws CollidingIdentifierException {
+ decorated.createSubmodel(submodel);
+ eventHandler.onSubmodelCreated(submodel);
+ }
+
+ @Override
+ public void deleteSubmodel(String submodelId) throws ElementDoesNotExistException {
+ decorated.deleteSubmodel(submodelId);
+ eventHandler.onSubmodelDeleted(submodelId);
+ }
+
+ @Override
+ public CursorResult> getSubmodelElements(String submodelId, PaginationInfo pInfo)
+ throws ElementDoesNotExistException {
+ return decorated.getSubmodelElements(submodelId, pInfo);
+ }
+
+ @Override
+ public SubmodelElement getSubmodelElement(String submodelId, String smeIdShort)
+ throws ElementDoesNotExistException {
+ return decorated.getSubmodelElement(submodelId, smeIdShort);
+ }
+
+ @Override
+ public SubmodelElementValue getSubmodelElementValue(String submodelId, String smeIdShort)
+ throws ElementDoesNotExistException {
+ return decorated.getSubmodelElementValue(submodelId, smeIdShort);
+ }
+
+ @Override
+ public void setSubmodelElementValue(String submodelId, String idShortPath, SubmodelElementValue value)
+ throws ElementDoesNotExistException {
+ decorated.setSubmodelElementValue(submodelId, idShortPath, value);
+ SubmodelElement submodelElement = decorated.getSubmodelElement(submodelId, idShortPath);
+ eventHandler.onSubmodelElementUpdated(submodelElement, submodelId, idShortPath);
+ }
+
+ @Override
+ public void createSubmodelElement(String submodelId, SubmodelElement submodelElement) {
+ decorated.createSubmodelElement(submodelId, submodelElement);
+ eventHandler.onSubmodelElementCreated(submodelElement, submodelId, submodelElement.getIdShort());
+ }
+
+ @Override
+ public void createSubmodelElement(String submodelId, String idShortPath, SubmodelElement submodelElement)
+ throws ElementDoesNotExistException {
+ decorated.createSubmodelElement(submodelId, idShortPath, submodelElement);
+ eventHandler.onSubmodelElementCreated(submodelElement, submodelId, idShortPath);
+ }
+
+ @Override
+ public void updateSubmodelElement(String submodelIdentifier, String idShortPath, SubmodelElement submodelElement)
+ throws ElementDoesNotExistException {
+ decorated.updateSubmodelElement(submodelIdentifier, idShortPath, submodelElement);
+ eventHandler.onSubmodelElementUpdated(submodelElement, submodelIdentifier, idShortPath);
+ }
+
+ @Override
+ public void deleteSubmodelElement(String submodelId, String idShortPath) throws ElementDoesNotExistException {
+ decorated.deleteSubmodelElement(submodelId, idShortPath);
+ eventHandler.onSubmodelElementDeleted(submodelId, idShortPath);
+ }
+
+ @Override
+ public String getName() {
+ return decorated.getName();
+ }
+
+ @Override
+ public SubmodelValueOnly getSubmodelByIdValueOnly(String submodelId) {
+ return decorated.getSubmodelByIdValueOnly(submodelId);
+ }
+
+ @Override
+ public Submodel getSubmodelByIdMetadata(String submodelId) {
+ return decorated.getSubmodelByIdMetadata(submodelId);
+ }
+
+ @Override
+ public OperationVariable[] invokeOperation(String submodelId, String idShortPath, OperationVariable[] input)
+ throws ElementDoesNotExistException {
+ return decorated.invokeOperation(submodelId, idShortPath, input);
+ }
+
+ @Override
+ public java.io.File getFileByPathSubmodel(String submodelId, String idShortPath) {
+ return decorated.getFileByPathSubmodel(submodelId, idShortPath);
+ }
+
+ @Override
+ public void deleteFileValue(String identifier, String idShortPath) {
+ decorated.deleteFileValue(identifier, idShortPath);
+ }
+
+ @Override
+ public void setFileValue(String submodelId, String idShortPath, String fileName, InputStream inputStream) {
+ decorated.setFileValue(submodelId, idShortPath, fileName, inputStream);
+ }
+
+ @Override
+ public void patchSubmodelElements(String submodelId, List submodelElementList) {
+ decorated.patchSubmodelElements(submodelId, submodelElementList);
+ Submodel submodel = getSubmodel(submodelId);
+ eventHandler.onSubmodelUpdated(submodel);
+ }
+
+ @Override
+ public InputStream getFileByFilePath(String submodelId, String filePath) {
+ return decorated.getFileByFilePath(submodelId, filePath);
+ }
+
+ @Override
+ public CursorResult> getAllSubmodels(String semanticId, PaginationInfo pInfo) {
+ return decorated.getAllSubmodels(semanticId, pInfo);
+ }
+}
diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/kafka/KafkaSubmodelRepositoryConfiguration.java b/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/kafka/KafkaSubmodelRepositoryConfiguration.java
new file mode 100644
index 000000000..04515aee3
--- /dev/null
+++ b/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/kafka/KafkaSubmodelRepositoryConfiguration.java
@@ -0,0 +1,74 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelrepository.feature.kafka;
+
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonSerializer;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.KafkaSubmodelServiceConfiguration;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.DistributingSubmodelEventHandler;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.SubmodelEventDistributer;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.SubmodelEventHandler;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.DataPreservationLevel;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.core.KafkaTemplate;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+@ConditionalOnExpression(KafkaSubmodelRepositoryFeature.FEATURE_ENABLED_EXPRESSION)
+@Configuration
+public class KafkaSubmodelRepositoryConfiguration {
+
+ @ConditionalOnMissingBean
+ @Bean
+ public JsonSerializer aas4jSerializer() {
+ return new JsonSerializer();
+ }
+
+ @ConditionalOnMissingBean
+ @Bean
+ public DataPreservationLevel submodelPreservationLevel(
+ @Value("${" + KafkaSubmodelRepositoryFeature.FEATURENAME + ".preservationlevel:RETAIN_FULL}") String level) {
+ return DataPreservationLevel.valueOf(level);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public SubmodelEventDistributer submodelEventDistributer(DataPreservationLevel level, JsonSerializer serializer,
+ KafkaTemplate template,
+ @Value("${" + KafkaSubmodelRepositoryFeature.FEATURENAME + ".topic.name:submodel-events}") String topicName) {
+ return new KafkaSubmodelServiceConfiguration().eventDistributer(level, serializer, template, topicName);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public SubmodelEventHandler submodelEventHandler(SubmodelEventDistributer distributer) {
+ return new DistributingSubmodelEventHandler(distributer);
+ }
+}
diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/kafka/KafkaSubmodelRepositoryFactory.java b/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/kafka/KafkaSubmodelRepositoryFactory.java
new file mode 100644
index 000000000..f7a3303d7
--- /dev/null
+++ b/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/kafka/KafkaSubmodelRepositoryFactory.java
@@ -0,0 +1,50 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelrepository.feature.kafka;
+
+import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository;
+import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepositoryFactory;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.SubmodelEventHandler;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public class KafkaSubmodelRepositoryFactory implements SubmodelRepositoryFactory {
+
+ private final SubmodelRepositoryFactory decorated;
+ private final SubmodelEventHandler handler;
+
+
+ public KafkaSubmodelRepositoryFactory(SubmodelRepositoryFactory decorated, SubmodelEventHandler handler) {
+ this.decorated = decorated;
+ this.handler = handler;
+ }
+
+ @Override
+ public SubmodelRepository create() {
+ return new KafkaSubmodelRepository(decorated.create(), handler);
+ }
+}
diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/kafka/KafkaSubmodelRepositoryFeature.java b/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/kafka/KafkaSubmodelRepositoryFeature.java
new file mode 100644
index 000000000..b3da65ee3
--- /dev/null
+++ b/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/kafka/KafkaSubmodelRepositoryFeature.java
@@ -0,0 +1,77 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelrepository.feature.kafka;
+
+import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepositoryFactory;
+import org.eclipse.digitaltwin.basyx.submodelrepository.feature.SubmodelRepositoryFeature;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.SubmodelEventHandler;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+@ConditionalOnExpression("#{${" + KafkaSubmodelRepositoryFeature.FEATURENAME + ".enabled:false} or ${basyx.feature.kafka.enabled:false}}")
+@Component
+public class KafkaSubmodelRepositoryFeature implements SubmodelRepositoryFeature {
+
+ public final static String FEATURENAME = "basyx.submodelrepository.feature.kafka";
+
+ public final static String FEATURE_ENABLED_EXPRESSION = "#{${" + FEATURENAME + ".enabled:false} or ${basyx.feature.kafka.enabled:false}}";
+
+ private final SubmodelEventHandler handler;
+
+ @Autowired
+ public KafkaSubmodelRepositoryFeature(SubmodelEventHandler handler) {
+ this.handler = handler;
+ }
+
+ @Override
+ public SubmodelRepositoryFactory decorate(SubmodelRepositoryFactory factory) {
+ return new KafkaSubmodelRepositoryFactory(factory, handler);
+ }
+
+ @Override
+ public void initialize() {
+ }
+
+ @Override
+ public void cleanUp() {
+
+ }
+
+ @Override
+ public String getName() {
+ return "SubmodelRepository Kafka";
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+}
diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/kafka/KafkaEventsInMemoryStorageIntegrationTest.java b/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/kafka/KafkaEventsInMemoryStorageIntegrationTest.java
new file mode 100644
index 000000000..f9aa1c9d4
--- /dev/null
+++ b/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/kafka/KafkaEventsInMemoryStorageIntegrationTest.java
@@ -0,0 +1,354 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelrepository.feature.kafka;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.SerializationException;
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonSerializer;
+import org.eclipse.digitaltwin.aas4j.v3.model.Blob;
+import org.eclipse.digitaltwin.aas4j.v3.model.Property;
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
+import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementList;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultBlob;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodel;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementCollection;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementList;
+import org.eclipse.digitaltwin.basyx.core.exceptions.ElementDoesNotExistException;
+import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo;
+import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository;
+import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepositoryFactory;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.SubmodelEventKafkaListener;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.SubmodelServiceTestComponent;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.TestSubmodels;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.SubmodelEvent;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.SubmodelEventType;
+import org.eclipse.digitaltwin.basyx.submodelservice.value.PropertyValue;
+import org.eclipse.digitaltwin.basyx.submodelservice.value.SubmodelValueOnly;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.annotation.DirtiesContext.ClassMode;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit4.SpringRunner;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
+@RunWith(SpringRunner.class)
+@ActiveProfiles("test-submodel")
+@ContextConfiguration(classes = SubmodelServiceTestComponent.class)
+@TestPropertySource(properties = { "basyx.backend=InMemory", "spring.kafka.bootstrap-servers=PLAINTEXT_HOST://localhost:9092", KafkaSubmodelRepositoryFeature.FEATURENAME + ".preservationlevel=REMOVE_BLOB_VALUE",
+ KafkaSubmodelRepositoryFeature.FEATURENAME + ".enabled=true", KafkaSubmodelRepositoryFeature.FEATURENAME + ".topic.name=submodel-events" })
+@Import(value = { SubmodelEventKafkaListener.class })
+public class KafkaEventsInMemoryStorageIntegrationTest {
+
+ private static final String IDSHORT_BLOB = "blob";
+
+ private static final String IDSHORT_LIST0 = "List0";
+
+ private static final String ID_SM1 = "http://sm.id/1";
+
+ @Autowired
+ private SubmodelEventKafkaListener listener;
+
+ @Autowired
+ private KafkaSubmodelRepositoryFeature feature;
+
+ @Autowired
+ private SubmodelRepositoryFactory factory;
+
+ private SubmodelRepository repo;
+
+ @Autowired
+ private JsonSerializer serializer;
+
+ @Before
+ public void awaitAssignment() throws InterruptedException {
+ listener.awaitTopicAssignment();
+ repo = feature.decorate(factory).create();
+
+ cleanup();
+ }
+
+ @Test
+ public void testSubmodelCreated() throws InterruptedException {
+ Submodel sm = TestSubmodels.createSubmodel(ID_SM1, TestSubmodels.IDSHORT_PROP_0, "7");
+ repo.createSubmodel(sm);
+
+ SubmodelEvent evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SM_CREATED, evt.getType());
+ Assert.assertEquals(ID_SM1, evt.getId());
+ Assert.assertEquals(sm, evt.getSubmodel());
+ Assert.assertNull(evt.getSmElementPath());
+ Assert.assertNull(evt.getSmElement());
+ }
+
+ @Test
+ public void testSubmodelUpdated() throws InterruptedException {
+ Submodel sm = TestSubmodels.createSubmodel(ID_SM1, TestSubmodels.IDSHORT_PROP_0, "7");
+ repo.createSubmodel(sm);
+ SubmodelEvent evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SM_CREATED, evt.getType());
+
+ Submodel smUpdated = TestSubmodels.createSubmodel(ID_SM1, TestSubmodels.IDSHORT_PROP_0, "9");
+ repo.updateSubmodel(ID_SM1, smUpdated);
+
+ evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SM_UPDATED, evt.getType());
+ Assert.assertEquals(ID_SM1, evt.getId());
+ Assert.assertEquals(smUpdated, evt.getSubmodel());
+ Assert.assertNull(evt.getSmElementPath());
+ Assert.assertNull(evt.getSmElement());
+ }
+
+ @Test
+ public void testSubmodelPatched() throws InterruptedException {
+ Submodel sm = TestSubmodels.createSubmodel(ID_SM1, TestSubmodels.IDSHORT_PROP_0, "7");
+ repo.createSubmodel(sm);
+ SubmodelEvent evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SM_CREATED, evt.getType());
+
+ SubmodelElement elem1 = TestSubmodels.submodelElement(TestSubmodels.IDSHORT_PROP_1, "2");
+ repo.patchSubmodelElements(ID_SM1, List.of(elem1));
+
+ evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SM_UPDATED, evt.getType());
+ Assert.assertEquals(ID_SM1, evt.getId());
+ Submodel smPatched = new DefaultSubmodel.Builder().id(ID_SM1).idShort(TestSubmodels.IDSHORT_SM).submodelElements(List.of(elem1)).build();
+ Assert.assertEquals(smPatched, evt.getSubmodel());
+ Assert.assertNull(evt.getSmElementPath());
+ Assert.assertNull(evt.getSmElement());
+ }
+
+ @Test
+ public void testSetSubmodelElementValue() throws InterruptedException {
+ PropertyValue value = new PropertyValue("111");
+ Submodel sm = TestSubmodels.createSubmodel(TestSubmodels.ID_SM, TestSubmodels.IDSHORT_PROP_0, "7");
+ repo.createSubmodel(sm);
+ SubmodelEvent evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SM_CREATED, evt.getType());
+ repo.setSubmodelElementValue(TestSubmodels.ID_SM, TestSubmodels.IDSHORT_PROP_0, value);
+ evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SME_UPDATED, evt.getType());
+
+ Assert.assertEquals(TestSubmodels.ID_SM, evt.getId());
+ Assert.assertNull(evt.getSubmodel());
+ Assert.assertEquals(TestSubmodels.IDSHORT_PROP_0, evt.getSmElementPath());
+ Assert.assertEquals(TestSubmodels.submodelElement(TestSubmodels.IDSHORT_PROP_0, "111"), evt.getSmElement());
+ }
+
+ @Test
+ public void testSubmodelDeleted() throws InterruptedException {
+ Submodel sm = TestSubmodels.createSubmodel(ID_SM1, TestSubmodels.IDSHORT_PROP_0, "7");
+ repo.createSubmodel(sm);
+ SubmodelEvent evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SM_CREATED, evt.getType());
+
+ repo.deleteSubmodel(ID_SM1);
+ evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SM_DELETED, evt.getType());
+
+ Assert.assertEquals(ID_SM1, evt.getId());
+ Assert.assertNull(evt.getSubmodel());
+ Assert.assertNull(evt.getSmElementPath());
+ Assert.assertNull(evt.getSmElement());
+ }
+
+ @Test
+ public void testSubmodelElementAdded() throws InterruptedException {
+ Submodel sm = TestSubmodels.createSubmodel(ID_SM1, TestSubmodels.IDSHORT_PROP_0, "7");
+ repo.createSubmodel(sm);
+ SubmodelEvent evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SM_CREATED, evt.getType());
+
+ SubmodelElement elem = TestSubmodels.submodelElement(TestSubmodels.IDSHORT_PROP_1, "88");
+ repo.createSubmodelElement(ID_SM1, elem);
+
+ evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SME_CREATED, evt.getType());
+ Assert.assertEquals(ID_SM1, evt.getId());
+ Assert.assertEquals(TestSubmodels.IDSHORT_PROP_1, evt.getSmElementPath());
+ Assert.assertNull(evt.getSubmodel());
+ Assert.assertEquals(elem, evt.getSmElement());
+ }
+
+ @Test
+ public void testSubmodelElementUpdated() throws InterruptedException {
+ Submodel sm = TestSubmodels.createSubmodel(ID_SM1, TestSubmodels.IDSHORT_PROP_0, "7");
+ repo.createSubmodel(sm);
+ SubmodelEvent evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SM_CREATED, evt.getType());
+
+ SubmodelElement elem = TestSubmodels.submodelElement(TestSubmodels.IDSHORT_PROP_0, "88");
+ repo.updateSubmodelElement(ID_SM1, TestSubmodels.IDSHORT_PROP_0, elem);
+
+ evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SME_UPDATED, evt.getType());
+ Assert.assertEquals(ID_SM1, evt.getId());
+ Assert.assertEquals(TestSubmodels.IDSHORT_PROP_0, evt.getSmElementPath());
+ Assert.assertNull(evt.getSubmodel());
+ Assert.assertEquals(elem, evt.getSmElement());
+
+ }
+
+ @Test
+ public void testSubmodelElementAddedUnderPath() throws InterruptedException {
+ Submodel sm = TestSubmodels.createSubmodel(ID_SM1, TestSubmodels.IDSHORT_PROP_0, "7");
+ repo.createSubmodel(sm);
+ SubmodelEvent evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SM_CREATED, evt.getType());
+
+ SubmodelElement elem = TestSubmodels.submodelElement(TestSubmodels.IDSHORT_PROP_1, "88");
+ repo.createSubmodelElement(ID_SM1, TestSubmodels.IDSHORT_COLL, elem);
+
+ evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SME_CREATED, evt.getType());
+ Assert.assertEquals(ID_SM1, evt.getId());
+ Assert.assertEquals(TestSubmodels.IDSHORT_COLL, evt.getSmElementPath());
+ Assert.assertNull(evt.getSubmodel());
+ Assert.assertEquals(elem, evt.getSmElement());
+ }
+
+ @Test
+ public void testSubmodelElementAddedAndBlobValueNotPartOfTheEvent() throws InterruptedException {
+
+ Submodel sm = TestSubmodels.submodel();
+ repo.createSubmodel(sm);
+ SubmodelEvent evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SM_CREATED, evt.getType());
+
+ SubmodelElementList sme = new DefaultSubmodelElementList.Builder().idShort(IDSHORT_LIST0).value(new DefaultBlob.Builder().idShort(IDSHORT_BLOB).value(new byte[] { 1, 2, 3, 4 }).build()).build();
+ repo.createSubmodelElement(TestSubmodels.ID_SM, TestSubmodels.IDSHORT_COLL, sme);
+
+ SubmodelEvent evtCreated = listener.next();
+ Assert.assertEquals(SubmodelEventType.SME_CREATED, evtCreated.getType());
+
+ SubmodelElementList expected = new DefaultSubmodelElementList.Builder().idShort(IDSHORT_LIST0).value(new DefaultBlob.Builder().idShort(IDSHORT_BLOB).build()).build();
+
+ Assert.assertEquals(TestSubmodels.ID_SM, evtCreated.getId());
+ Assert.assertEquals(TestSubmodels.IDSHORT_COLL, evtCreated.getSmElementPath());
+ Assert.assertEquals(expected, evtCreated.getSmElement());
+ Assert.assertNull(evtCreated.getSubmodel());
+ }
+
+ @Test
+ public void testSubmodelElementCreatedAndBlobValueNotPartOfTheEvent() throws InterruptedException {
+ Submodel sm = TestSubmodels.submodel();
+ repo.createSubmodel(sm);
+ SubmodelEvent evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SM_CREATED, evt.getType());
+
+ Blob blob = new DefaultBlob.Builder().idShort(IDSHORT_BLOB).value(new byte[] { 1, 2, 3, 4 }).build();
+ repo.createSubmodelElement(TestSubmodels.ID_SM, blob);
+
+ SubmodelEvent evtCreated = listener.next();
+ Assert.assertEquals(SubmodelEventType.SME_CREATED, evtCreated.getType());
+
+ Blob expected = new DefaultBlob.Builder().idShort(IDSHORT_BLOB).build();
+
+ Assert.assertEquals(TestSubmodels.ID_SM, evtCreated.getId());
+ Assert.assertEquals(IDSHORT_BLOB, evtCreated.getSmElementPath());
+ Assert.assertEquals(expected, evtCreated.getSmElement());
+ Assert.assertNull(evtCreated.getSubmodel());
+ }
+
+ @Test
+ public void testSubmodelElementDeleted() throws InterruptedException {
+ Submodel sm = TestSubmodels.submodel();
+ repo.createSubmodel(sm);
+ SubmodelEvent evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SM_CREATED, evt.getType());
+
+ Property prop = new DefaultProperty.Builder().idShort(IDSHORT_BLOB).value("4").build();
+ repo.createSubmodelElement(TestSubmodels.ID_SM, prop);
+
+ SubmodelEvent evtCreated = listener.next();
+ Assert.assertEquals(SubmodelEventType.SME_CREATED, evtCreated.getType());
+ repo.deleteSubmodelElement(TestSubmodels.ID_SM, TestSubmodels.IDSHORT_PROP_TO_BE_REMOVED);
+
+ SubmodelEvent evtDeleted = listener.next();
+ Assert.assertEquals(SubmodelEventType.SME_DELETED, evtDeleted.getType());
+
+ Assert.assertEquals(TestSubmodels.ID_SM, evtDeleted.getId());
+ Assert.assertEquals(TestSubmodels.IDSHORT_PROP_TO_BE_REMOVED, evtDeleted.getSmElementPath());
+ Assert.assertNull(evtDeleted.getSmElement());
+ Assert.assertNull(evtDeleted.getSubmodel());
+ }
+
+ @Test
+ public void testGetterAreWorking() throws ElementDoesNotExistException, SerializationException {
+ Submodel expectedSm = TestSubmodels.submodel();
+ repo.createSubmodel(expectedSm);
+ List result = repo.getAllSubmodels(new PaginationInfo(null, null)).getResult();
+ Assert.assertEquals(1, result.size());
+ Assert.assertEquals(expectedSm, result.get(0));
+ List expectedElems = expectedSm.getSubmodelElements().stream().sorted(Comparator.comparing(SubmodelElement::getIdShort)).collect(Collectors.toList());
+ Assert.assertEquals(expectedElems, repo.getSubmodelElements(TestSubmodels.ID_SM, new PaginationInfo(null, null)).getResult());
+
+ Assert.assertEquals(expectedSm, repo.getSubmodel(TestSubmodels.ID_SM));
+
+ SubmodelValueOnly smvOnly = new SubmodelValueOnly(expectedSm.getSubmodelElements());
+
+ Assert.assertEquals(serializer.write(smvOnly), serializer.write(repo.getSubmodelByIdValueOnly(TestSubmodels.ID_SM)));
+
+ Submodel expectedMetaData = new DefaultSubmodel.Builder().id(TestSubmodels.ID_SM).submodelElements((List) null).idShort(TestSubmodels.IDSHORT_SM).build();
+ Assert.assertEquals(expectedMetaData, repo.getSubmodelByIdMetadata(TestSubmodels.ID_SM));
+
+ Assert.assertEquals(new DefaultSubmodelElementCollection.Builder().idShort(TestSubmodels.IDSHORT_COLL).build(), repo.getSubmodelElement(TestSubmodels.ID_SM, TestSubmodels.IDSHORT_COLL));
+ }
+
+ @Test
+ public void testFeatureIsEnabled() {
+ Assert.assertTrue(feature.isEnabled());
+ }
+
+ @After
+ public void cleanup() throws InterruptedException {
+ if (repo != null) {
+ for (Submodel sm : repo.getAllSubmodels(new PaginationInfo(null, null)).getResult()) {
+ repo.deleteSubmodel(sm.getId());
+ }
+ }
+ while (listener.next(300, TimeUnit.MICROSECONDS) != null);
+
+ }
+}
diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/SubmodelRepositoryRegistryLinkTestSuite.java b/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/SubmodelRepositoryRegistryLinkTestSuite.java
index 3a2ba08b9..ef06637d0 100644
--- a/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/SubmodelRepositoryRegistryLinkTestSuite.java
+++ b/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/SubmodelRepositoryRegistryLinkTestSuite.java
@@ -42,7 +42,6 @@
import org.eclipse.digitaltwin.basyx.submodelregistry.client.mapper.DummySubmodelDescriptorFactory;
import org.eclipse.digitaltwin.basyx.submodelregistry.client.model.GetSubmodelDescriptorsResult;
import org.eclipse.digitaltwin.basyx.submodelregistry.client.model.SubmodelDescriptor;
-import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.HttpStatus;
@@ -65,6 +64,11 @@ public abstract class SubmodelRepositoryRegistryLinkTestSuite {
private final SubmodelDescriptor DUMMY_DESCRIPTOR = DummySubmodelDescriptorFactory.createDummyDescriptor(DUMMY_SUBMODEL_ID, DUMMY_SUBMODEL_IDSHORT, DummySubmodelDescriptorFactory.createSemanticId(),
DummySubmodelDescriptorFactory.buildAdministrationInformation("0", "9", "testTemplateId"), getSubmodelRepoBaseUrls());
+ @Before
+ public void cleanup() throws ApiException {
+ getSubmodelRegistryApi().deleteAllSubmodelDescriptors();
+ }
+
@Test
public void createSubmodel() throws FileNotFoundException, IOException, ApiException {
String submodelJsonContent = getSubmodelJSONString();
diff --git a/basyx.submodelrepository/basyx.submodelrepository.component/pom.xml b/basyx.submodelrepository/basyx.submodelrepository.component/pom.xml
index 66cc6d53c..beecc5013 100644
--- a/basyx.submodelrepository/basyx.submodelrepository.component/pom.xml
+++ b/basyx.submodelrepository/basyx.submodelrepository.component/pom.xml
@@ -70,6 +70,22 @@
tests
test
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelrepository-feature-kafka
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelrepository-feature-kafka
+ tests
+ test
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelservice-feature-kafka
+ tests
+ test
+
org.eclipse.digitaltwin.basyx
basyx.submodelrepository-http
diff --git a/basyx.submodelrepository/basyx.submodelrepository.component/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/component/SubmodelKafkaFeatureEnabledSmokeTest.java b/basyx.submodelrepository/basyx.submodelrepository.component/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/component/SubmodelKafkaFeatureEnabledSmokeTest.java
new file mode 100644
index 000000000..292304f56
--- /dev/null
+++ b/basyx.submodelrepository/basyx.submodelrepository.component/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/component/SubmodelKafkaFeatureEnabledSmokeTest.java
@@ -0,0 +1,107 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelrepository.component;
+
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.SerializationException;
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonSerializer;
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodel;
+import org.eclipse.digitaltwin.basyx.submodelrepository.feature.kafka.KafkaSubmodelRepositoryFeature;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.SubmodelEventKafkaListener;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.SubmodelEvent;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.SubmodelEventType;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.annotation.DirtiesContext.ClassMode;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit4.SpringRunner;
+
+/**
+ * @author sonnenberg (DFKI GmbH)
+ */
+@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@ComponentScan(basePackages = { "org.eclipse.digitaltwin.basyx" })
+@ContextConfiguration(classes = { SubmodelRepositoryComponent.class })
+@RunWith(SpringRunner.class)
+@TestPropertySource(properties = { "basyx.feature.kafka.enabled=true",
+ "spring.kafka.bootstrap-servers=PLAINTEXT_HOST://localhost:9092",
+ KafkaSubmodelRepositoryFeature.FEATURENAME + ".enabled=true",
+ KafkaSubmodelRepositoryFeature.FEATURENAME + ".topic.name=" + SubmodelEventKafkaListener.TOPIC_NAME })
+@Import(SubmodelEventKafkaListener.class)
+public class SubmodelKafkaFeatureEnabledSmokeTest {
+
+ @LocalServerPort
+ private int port;
+
+ @Autowired
+ private TestRestTemplate restTemplate;
+
+ @Autowired
+ private JsonSerializer serializer;
+
+ @Autowired
+ private SubmodelEventKafkaListener listener;
+
+ @Before
+ public void provideAas() throws InterruptedException {
+ listener.awaitTopicAssignment();
+ }
+
+ @Test
+ public void testAasCreatedEvent() throws InterruptedException, SerializationException {
+ Submodel sm = new DefaultSubmodel.Builder().id("http://sm.id/1").build();
+ HttpEntity entity = createHttpEntity(sm);
+ Assert.assertTrue(restTemplate.exchange(createEndpointUrl(), HttpMethod.POST, entity, String.class).getStatusCode().is2xxSuccessful());
+ SubmodelEvent event = listener.next();
+ Assert.assertEquals(SubmodelEventType.SM_CREATED, event.getType());
+ Assert.assertEquals(sm.getId(), event.getId());
+ Assert.assertEquals(sm, event.getSubmodel());
+ }
+
+ private String createEndpointUrl() {
+ return "http://localhost:" + port + "/submodels";
+ }
+
+ private HttpEntity createHttpEntity(Submodel sm) throws SerializationException {
+ HttpHeaders headers = new HttpHeaders();
+ headers.set(HttpHeaders.CONTENT_TYPE, "application/json");
+ return new HttpEntity<>(serializer.write(sm), headers);
+ }
+}
diff --git a/basyx.submodelrepository/pom.xml b/basyx.submodelrepository/pom.xml
index b4222edd5..eb44d50d6 100644
--- a/basyx.submodelrepository/pom.xml
+++ b/basyx.submodelrepository/pom.xml
@@ -19,6 +19,7 @@
basyx.submodelrepository-backend
basyx.submodelrepository-backend-inmemory
basyx.submodelrepository-feature-mqtt
+ basyx.submodelrepository-feature-kafka
basyx.submodelrepository-feature-registry-integration
basyx.submodelrepository-feature-authorization
basyx.submodelrepository-feature-operation-delegation
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/README.md b/basyx.submodelservice/basyx.submodelservice-feature-kafka/README.md
new file mode 100644
index 000000000..de9abc0e1
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/README.md
@@ -0,0 +1,45 @@
+# Submodel Repository - KAFKA Eventing
+
+This feature provides KAFKA eventing. Messages are sent whenever a resource in the submodel repository is created, modified, or deleted.
+
+It is essential to maintain the insertion order of events per submodel. Events for creating, updating, and deleting must not overtake one another. For this reason, only one topic is used, and all messages related to a submodel are stored in a single partition, as the submodel ID is used as the message key. Only within a partition is the order guaranteed when consuming messages.
+
+## Feature Configuration
+
+The feature is configured using the following Spring properties:
+
+| Property | Default | Description |
+|----------------------------------------------------|------------------|--------------------------------------------------------------------------------------------|
+| basyx.submodelservice.feature.kafka.enabled | false | Specifies whether the feature is enabled |
+| basyx.feature.kafka.enabled | false | Specifies whether the feature is enabled (for both aas-repository and submodel-repository) |
+| basyx.submodelservice.feature.kafka.topic.name | submodel-events | The name of the topic where events are sent |
+| basyx.submodelservice.feature.kafka.submodelevents | false | Specifies whether to send submodel creation and deletion events when starting and tearing down the submodel service |
+| spring.kafka.bootstrap-servers | - | The address of the Kafka brokers, e.g., `PLAINTEXT_HOST://localhost:9092` |
+
+
+## Message Structure
+
+The messages are transmitted as strings in JSON format:
+
+```json
+{
+ "type": "SM_CREATED",
+ "id": "http://sm.ids.org/1",
+ "submodel": {
+ "modelType": "Submodel",
+ "id": "http://sm.ids.org/1",
+ "idShort": "1"
+ },
+ "smElement": null,
+ "smElementPath": null
+}
+```
+
+Depending on the event type, the fields of the JSON message are populated. The following event types are available:
+
+* SM_CREATED
+* SM_UPDATED
+* SM_DELETED
+* SME_UPDATED
+* SME_CREATED
+* SME_DELETED
\ No newline at end of file
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/pom.xml b/basyx.submodelservice/basyx.submodelservice-feature-kafka/pom.xml
new file mode 100644
index 000000000..11f5c64d3
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/pom.xml
@@ -0,0 +1,44 @@
+
+ 4.0.0
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelservice
+ ${revision}
+
+ basyx.submodelservice-feature-kafka
+ BaSyx submodelservice-feature-kafka
+ BaSyx submodelservice-feature-kafka
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelservice-backend-inmemory
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+ org.junit.vintage
+ junit-vintage-engine
+ test
+
+
+ org.hamcrest
+ hamcrest-core
+
+
+
+
+
\ No newline at end of file
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelService.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelService.java
new file mode 100644
index 000000000..d2846fa78
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelService.java
@@ -0,0 +1,148 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.List;
+
+import org.eclipse.digitaltwin.aas4j.v3.model.OperationVariable;
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
+import org.eclipse.digitaltwin.basyx.core.exceptions.ElementDoesNotExistException;
+import org.eclipse.digitaltwin.basyx.core.exceptions.ElementNotAFileException;
+import org.eclipse.digitaltwin.basyx.core.exceptions.FileDoesNotExistException;
+import org.eclipse.digitaltwin.basyx.core.pagination.CursorResult;
+import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo;
+import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelService;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.SubmodelEventHandler;
+import org.eclipse.digitaltwin.basyx.submodelservice.value.SubmodelElementValue;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public class KafkaSubmodelService implements SubmodelService {
+
+ private final SubmodelService decorated;
+ private final SubmodelEventHandler eventHandler;
+ private final String submodelId;
+
+ public KafkaSubmodelService(SubmodelService decorated, SubmodelEventHandler handler, String submodelId) {
+ this.decorated = decorated;
+ this.eventHandler = handler;
+ this.submodelId = submodelId;
+ }
+
+ @Override
+ public Submodel getSubmodel() {
+ return decorated.getSubmodel();
+ }
+
+ @Override
+ public CursorResult> getSubmodelElements(PaginationInfo pInfo) {
+ return decorated.getSubmodelElements(pInfo);
+ }
+
+ @Override
+ public SubmodelElement getSubmodelElement(String idShortPath) throws ElementDoesNotExistException {
+ return decorated.getSubmodelElement(idShortPath);
+ }
+
+ @Override
+ public SubmodelElementValue getSubmodelElementValue(String idShortPath) throws ElementDoesNotExistException {
+ return decorated.getSubmodelElementValue(idShortPath);
+ }
+
+ @Override
+ public void setSubmodelElementValue(String idShortPath, SubmodelElementValue value)
+ throws ElementDoesNotExistException {
+ decorated.setSubmodelElementValue(idShortPath, value);
+ SubmodelElement submodelElement = decorated.getSubmodelElement(idShortPath);
+ eventHandler.onSubmodelElementUpdated(submodelElement, submodelId, idShortPath);
+ }
+
+ @Override
+ public void createSubmodelElement(SubmodelElement submodelElement) {
+ decorated.createSubmodelElement(submodelElement);
+ eventHandler.onSubmodelElementCreated(submodelElement, submodelId, submodelElement.getIdShort());
+ }
+
+ @Override
+ public void createSubmodelElement(String idShortPath, SubmodelElement submodelElement)
+ throws ElementDoesNotExistException {
+ decorated.createSubmodelElement(idShortPath, submodelElement);
+ eventHandler.onSubmodelElementCreated(submodelElement, submodelId, idShortPath + "." + submodelElement.getIdShort());
+ }
+
+ @Override
+ public void updateSubmodelElement(String idShortPath, SubmodelElement submodelElement)
+ throws ElementDoesNotExistException {
+ decorated.updateSubmodelElement(idShortPath, submodelElement);
+ eventHandler.onSubmodelElementUpdated(submodelElement, submodelId, idShortPath);
+ }
+
+ @Override
+ public void deleteSubmodelElement(String idShortPath) throws ElementDoesNotExistException {
+ decorated.deleteSubmodelElement(idShortPath);
+ eventHandler.onSubmodelElementDeleted(submodelId, idShortPath);
+ }
+
+ @Override
+ public void patchSubmodelElements(List submodelElementList) {
+ decorated.patchSubmodelElements(submodelElementList);
+ Submodel submodel = decorated.getSubmodel();
+ eventHandler.onSubmodelUpdated(submodel);
+ }
+
+ @Override
+ public OperationVariable[] invokeOperation(String idShortPath, OperationVariable[] input)
+ throws ElementDoesNotExistException {
+ return decorated.invokeOperation(idShortPath, input);
+ }
+
+ @Override
+ public File getFileByPath(String idShortPath)
+ throws ElementDoesNotExistException, ElementNotAFileException, FileDoesNotExistException {
+ return decorated.getFileByPath(idShortPath);
+ }
+
+ @Override
+ public void setFileValue(String idShortPath, String fileName, InputStream inputStream)
+ throws ElementDoesNotExistException, ElementNotAFileException {
+ decorated.setFileValue(idShortPath, fileName, inputStream);
+ }
+
+ @Override
+ public void deleteFileValue(String idShortPath)
+ throws ElementDoesNotExistException, ElementNotAFileException, FileDoesNotExistException {
+ decorated.deleteFileValue(idShortPath);
+ }
+
+ @Override
+ public InputStream getFileByFilePath(String filePath) throws FileDoesNotExistException {
+ return decorated.getFileByFilePath(filePath);
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceApplicationListener.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceApplicationListener.java
new file mode 100644
index 000000000..528c89d9f
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceApplicationListener.java
@@ -0,0 +1,64 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka;
+
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.SubmodelEventHandler;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.ApplicationEvent;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.event.ContextClosedEvent;
+import org.springframework.stereotype.Component;
+
+@Component
+@ConditionalOnExpression(KafkaSubmodelServiceFeature.FEATURE_ENABLED_EXPRESSION)
+@ConditionalOnProperty(name = KafkaSubmodelServiceApplicationListener.SUBMODEL_EVENTS_ACTIVATED, havingValue = "true", matchIfMissing = false)
+public class KafkaSubmodelServiceApplicationListener implements ApplicationListener {
+
+ public static final String SUBMODEL_EVENTS_ACTIVATED = KafkaSubmodelServiceFeature.FEATURENAME + ".submodelevents";
+
+ private final SubmodelEventHandler handler;
+ private final Submodel submodel;
+
+ @Autowired
+ private KafkaSubmodelServiceApplicationListener(SubmodelEventHandler handler, Submodel submodel) {
+ this.handler = handler;
+ this.submodel = submodel;
+ }
+
+ @Override
+ public void onApplicationEvent(ApplicationEvent event) {
+ // only fired if submodelEvents are active
+ if (event instanceof ApplicationReadyEvent) {
+ handler.onSubmodelCreated(submodel);
+ } else if (event instanceof ContextClosedEvent) {
+ handler.onSubmodelDeleted(submodel.getId());
+ }
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceConfiguration.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceConfiguration.java
new file mode 100644
index 000000000..9225c3400
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceConfiguration.java
@@ -0,0 +1,84 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka;
+
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonSerializer;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.BlobRemovingSubmodelShrinker;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.DistributingSubmodelEventHandler;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.IdOnlyEventDistributer;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.KafkaSubmodelEventDistributer;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.SubmodelEventDistributer;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.SubmodelEventHandler;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.WithoutBlobEventDistributer;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.DataPreservationLevel;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.core.KafkaTemplate;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+@ConditionalOnExpression(KafkaSubmodelServiceFeature.FEATURE_ENABLED_EXPRESSION)
+@Configuration
+public class KafkaSubmodelServiceConfiguration {
+
+ @ConditionalOnMissingBean
+ @Bean
+ public JsonSerializer aas4jSerializer() {
+ return new JsonSerializer();
+ }
+
+ @ConditionalOnMissingBean
+ @Bean
+ public DataPreservationLevel preservationLevel(
+ @Value("${" + KafkaSubmodelServiceFeature.FEATURENAME + ".preservationlevel:RETAIN_FULL}") String level) {
+ return DataPreservationLevel.valueOf(level);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public SubmodelEventDistributer eventDistributer(DataPreservationLevel level, JsonSerializer serializer,
+ KafkaTemplate template,
+ @Value("${" + KafkaSubmodelServiceFeature.FEATURENAME + ".topic.name:submodel-events}") String topicName) {
+ SubmodelEventDistributer distributer = new KafkaSubmodelEventDistributer(serializer, template, topicName);
+ if (DataPreservationLevel.REMOVE_BLOB_VALUE == level) {
+ BlobRemovingSubmodelShrinker shrinker = new BlobRemovingSubmodelShrinker();
+ return new WithoutBlobEventDistributer(distributer, shrinker);
+ } else if (DataPreservationLevel.IDS_ONLY == level) {
+ return new IdOnlyEventDistributer(distributer);
+ }
+ return distributer;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public SubmodelEventHandler submodelEventHandler(SubmodelEventDistributer distributer) {
+ return new DistributingSubmodelEventHandler(distributer);
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceFactory.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceFactory.java
new file mode 100644
index 000000000..4ece5b746
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceFactory.java
@@ -0,0 +1,56 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka;
+
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelService;
+import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelServiceFactory;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.SubmodelEventHandler;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public class KafkaSubmodelServiceFactory implements SubmodelServiceFactory {
+
+ private final SubmodelServiceFactory decorated;
+ private final SubmodelEventHandler handler;
+
+ public KafkaSubmodelServiceFactory(SubmodelServiceFactory decorated, SubmodelEventHandler evtHandler) {
+ this.decorated = decorated;
+ this.handler = evtHandler;
+ }
+
+ @Override
+ public SubmodelService create(Submodel submodel) {
+ return new KafkaSubmodelService(decorated.create(submodel), handler, submodel.getId());
+ }
+
+ @Override
+ public SubmodelService create(String submodelId) {
+ // do not perform a decoration here as this is only used from submodel repository
+ return decorated.create(submodelId);
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceFeature.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceFeature.java
new file mode 100644
index 000000000..64ae3b92f
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceFeature.java
@@ -0,0 +1,76 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka;
+
+import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelServiceFactory;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.SubmodelServiceFeature;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.SubmodelEventHandler;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+@ConditionalOnExpression(KafkaSubmodelServiceFeature.FEATURE_ENABLED_EXPRESSION)
+@Component
+public class KafkaSubmodelServiceFeature implements SubmodelServiceFeature {
+
+ public final static String FEATURENAME = "basyx.submodelservice.feature.kafka";
+
+ public final static String FEATURE_ENABLED_EXPRESSION = "#{${" + FEATURENAME + ".enabled:false} or ${basyx.feature.kafka.enabled:false}}";
+
+ private SubmodelEventHandler handler;
+
+ @Autowired
+ public KafkaSubmodelServiceFeature(SubmodelEventHandler handler) {
+ this.handler = handler;
+ }
+
+ @Override
+ public SubmodelServiceFactory decorate(SubmodelServiceFactory component) {
+ return new KafkaSubmodelServiceFactory(component, handler);
+ }
+
+ @Override
+ public void initialize() {
+ }
+
+ @Override
+ public void cleanUp() {
+
+ }
+
+ @Override
+ public String getName() {
+ return "SubmodelService Kafka";
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/BlobRemovingSubmodelShrinker.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/BlobRemovingSubmodelShrinker.java
new file mode 100644
index 000000000..07a4eff0b
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/BlobRemovingSubmodelShrinker.java
@@ -0,0 +1,196 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.eclipse.digitaltwin.aas4j.v3.model.AnnotatedRelationshipElement;
+import org.eclipse.digitaltwin.aas4j.v3.model.Blob;
+import org.eclipse.digitaltwin.aas4j.v3.model.DataElement;
+import org.eclipse.digitaltwin.aas4j.v3.model.Entity;
+import org.eclipse.digitaltwin.aas4j.v3.model.Operation;
+import org.eclipse.digitaltwin.aas4j.v3.model.OperationVariable;
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
+import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementCollection;
+import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementList;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultAnnotatedRelationshipElement;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultBlob;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultEntity;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultOperation;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultOperationVariable;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodel;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementCollection;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementList;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public class BlobRemovingSubmodelShrinker implements SubmodelShrinker {
+
+
+ @Override
+ public Submodel shrinkSubmodel(Submodel submodel) {
+ // do not retain blob content
+ return new DefaultSubmodel.Builder().administration(submodel.getAdministration())
+ .category(submodel.getCategory()).description(submodel.getDescription())
+ .displayName(submodel.getDisplayName())
+ .embeddedDataSpecifications(submodel.getEmbeddedDataSpecifications())
+ .extensions(submodel.getExtensions()).id(submodel.getId()).idShort(submodel.getIdShort())
+ .kind(submodel.getKind()).qualifiers(submodel.getQualifiers()).semanticId(submodel.getSemanticId())
+ .submodelElements(shrinkSubmodelElements(submodel.getSubmodelElements()))
+ .supplementalSemanticIds(submodel.getSupplementalSemanticIds()).build();
+ }
+
+ @Override
+ public SubmodelElement shrinkSubmodelElement(SubmodelElement from) {
+ if (from instanceof DataElement) {
+ return shrinkDataElement((DataElement) from);
+ } else if (from instanceof Entity) {
+ return shrinkEntity((Entity) from);
+ } else if (from instanceof SubmodelElementCollection) {
+ return shrinkSubmodelElementCollection((SubmodelElementCollection) from);
+ } else if (from instanceof SubmodelElementList) {
+ return shrinkSubmodelElementList((SubmodelElementList) from);
+ } else if (from instanceof Operation) {
+ return shrinkOperation((Operation) from);
+ } else if (from instanceof AnnotatedRelationshipElement) {
+ return shrinkAnnotatedRelationshipElement((AnnotatedRelationshipElement)from);
+ }
+ return from;
+ }
+
+ private List shrinkSubmodelElements(List submodelElements) {
+ if (CollectionUtils.isEmpty(submodelElements)) {
+ return submodelElements;
+ }
+ return submodelElements.stream().map(this::shrinkSubmodelElement).collect(Collectors.toList());
+ }
+
+ private DataElement shrinkDataElement(DataElement from) {
+ if (from instanceof Blob) {
+ return shrinkBlob((Blob) from);
+ }
+ return from;
+ }
+
+ private AnnotatedRelationshipElement shrinkAnnotatedRelationshipElement(AnnotatedRelationshipElement from) {
+ List annotations = from.getAnnotations();
+ if (CollectionUtils.isEmpty(annotations)) {
+ return from;
+ }
+ DefaultAnnotatedRelationshipElement toReturn = new DefaultAnnotatedRelationshipElement();
+
+ toReturn.setFirst(from.getFirst());
+ toReturn.setSecond(toReturn.getSecond());
+ toReturn.setAnnotations(annotations.stream().map(this::shrinkDataElement).collect(Collectors.toList()));
+ return applySubmodelElementValues(from, toReturn);
+ }
+
+ private Operation shrinkOperation(Operation from) {
+ List inVars = from.getInputVariables();
+ List inOutVars = from.getInoutputVariables();
+ List outVars = from.getOutputVariables();
+ if (CollectionUtils.isEmpty(inVars) && CollectionUtils.isEmpty(inOutVars) && CollectionUtils.isEmpty(outVars)) {
+ return from;
+ }
+ DefaultOperation op = new DefaultOperation();
+ if (!CollectionUtils.isEmpty(inVars)) {
+ op.setInputVariables(inVars.stream().map(this::shrinkOperationVariable).collect(Collectors.toList()));
+ }
+ if (!CollectionUtils.isEmpty(inOutVars)) {
+ op.setInoutputVariables(inOutVars.stream().map(this::shrinkOperationVariable).collect(Collectors.toList()));
+ }
+ if (!CollectionUtils.isEmpty(outVars)) {
+ op.setOutputVariables(outVars.stream().map(this::shrinkOperationVariable).collect(Collectors.toList()));
+ }
+ return applySubmodelElementValues(from, op);
+ }
+
+ private OperationVariable shrinkOperationVariable(OperationVariable var) {
+ SubmodelElement elem = var.getValue();
+ if (elem == null) {
+ return var;
+ }
+ return new DefaultOperationVariable.Builder().value(shrinkSubmodelElement(elem)).build();
+ }
+
+ private SubmodelElementList shrinkSubmodelElementList(SubmodelElementList from) {
+ List elements = from.getValue();
+ if (CollectionUtils.isEmpty(elements)) {
+ return from;
+ }
+ DefaultSubmodelElementList list = new DefaultSubmodelElementList();
+ list.setValue(elements.stream().map(this::shrinkSubmodelElement).collect(Collectors.toList()));
+ return applySubmodelElementValues(from, list);
+ }
+
+ private SubmodelElementCollection shrinkSubmodelElementCollection(SubmodelElementCollection from) {
+ List elements = from.getValue();
+ if (CollectionUtils.isEmpty(elements)) {
+ return from;
+ }
+ DefaultSubmodelElementCollection collection = new DefaultSubmodelElementCollection();
+ collection.setValue(elements.stream().map(this::shrinkSubmodelElement).collect(Collectors.toList()));
+ return applySubmodelElementValues(from, collection);
+ }
+
+ private Entity shrinkEntity(Entity from) {
+ List statements = from.getStatements();
+ if (CollectionUtils.isEmpty(statements)) {
+ return from;
+ }
+ DefaultEntity entity = new DefaultEntity();
+ entity.setGlobalAssetId(from.getGlobalAssetId());
+ entity.setEntityType(from.getEntityType());
+ entity.setSpecificAssetIds(from.getSpecificAssetIds());
+ entity.setStatements(statements.stream().map(this::shrinkSubmodelElement).collect(Collectors.toList()));
+ return applySubmodelElementValues(from, entity);
+ }
+
+ private Blob shrinkBlob(Blob blob) {
+ DefaultBlob toReturn = new DefaultBlob();
+ toReturn.setContentType(blob.getContentType());
+ // do not apply the value
+ return applySubmodelElementValues(blob, toReturn);
+ }
+
+ private T applySubmodelElementValues(T from, T copy) {
+ copy.setCategory(from.getCategory());
+ copy.setDescription(from.getDescription());
+ copy.setDisplayName(from.getDisplayName());
+ copy.setEmbeddedDataSpecifications(from.getEmbeddedDataSpecifications());
+ copy.setExtensions(from.getExtensions());
+ copy.setIdShort(from.getIdShort());
+ copy.setQualifiers(from.getQualifiers());
+ copy.setSemanticId(from.getSemanticId());
+ copy.setSupplementalSemanticIds(from.getSupplementalSemanticIds());
+ return copy;
+ }
+
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/DistributingSubmodelEventHandler.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/DistributingSubmodelEventHandler.java
new file mode 100644
index 000000000..282b607fe
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/DistributingSubmodelEventHandler.java
@@ -0,0 +1,97 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events;
+
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.SubmodelEvent;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.SubmodelEventType;
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public class DistributingSubmodelEventHandler implements SubmodelEventHandler {
+
+ private final SubmodelEventDistributer evtDistributer;
+
+ public DistributingSubmodelEventHandler(SubmodelEventDistributer evtDistributer) {
+ this.evtDistributer = evtDistributer;
+ }
+
+ @Override
+ public void onSubmodelCreated(Submodel submodel) {
+ SubmodelEvent event = new SubmodelEvent();
+ event.setType(SubmodelEventType.SM_CREATED);
+ event.setId(submodel.getId());
+ event.setSubmodel(submodel);
+ evtDistributer.distribute(event);
+ }
+
+ @Override
+ public void onSubmodelUpdated(Submodel submodel) {
+ SubmodelEvent event = new SubmodelEvent();
+ event.setType(SubmodelEventType.SM_UPDATED);
+ event.setId(submodel.getId());
+ event.setSubmodel(submodel);
+ evtDistributer.distribute(event);
+ }
+
+ @Override
+ public void onSubmodelDeleted(String id) {
+ SubmodelEvent event = new SubmodelEvent();
+ event.setType(SubmodelEventType.SM_DELETED);
+ event.setId(id);
+ evtDistributer.distribute(event);
+ }
+
+ @Override
+ public void onSubmodelElementCreated(SubmodelElement smElement, String submodelId, String idShortPath) {
+ SubmodelEvent event = new SubmodelEvent();
+ event.setType(SubmodelEventType.SME_CREATED);
+ event.setId(submodelId);
+ event.setSmElement(smElement);
+ event.setSmElementPath(idShortPath);
+ evtDistributer.distribute(event);
+ }
+
+ @Override
+ public void onSubmodelElementUpdated(SubmodelElement smElement, String submodelIdentifier, String idShortPath) {
+ SubmodelEvent event = new SubmodelEvent();
+ event.setType(SubmodelEventType.SME_UPDATED);
+ event.setId(submodelIdentifier);
+ event.setSmElement(smElement);
+ event.setSmElementPath(idShortPath);
+ evtDistributer.distribute(event);
+ }
+
+ @Override
+ public void onSubmodelElementDeleted(String submodelId, String idShortPath) {
+ SubmodelEvent event = new SubmodelEvent();
+ event.setType(SubmodelEventType.SME_DELETED);
+ event.setId(submodelId);
+ event.setSmElementPath(idShortPath);
+ evtDistributer.distribute(event);
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/IdOnlyEventDistributer.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/IdOnlyEventDistributer.java
new file mode 100644
index 000000000..8950f83c4
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/IdOnlyEventDistributer.java
@@ -0,0 +1,46 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events;
+
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.SubmodelEvent;
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public class IdOnlyEventDistributer implements SubmodelEventDistributer {
+
+ private final SubmodelEventDistributer decorated;
+
+ public IdOnlyEventDistributer(SubmodelEventDistributer decorated) {
+ this.decorated = decorated;
+ }
+
+ @Override
+ public void distribute(SubmodelEvent evt) {
+ evt.setSmElement(null);
+ evt.setSubmodel(null);
+ decorated.distribute(evt);
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/KafkaSubmodelEventDistributer.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/KafkaSubmodelEventDistributer.java
new file mode 100644
index 000000000..981776d9b
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/KafkaSubmodelEventDistributer.java
@@ -0,0 +1,66 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.SerializationException;
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonSerializer;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.SubmodelEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.kafka.core.KafkaTemplate;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public class KafkaSubmodelEventDistributer implements SubmodelEventDistributer {
+
+ private static Logger LOGGER = LoggerFactory.getLogger(KafkaSubmodelEventDistributer.class);
+
+ private final JsonSerializer serializer;
+ private final KafkaTemplate template;
+ private String topicName;
+
+ public KafkaSubmodelEventDistributer(JsonSerializer serializer, KafkaTemplate template, String topicName) {
+ this.serializer = serializer;
+ this.template = template;
+ this.topicName = topicName;
+ }
+
+ @Override
+ public void distribute(SubmodelEvent evt) {
+ try {
+ String payload = serializer.write(evt);
+ LOGGER.debug("Send kafka message to " + topicName + ".");
+ template.send(topicName, evt.getId(), payload).get(3, TimeUnit.SECONDS);
+ } catch (InterruptedException | ExecutionException | TimeoutException | SerializationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/SubmodelEventDistributer.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/SubmodelEventDistributer.java
new file mode 100644
index 000000000..55fa010c1
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/SubmodelEventDistributer.java
@@ -0,0 +1,36 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events;
+
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.SubmodelEvent;
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public interface SubmodelEventDistributer {
+
+ void distribute(SubmodelEvent evt);
+
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/SubmodelEventHandler.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/SubmodelEventHandler.java
new file mode 100644
index 000000000..5019b5452
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/SubmodelEventHandler.java
@@ -0,0 +1,47 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events;
+
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public interface SubmodelEventHandler {
+
+ void onSubmodelCreated(Submodel submodel);
+
+ void onSubmodelUpdated(Submodel submodel);
+
+ void onSubmodelElementDeleted(String submodelId, String idShortPath);
+
+ void onSubmodelDeleted(String id);
+
+ void onSubmodelElementCreated(SubmodelElement smElement, String submodelId, String idShortPath);
+
+ void onSubmodelElementUpdated(SubmodelElement smElement, String submodelIdentifier, String idShortPath);
+
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/SubmodelShrinker.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/SubmodelShrinker.java
new file mode 100644
index 000000000..53a71992a
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/SubmodelShrinker.java
@@ -0,0 +1,39 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events;
+
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public interface SubmodelShrinker {
+
+ Submodel shrinkSubmodel(Submodel submodel);
+
+ SubmodelElement shrinkSubmodelElement(SubmodelElement from);
+
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/WithoutBlobEventDistributer.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/WithoutBlobEventDistributer.java
new file mode 100644
index 000000000..81899115f
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/WithoutBlobEventDistributer.java
@@ -0,0 +1,58 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events;
+
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.SubmodelEvent;
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public class WithoutBlobEventDistributer implements SubmodelEventDistributer {
+
+ private final SubmodelEventDistributer decorated;
+ private final SubmodelShrinker shrinker;
+
+ public WithoutBlobEventDistributer(SubmodelEventDistributer decorated, SubmodelShrinker shrinker) {
+ this.decorated = decorated;
+ this.shrinker = shrinker;
+ }
+
+ @Override
+ public void distribute(SubmodelEvent evt) {
+ Submodel submodel = evt.getSubmodel();
+ if (submodel != null) {
+ submodel = shrinker.shrinkSubmodel(submodel);
+ evt.setSubmodel(submodel);
+ }
+ SubmodelElement element = evt.getSmElement();
+ if (element != null) {
+ element = shrinker.shrinkSubmodelElement(element);
+ evt.setSmElement(element);
+ }
+ decorated.distribute(evt);
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/model/DataPreservationLevel.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/model/DataPreservationLevel.java
new file mode 100644
index 000000000..3fcea87ad
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/model/DataPreservationLevel.java
@@ -0,0 +1,33 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public enum DataPreservationLevel {
+ RETAIN_FULL, REMOVE_BLOB_VALUE, IDS_ONLY
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/model/SubmodelEvent.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/model/SubmodelEvent.java
new file mode 100644
index 000000000..6f21ec243
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/model/SubmodelEvent.java
@@ -0,0 +1,108 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model;
+
+import java.util.Objects;
+
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public class SubmodelEvent {
+
+ private SubmodelEventType type;
+ private String id;
+ private Submodel submodel;
+ private SubmodelElement smElement;
+ private String smElementPath;
+
+ public SubmodelEventType getType() {
+ return type;
+ }
+
+ public void setType(SubmodelEventType type) {
+ this.type = type;
+ }
+
+ public Submodel getSubmodel() {
+ return submodel;
+ }
+
+ public void setSubmodel(Submodel submodel) {
+ this.submodel = submodel;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setSmElement(SubmodelElement element) {
+ this.smElement = element;
+ }
+
+ public SubmodelElement getSmElement() {
+ return smElement;
+ }
+
+ public void setSmElementPath(String path) {
+ this.smElementPath = path;
+ }
+
+ public String getSmElementPath() {
+ return smElementPath;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, smElement, smElementPath, submodel, type);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ SubmodelEvent other = (SubmodelEvent) obj;
+ return Objects.equals(id, other.id) && Objects.equals(smElement, other.smElement)
+ && Objects.equals(smElementPath, other.smElementPath) && Objects.equals(submodel, other.submodel)
+ && type == other.type;
+ }
+
+ @Override
+ public String toString() {
+ return "SubmodelEvent [type=" + type + ", id=" + id + ", submodel=" + submodel + ", smElement=" + smElement
+ + ", smElementPath=" + smElementPath + "]";
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/model/SubmodelEventType.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/model/SubmodelEventType.java
new file mode 100644
index 000000000..1939f8251
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/events/model/SubmodelEventType.java
@@ -0,0 +1,34 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public enum SubmodelEventType {
+
+ SM_CREATED, SM_UPDATED, SM_DELETED, SME_UPDATED, SME_CREATED, SME_DELETED
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceIdsOnlySmokeTest.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceIdsOnlySmokeTest.java
new file mode 100644
index 000000000..9b91bc754
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceIdsOnlySmokeTest.java
@@ -0,0 +1,140 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.SerializationException;
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonSerializer;
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
+import org.eclipse.digitaltwin.basyx.core.filerepository.FileRepository;
+import org.eclipse.digitaltwin.basyx.core.filerepository.InMemoryFileRepository;
+import org.eclipse.digitaltwin.basyx.submodelservice.InMemorySubmodelBackend;
+import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelService;
+import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelServiceFactory;
+import org.eclipse.digitaltwin.basyx.submodelservice.backend.CrudSubmodelServiceFactory;
+import org.eclipse.digitaltwin.basyx.submodelservice.backend.SubmodelBackend;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.SubmodelEvent;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.SubmodelEventType;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.annotation.DirtiesContext.ClassMode;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit4.SpringRunner;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
+@ComponentScan(basePackages = { "org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka" })
+@ActiveProfiles("test-submodel")
+@ContextConfiguration(classes = SubmodelServiceTestComponent.class)
+@RunWith(SpringRunner.class)
+@TestPropertySource(properties = { "spring.kafka.bootstrap-servers=PLAINTEXT_HOST://localhost:9092",
+ KafkaSubmodelServiceFeature.FEATURENAME + ".preservationlevel=IDS_ONLY",
+ KafkaSubmodelServiceFeature.FEATURENAME + ".enabled=true",
+ KafkaSubmodelServiceFeature.FEATURENAME + ".topic.name=" + SubmodelEventKafkaListener.TOPIC_NAME,
+})
+@Import(SubmodelEventKafkaListener.class)
+public class KafkaSubmodelServiceIdsOnlySmokeTest {
+
+ @Autowired
+ private SubmodelEventKafkaListener listener;
+
+ @Autowired
+ private KafkaSubmodelServiceFeature feature;
+
+ @Autowired
+ private Submodel submodel;
+
+ private SubmodelService service;
+
+ @Autowired
+ JsonSerializer serializer;
+
+ @Before
+ public void awaitAssignment() throws InterruptedException, SerializationException {
+ listener.awaitTopicAssignment();
+
+ while(listener.next(100, TimeUnit.MICROSECONDS) != null);
+
+ FileRepository repository = new InMemoryFileRepository();
+ SubmodelBackend backend = new InMemorySubmodelBackend();
+ SubmodelServiceFactory smFactory = new CrudSubmodelServiceFactory(backend ,repository);
+ service = feature.decorate(smFactory).create(submodel);
+ }
+
+ @Test
+ public void testToplevelSubmodelElementAdded() throws InterruptedException {
+ Assert.assertTrue(feature.isEnabled());
+
+ SubmodelElement elem = TestSubmodels.submodelElement(TestSubmodels.IDSHORT_PROP_1, "ID");
+ service.createSubmodelElement(elem);
+
+ SubmodelEvent evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SME_CREATED, evt.getType());
+ Assert.assertEquals(submodel.getId(), evt.getId());
+ Assert.assertEquals(TestSubmodels.IDSHORT_PROP_1, evt.getSmElementPath());
+ Assert.assertNull(evt.getSubmodel());
+ Assert.assertNull(evt.getSmElement());
+ }
+
+ @Test
+ public void testSubmodelElementPatched() throws InterruptedException, SerializationException {
+ Assert.assertTrue(feature.isEnabled());
+
+ SubmodelElement elem0 = TestSubmodels.submodelElement(TestSubmodels.IDSHORT_PROP_0, "0");
+ SubmodelElement elem1 = TestSubmodels.submodelElement(TestSubmodels.IDSHORT_PROP_1, "1");
+ service.patchSubmodelElements(List.of(elem0, elem1));
+
+ SubmodelEvent evt = listener.next();
+ // the submodel was updated
+ Assert.assertEquals(SubmodelEventType.SM_UPDATED, evt.getType());
+ Assert.assertEquals(submodel.getId(), evt.getId());
+ Assert.assertNull(evt.getSubmodel()); // ids only
+ Assert.assertNull(evt.getSmElementPath());
+ Assert.assertNull(evt.getSmElement());
+
+ }
+
+ @After
+ public void assertNoAdditionalKafkaMessageOnTopic() throws InterruptedException, SerializationException {
+ Assert.assertNull(listener.next(300, TimeUnit.MILLISECONDS));
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceSubmodelElementsEventsIntegrationTest.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceSubmodelElementsEventsIntegrationTest.java
new file mode 100644
index 000000000..9dbd5a6f0
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceSubmodelElementsEventsIntegrationTest.java
@@ -0,0 +1,178 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka;
+
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.SerializationException;
+import org.eclipse.digitaltwin.aas4j.v3.model.Blob;
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultBlob;
+import org.eclipse.digitaltwin.basyx.core.filerepository.FileRepository;
+import org.eclipse.digitaltwin.basyx.core.filerepository.InMemoryFileRepository;
+import org.eclipse.digitaltwin.basyx.submodelservice.InMemorySubmodelBackend;
+import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelService;
+import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelServiceFactory;
+import org.eclipse.digitaltwin.basyx.submodelservice.backend.CrudSubmodelServiceFactory;
+import org.eclipse.digitaltwin.basyx.submodelservice.backend.SubmodelBackend;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.SubmodelEvent;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.SubmodelEventType;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.annotation.DirtiesContext.ClassMode;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit4.SpringRunner;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
+@ComponentScan(basePackages = { "org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka" })
+@ActiveProfiles("test-submodel")
+@RunWith(SpringRunner.class)
+@ContextConfiguration(classes = {SubmodelServiceTestComponent.class})
+@TestPropertySource(properties = { "spring.kafka.bootstrap-servers=PLAINTEXT_HOST://localhost:9092",
+ KafkaSubmodelServiceFeature.FEATURENAME + ".preservationlevel=REMOVE_BLOB_VALUE",
+ KafkaSubmodelServiceFeature.FEATURENAME + ".enabled=true",
+ KafkaSubmodelServiceFeature.FEATURENAME + ".topic.name=" + SubmodelEventKafkaListener.TOPIC_NAME
+})
+@Import(SubmodelEventKafkaListener.class)
+public class KafkaSubmodelServiceSubmodelElementsEventsIntegrationTest {
+
+ @Autowired
+ private SubmodelEventKafkaListener listener;
+
+ @Autowired
+ private KafkaSubmodelServiceFeature feature;
+
+ @Autowired
+ private Submodel submodel;
+
+ private SubmodelService service;
+
+
+ @Before
+ public void awaitAssignment() throws InterruptedException {
+ listener.awaitTopicAssignment();
+
+ while(listener.next(100, TimeUnit.MICROSECONDS) != null);
+
+ FileRepository repository = new InMemoryFileRepository();
+ SubmodelBackend backend = new InMemorySubmodelBackend();
+ SubmodelServiceFactory smFactory = new CrudSubmodelServiceFactory(backend ,repository);
+ service = feature.decorate(smFactory).create(submodel);
+ }
+
+ @Test
+ public void testToplevelSubmodelElementAdded() throws InterruptedException, SerializationException {
+ Assert.assertTrue(feature.isEnabled());
+
+ SubmodelEvent evt = listener.next(2, TimeUnit.SECONDS);
+
+
+ SubmodelElement elem = TestSubmodels.submodelElement(TestSubmodels.IDSHORT_PROP_1, "ID");
+ service.createSubmodelElement(elem);
+
+ evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SME_CREATED, evt.getType());
+ Assert.assertEquals(submodel.getId(), evt.getId());
+ Assert.assertEquals(TestSubmodels.IDSHORT_PROP_1, evt.getSmElementPath());
+ Assert.assertNull(evt.getSubmodel());
+ Assert.assertEquals(elem, evt.getSmElement());
+ }
+
+ @Test
+ public void testElementAddedUnderCollection() throws InterruptedException {
+ SubmodelElement elem = TestSubmodels.submodelElement(TestSubmodels.IDSHORT_PROP_1, "55");
+ service.createSubmodelElement(TestSubmodels.IDSHORT_COLL, elem);
+
+ SubmodelEvent evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SME_CREATED, evt.getType());
+ Assert.assertEquals(submodel.getId(), evt.getId());
+ String expected = TestSubmodels.path(TestSubmodels.IDSHORT_COLL, TestSubmodels.IDSHORT_PROP_1);
+ Assert.assertEquals(expected, evt.getSmElementPath());
+ Assert.assertNull(evt.getSubmodel());
+ Assert.assertEquals(elem, evt.getSmElement());
+ }
+
+ @Test
+ public void testSubmodelElementAddedAndBlobValueNotPartOfTheEvent() throws InterruptedException {
+ String idShortBlob = "blob";
+ Blob blob = new DefaultBlob.Builder().idShort(idShortBlob).value(new byte[] {1,2,3,4,5}).build();
+ service.createSubmodelElement(blob);
+
+ SubmodelEvent evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SME_CREATED, evt.getType());
+ Assert.assertEquals(TestSubmodels.ID_SM, evt.getId());
+ Assert.assertEquals(idShortBlob, evt.getSmElementPath());
+ Assert.assertNull(evt.getSubmodel());
+ Assert.assertNotEquals(blob, evt.getSmElement());
+ SubmodelElement expectedElem = new DefaultBlob.Builder().idShort(idShortBlob).build(); // expected has no value
+ Assert.assertEquals(expectedElem, evt.getSmElement());
+ }
+
+ @Test
+ public void testSubmodelElementUpdated() throws InterruptedException {
+ SubmodelElement elem = TestSubmodels.submodelElement(TestSubmodels.IDSHORT_PROP_0, "99");
+ service.updateSubmodelElement(TestSubmodels.IDSHORT_PROP_0, elem);
+
+ SubmodelEvent evtUpdated = listener.next();
+ Assert.assertEquals(SubmodelEventType.SME_UPDATED, evtUpdated.getType());
+ Assert.assertEquals(TestSubmodels.ID_SM, evtUpdated.getId());
+ Assert.assertEquals(TestSubmodels.IDSHORT_PROP_0, evtUpdated.getSmElementPath());
+ Assert.assertNull(evtUpdated.getSubmodel());
+ Assert.assertEquals(elem, evtUpdated.getSmElement());
+ }
+
+ @Test
+ public void testSubmodelElementDeleted() throws InterruptedException {
+ service.deleteSubmodelElement(TestSubmodels.IDSHORT_PROP_TO_BE_REMOVED);
+
+ SubmodelEvent evtUpdated = listener.next();
+ Assert.assertEquals(SubmodelEventType.SME_DELETED, evtUpdated.getType());
+ Assert.assertEquals(TestSubmodels.ID_SM, evtUpdated.getId());
+ Assert.assertEquals(TestSubmodels.IDSHORT_PROP_TO_BE_REMOVED, evtUpdated.getSmElementPath());
+ Assert.assertNull(evtUpdated.getSubmodel());
+ Assert.assertNull(evtUpdated.getSmElement());
+ }
+
+ @After
+ public void assertNoAdditionalKafkaMessageOnTopic() throws InterruptedException, SerializationException {
+ Assert.assertNull(listener.next(300, TimeUnit.MILLISECONDS));
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceSubmodelEventsIntegrationTest.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceSubmodelEventsIntegrationTest.java
new file mode 100644
index 000000000..4ef3264fb
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/KafkaSubmodelServiceSubmodelEventsIntegrationTest.java
@@ -0,0 +1,110 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka;
+
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.SerializationException;
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.SubmodelEvent;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.SubmodelEventType;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.event.ContextClosedEvent;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.annotation.DirtiesContext.ClassMode;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit4.SpringRunner;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
+@ComponentScan(basePackages = { "org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka" })
+@RunWith(SpringRunner.class)
+@ActiveProfiles("test-submodel")
+@ContextConfiguration(classes = {SubmodelServiceTestComponent.class})
+@TestPropertySource(properties = { "spring.kafka.bootstrap-servers=PLAINTEXT_HOST://localhost:9092",
+ KafkaSubmodelServiceFeature.FEATURENAME + ".preservationlevel=REMOVE_BLOB_VALUE",
+ KafkaSubmodelServiceFeature.FEATURENAME + ".enabled=true",
+ KafkaSubmodelServiceFeature.FEATURENAME + ".topic.name=" + SubmodelEventKafkaListener.TOPIC_NAME,
+ KafkaSubmodelServiceApplicationListener.SUBMODEL_EVENTS_ACTIVATED + "=true"
+})
+@Import(SubmodelEventKafkaListener.class)
+public class KafkaSubmodelServiceSubmodelEventsIntegrationTest {
+
+ @Autowired
+ private SubmodelEventKafkaListener listener;
+
+ @Autowired
+ private Submodel submodel;
+
+ @Autowired
+ private ApplicationContext context;
+
+ @Before
+ public void awaitAssignment() throws InterruptedException {
+ listener.awaitTopicAssignment();
+ while(listener.next(100, TimeUnit.MICROSECONDS) != null);
+ }
+
+ @Test
+ public void testSubmodelEvents() throws InterruptedException {
+ // we expect the "onStartup" submodel created event
+ SubmodelEvent evt = listener.next();
+ Assert.assertEquals(SubmodelEventType.SM_CREATED, evt.getType());
+ Assert.assertEquals(submodel.getId(), evt.getId());
+ Assert.assertEquals(submodel, evt.getSubmodel());
+ Assert.assertNull(evt.getSmElementPath());
+ Assert.assertNull(evt.getSmElement());
+
+ // simulate closing
+ context.publishEvent(new ContextClosedEvent(context));
+ evt = listener.next(5, TimeUnit.SECONDS);
+ Assert.assertEquals(SubmodelEventType.SM_DELETED, evt.getType());
+ Assert.assertEquals(submodel.getId(), evt.getId());
+ Assert.assertNull(evt.getSubmodel());
+ Assert.assertNull(evt.getSmElementPath());
+ Assert.assertNull(evt.getSmElement());
+ }
+
+ @After
+ public void assertNoAdditionalKafkaMessageOnTopic() throws InterruptedException, SerializationException {
+ Assert.assertNull(listener.next(300, TimeUnit.MILLISECONDS));
+ }
+
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/SubmodelEventKafkaListener.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/SubmodelEventKafkaListener.java
new file mode 100644
index 000000000..c89908347
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/SubmodelEventKafkaListener.java
@@ -0,0 +1,88 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.kafka.common.TopicPartition;
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.DeserializationException;
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonDeserializer;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.SubmodelEvent;
+import org.springframework.boot.test.context.TestComponent;
+import org.springframework.kafka.annotation.KafkaHandler;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.kafka.listener.ConsumerSeekAware;
+import org.springframework.stereotype.Component;
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+@KafkaListener(topics = SubmodelEventKafkaListener.TOPIC_NAME, batch = "false", groupId = "kafka-test", autoStartup = "true")
+@TestComponent
+public class SubmodelEventKafkaListener implements ConsumerSeekAware {
+
+ public static final String TOPIC_NAME = "submodel-events";
+
+ private final LinkedBlockingDeque evt = new LinkedBlockingDeque();
+ private final JsonDeserializer deserializer = new JsonDeserializer();
+ private final CountDownLatch latch = new CountDownLatch(1);
+
+ @KafkaHandler
+ public void receiveMessage(String content) {
+ try {
+ SubmodelEvent event = deserializer.read(content, SubmodelEvent.class);
+ evt.offerFirst(event);
+ } catch (DeserializationException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public SubmodelEvent next(int value, TimeUnit unit) throws InterruptedException {
+ return evt.pollLast(value, unit);
+ }
+
+ public SubmodelEvent next() throws InterruptedException {
+ return next(1, TimeUnit.MINUTES);
+ }
+
+ @Override
+ public void onPartitionsAssigned(Map assignments, ConsumerSeekCallback callback) {
+ for (TopicPartition eachPartition : assignments.keySet()) {
+ if (TOPIC_NAME.equals(eachPartition.topic())) {
+ latch.countDown();
+ }
+ }
+ }
+
+ public void awaitTopicAssignment() throws InterruptedException {
+ if (!latch.await(1, TimeUnit.MINUTES)) {
+ throw new RuntimeException("Timeout occured while waiting for partition assignment. Is kafka running?");
+ }
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/SubmodelServiceTestComponent.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/SubmodelServiceTestComponent.java
new file mode 100644
index 000000000..05eee0256
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/SubmodelServiceTestComponent.java
@@ -0,0 +1,49 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka;
+
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration;
+import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Profile;
+
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+@SpringBootApplication(scanBasePackages = "org.eclipse.digitaltwin.basyx", exclude = { MongoAutoConfiguration.class,
+ MongoDataAutoConfiguration.class })
+@Profile("test-submodel")
+public class SubmodelServiceTestComponent {
+
+
+
+ @Bean
+ public Submodel submodel() {
+ return TestSubmodels.submodel();
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/TestShrinker.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/TestShrinker.java
new file mode 100644
index 000000000..88da81541
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/TestShrinker.java
@@ -0,0 +1,113 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka;
+
+import java.util.List;
+
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.SerializationException;
+import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultAnnotatedRelationshipElement;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultBlob;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultEntity;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultOperation;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultOperationVariable;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementCollection;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementList;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.BlobRemovingSubmodelShrinker;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka.events.model.DataPreservationLevel;
+import org.junit.Assert;
+import org.junit.Test;
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public class TestShrinker {
+
+ @Test
+ public void testRemoveBlobValue() throws SerializationException {
+ runTest(DataPreservationLevel.REMOVE_BLOB_VALUE);
+ }
+
+ private void runTest(DataPreservationLevel level) throws SerializationException {
+ SubmodelElement testValue = testInput();
+ SubmodelElement expected = expected(level);
+
+ BlobRemovingSubmodelShrinker shrinker = new BlobRemovingSubmodelShrinker();
+ SubmodelElement elem = shrinker.shrinkSubmodelElement(testValue);
+ Assert.assertEquals(expected, elem);
+ }
+
+ private SubmodelElement testInput() {
+ return data(true);
+ }
+
+ private SubmodelElement expected(DataPreservationLevel level) {
+ return data(level == DataPreservationLevel.RETAIN_FULL);
+ }
+
+ private SubmodelElement data(boolean withBlobValue) {
+ return new DefaultSubmodelElementList.Builder()
+ .value(new DefaultProperty.Builder().idShort("a").value("5").build())
+ .value(new DefaultEntity.Builder().idShort("b").statements(List.of()).build())
+ .value(new DefaultEntity.Builder().idShort("c")
+ .statements(new DefaultProperty.Builder().idShort("d").value("7").build()).build())
+ .value(new DefaultSubmodelElementCollection.Builder().idShort("e").build())
+ .value(new DefaultSubmodelElementCollection.Builder().idShort("f")
+ .value(new DefaultBlob.Builder().idShort("g")
+ .value(withBlobValue ? new byte[] { 0, 1, 2 } : null).build())
+ .value(new DefaultOperation.Builder().idShort("h")
+ .inputVariables(new DefaultOperationVariable.Builder()
+ .value(new DefaultProperty.Builder().idShort("i").value("77").build()).build())
+ .build())
+ .value(new DefaultAnnotatedRelationshipElement.Builder().idShort("j")
+ .annotations(withBlobValue ?
+ new DefaultBlob.Builder().idShort("k").value( new byte[] { 3, 4, 5 }).build()
+ : new DefaultBlob.Builder().idShort("k").build())
+ .build())
+ .value(new DefaultAnnotatedRelationshipElement.Builder().idShort("l").build())
+ .value(new DefaultOperation.Builder().idShort("m").build())
+ .value(new DefaultOperation.Builder().idShort("n")
+ .outputVariables(
+ new DefaultOperationVariable.Builder().value(
+ withBlobValue ?
+ new DefaultBlob.Builder().idShort("o").value(new byte[] { 3, 4, 5 }).build()
+ : new DefaultBlob.Builder().idShort("o").build())
+ .build()
+ ).build())
+ .value(new DefaultOperation.Builder().idShort("p")
+ .inoutputVariables(
+ new DefaultOperationVariable.Builder().value(
+ withBlobValue ?
+ new DefaultBlob.Builder().idShort("q").value(new byte[] { 3, 4, 5 }).build()
+ : new DefaultBlob.Builder().idShort("q").build())
+ .build()
+ ).build())
+ .build())
+ .value(new DefaultSubmodelElementList.Builder().idShort("r").value(List.of()).build())
+ .build();
+ }
+
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/TestSubmodels.java b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/TestSubmodels.java
new file mode 100644
index 000000000..d1f3ab89b
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-kafka/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/kafka/TestSubmodels.java
@@ -0,0 +1,68 @@
+/*******************************************************************************
+ * Copyright (C) 2024 DFKI GmbH (https://www.dfki.de/en/web)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ ******************************************************************************/
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.kafka;
+
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodel;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementCollection;
+/**
+ * @author geso02 (Sonnenberg DFKI GmbH)
+ */
+public class TestSubmodels {
+
+ public static final String IDSHORT_SM = "sm";
+ public static final String IDSHORT_PROP_0 = "prop_0";
+ public static final String IDSHORT_PROP_1 = "prop_1";
+ public static final String IDSHORT_PROP_TO_BE_REMOVED = "prop_toberemoved";
+ public static final String IDSHORT_COLL = "coll";
+ public static final String ID_SM = "http://sm.id/0";
+
+ private TestSubmodels() {
+
+ }
+
+ public static Submodel submodel() {
+ return TestSubmodels.createSubmodel(ID_SM, IDSHORT_PROP_0, "5");
+ }
+
+ public static Submodel createSubmodel(String smId, String smeId, String value) {
+ return new DefaultSubmodel.Builder().id(smId).idShort(IDSHORT_SM)
+ .submodelElements(submodelElement(smeId, value))
+ .submodelElements(new DefaultSubmodelElementCollection.Builder().idShort(IDSHORT_COLL).build())
+ .submodelElements(submodelElement(IDSHORT_PROP_TO_BE_REMOVED, "toBeRemoved"))
+ .build();
+ }
+
+ public static SubmodelElement submodelElement(String smeId, String value) {
+ return new DefaultProperty.Builder().idShort(smeId).value(value).build();
+ }
+
+ public static String path(String... idShorts) {
+ return String.join(".", idShorts);
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice.component/pom.xml b/basyx.submodelservice/basyx.submodelservice.component/pom.xml
index 02c5f3fee..a4eb86a45 100644
--- a/basyx.submodelservice/basyx.submodelservice.component/pom.xml
+++ b/basyx.submodelservice/basyx.submodelservice.component/pom.xml
@@ -29,6 +29,10 @@
org.eclipse.digitaltwin.basyx
basyx.submodelservice-feature-operation-dispatching
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelservice-feature-kafka
org.eclipse.digitaltwin.basyx
diff --git a/basyx.submodelservice/basyx.submodelservice.component/src/test/resources/application-integrationMongo.yml b/basyx.submodelservice/basyx.submodelservice.component/src/test/resources/application-integrationMongo.yml
index 41af54562..8430f538b 100644
--- a/basyx.submodelservice/basyx.submodelservice.component/src/test/resources/application-integrationMongo.yml
+++ b/basyx.submodelservice/basyx.submodelservice.component/src/test/resources/application-integrationMongo.yml
@@ -21,6 +21,7 @@ spring:
mongodb:
host: 127.0.0.1
port: 27017
+ collectionName: submodelServiceTestCollection
database: submodelservice
authentication-database: admin
username: mongoAdmin
diff --git a/basyx.submodelservice/pom.xml b/basyx.submodelservice/pom.xml
index 084c35dcf..01054a9c1 100644
--- a/basyx.submodelservice/pom.xml
+++ b/basyx.submodelservice/pom.xml
@@ -21,6 +21,7 @@
basyx.submodelservice-backend-inmemory
basyx.submodelservice-backend-mongodb
basyx.submodelservice-feature-mqtt
+ basyx.submodelservice-feature-kafka
basyx.submodelservice-feature-operation-dispatching
basyx.submodelservice.example
basyx.submodelservice.component
diff --git a/ci/docker-compose.yml b/ci/docker-compose.yml
index 10fecd1b9..628a83747 100644
--- a/ci/docker-compose.yml
+++ b/ci/docker-compose.yml
@@ -19,20 +19,6 @@ services:
networks:
- basyx-java-server-sdk
- zookeeper:
- image: confluentinc/cp-zookeeper:7.5.2
- environment:
- ZOOKEEPER_CLIENT_PORT: 2181
- ZOOKEEPER_TICK_TIME: 2000
- healthcheck:
- test: nc -z localhost 2181 || exit -1
- interval: 10s
- timeout: 5s
- retries: 10
- start_period: 10s
- networks:
- - basyx-java-server-sdk
-
akhq:
image: tchiotludo/akhq:0.24.0
container_name: akhq
@@ -42,7 +28,7 @@ services:
connections:
docker-kafka-server:
properties:
- bootstrap.servers: "kafka:29092"
+ bootstrap.servers: "kafka:29093"
ports:
- 8086:8080
restart: always
@@ -52,27 +38,38 @@ services:
- basyx-java-server-sdk
kafka:
- image: confluentinc/cp-kafka:7.5.2
+ image: confluentinc/cp-kafka:7.8.1
+ hostname: kafka
+ container_name: kafka
ports:
- - 9092:9092
+ - "9092:9092"
environment:
- KAFKA_BROKER_ID: 1
- KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
- KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
- KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
- KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
+ KAFKA_ADVERTISED_LISTENERS: INTER_BROKER://kafka:9094,PLAINTEXT://kafka:9093,EXTERNAL://localhost:9092
+ KAFKA_INTER_BROKER_LISTENER_NAME: INTER_BROKER
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
+ KAFKA_PROCESS_ROLES: broker,controller
+ KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
+ KAFKA_LISTENERS: INTER_BROKER://:9094,CONTROLLER://:9095,PLAINTEXT://:9093,EXTERNAL://:9092
+ KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,INTER_BROKER:PLAINTEXT,PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT
+ KAFKA_BROKER_ID: 1
+ KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9095
+ ALLOW_PLAINTEXT_LISTENER: 'yes'
+ KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'
+ CLUSTER_ID: jmpccZs2RHaYUbZ-LgaIhQ
+ KAFKA_LOG_DIRS: /var/lib/kafka/data
+ KAFKA_MIN_INSYNC_REPLICAS: 1
+ KAFKA_NUM_PARTITIONS: 1
+ KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
+ KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
healthcheck:
- test: nc -z localhost 9092 || exit -1
+ test: ["CMD", "bash", "-c", "echo > /dev/tcp/kafka/9093"]
interval: 5s
timeout: 10s
retries: 10
start_period: 15s
- depends_on:
- zookeeper:
- condition: service_healthy
networks:
- basyx-java-server-sdk
+
aas-registry-log-mem:
image: eclipsebasyx/aas-registry-log-mem:$BASYX_VERSION
diff --git a/examples/BaSyxKafka/.env b/examples/BaSyxKafka/.env
new file mode 100644
index 000000000..cdf0b7a5d
--- /dev/null
+++ b/examples/BaSyxKafka/.env
@@ -0,0 +1,4 @@
+BASYX_VERSION=2.0.0-SNAPSHOT
+AAS_WEBUI_VERSION=SNAPSHOT
+KAFKA_VERSION=7.7.1
+AKHQ_VERSION=0.25.1
\ No newline at end of file
diff --git a/examples/BaSyxKafka/README.md b/examples/BaSyxKafka/README.md
new file mode 100644
index 000000000..cc4909298
--- /dev/null
+++ b/examples/BaSyxKafka/README.md
@@ -0,0 +1,64 @@
+# 🌟 BaSyx Kafka Events Example Setup
+
+This guide will help you set up and run the BaSyx Kafka Events Example using Docker. Make sure Docker is installed on your system before proceeding.
+
+---
+
+## 🚀 Getting Started
+
+### ✅ Prerequisites
+Ensure you have **Docker** installed and running on your device.
+
+---
+
+## 📦 How to Start the Example Containers
+
+1. **Open a terminal** in this folder.
+2. **Start the BaSyx containers** with the following command:
+ ```bash
+ docker compose up -d
+ ```
+ This will launch all required services in detached mode.
+
+---
+
+## 🌐 Accessing the BaSyx Containers
+
+| Service | URL |
+|--------------------------|------------------------------------------|
+| **AKHQ Web GUI** | [http://localhost:8101](http://localhost:8101) |
+| **AAS Registry** | [http://localhost:8102/shell-descriptors](http://localhost:8102/aas-descriptors) |
+| **Submodel Registry** | [http://localhost:8103/submodel-descriptors](http://localhost:8103/submodel-descriptors) |
+| **AAS Repository** | [http://localhost:8104/shells](http://localhost:8104/shells) |
+| **Submodel Repository** | [http://localhost:8104/submodels](http://localhost:8104/submodels) |
+| **Submodel Service** | [http://localhost:8105/submodel](http://localhost:8105/submodel) |
+| **AAS Web GUI** | [http://localhost:8106](http://localhost:8106) |
+
+---
+
+## 📖 Usage Instructions
+
+1. **Open the [AKHQ GUI](http://localhost:8101)** and verify that the events triggered by the AASX file deployment are successfully delivered.
+2. **Monitor events** using the Kafka CLI Consumer:
+ ```bash
+ docker logs -f consumer-aas-example
+ ```
+ More information about the Kafka CLI Consumer can be found in the [Confluent Documentation](https://docs.confluent.io/kafka/operations-tools/kafka-tools.html#kafka-console-consumer-sh).
+
+---
+
+## ⚠️ Important Notice
+
+The **Registry events** only handle descriptor updates for registration operations. For a more comprehensive view, including updates and detailed information, refer to the **Repository events**.
+
+---
+
+## 📌 Tear Down
+
+To stop and remove the BaSyx containers:
+
+1. **Open a terminal** in this folder.
+2. **Shut down the containers** with:
+ ```bash
+ docker compose down
+ ```
diff --git a/examples/BaSyxKafka/aas/FrameAAS.aasx b/examples/BaSyxKafka/aas/FrameAAS.aasx
new file mode 100644
index 000000000..9bbcb4d5d
Binary files /dev/null and b/examples/BaSyxKafka/aas/FrameAAS.aasx differ
diff --git a/examples/BaSyxKafka/aas/GearAAS.aasx b/examples/BaSyxKafka/aas/GearAAS.aasx
new file mode 100644
index 000000000..eaf949ba9
Binary files /dev/null and b/examples/BaSyxKafka/aas/GearAAS.aasx differ
diff --git a/examples/BaSyxKafka/docker-compose.yaml b/examples/BaSyxKafka/docker-compose.yaml
new file mode 100644
index 000000000..58c3751f9
--- /dev/null
+++ b/examples/BaSyxKafka/docker-compose.yaml
@@ -0,0 +1,194 @@
+networks:
+ basyx-network:
+ driver: bridge
+ kafka-network:
+ driver: bridge
+
+services:
+
+ kafka-example:
+ image: confluentinc/cp-kafka:${KAFKA_VERSION}
+ hostname: kafka
+ container_name: kafka-example
+ ports:
+ - 9093:9093
+ environment:
+ KAFKA_ADVERTISED_LISTENERS: INTER_BROKER://kafka:9094,PLAINTEXT://kafka:9092,EXTERNAL://localhost:9093
+ KAFKA_INTER_BROKER_LISTENER_NAME: INTER_BROKER
+ KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
+ KAFKA_PROCESS_ROLES: broker,controller
+ KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
+ KAFKA_LISTENERS: INTER_BROKER://:9094,CONTROLLER://:9095,PLAINTEXT://:9092,EXTERNAL://:9093
+ KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,INTER_BROKER:PLAINTEXT,PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT
+ KAFKA_BROKER_ID: 1
+ KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9095
+ ALLOW_PLAINTEXT_LISTENER: 'yes'
+ KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'
+ CLUSTER_ID: jmpccZs2RHaYUbZ-LgaIhQ
+ KAFKA_LOG_DIRS: /var/lib/kafka/data
+ KAFKA_MIN_INSYNC_REPLICAS: 1
+ KAFKA_NUM_PARTITIONS: 1
+ KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
+ KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
+ networks:
+ - kafka-network
+ healthcheck:
+ test: ["CMD", "bash", "-c", "echo > /dev/tcp/kafka/9092"]
+ interval: 5s
+ timeout: 10s
+ retries: 10
+ start_period: 15s
+
+ akhq-example:
+ image: tchiotludo/akhq:${AKHQ_VERSION}
+ container_name: akhq-example
+ restart: always
+ ports:
+ - 8101:8080
+ environment:
+ AKHQ_CONFIGURATION: |
+ akhq:
+ connections:
+ docker-kafka-server:
+ properties:
+ bootstrap.servers: "kafka:9092"
+ depends_on:
+ - kafka-example
+ networks:
+ - kafka-network
+
+ consumer-aas-example:
+ image: confluentinc/cp-kafka:7.8.1
+ container_name: consumer-aas-example
+ depends_on:
+ - kafka-example
+ networks:
+ - kafka-network
+ entrypoint: >
+ sh -c "kafka-console-consumer --bootstrap-server kafka:9092 --include 'aas-registry|submodel-registry|aas-events|submodel-events' --from-beginning --group console-consumer-group"
+
+ aas-registry-example:
+ image: eclipsebasyx/aas-registry-kafka-mem:${BASYX_VERSION}
+ hostname: aas-registry
+ container_name: aas-registry-example
+ restart: always
+ ports:
+ - 8102:8080
+ environment:
+ KAFKA_BOOTSTRAP_SERVERS: PLAINTEXT://kafka:9092
+ BASYX_CORS_ALLOWED_ORIGINS: '*'
+ BASYX_CORS_ALLOWED_METHODS: GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD
+ depends_on:
+ - kafka-example
+ networks:
+ - basyx-network
+ - kafka-network
+
+ submodel-registry-example:
+ image: eclipsebasyx/submodel-registry-kafka-mem:${BASYX_VERSION}
+ hostname: submodel-registry
+ container_name: submodel-registry-example
+ restart: always
+ ports:
+ - 8103:8080
+ environment:
+ KAFKA_BOOTSTRAP_SERVERS: PLAINTEXT://kafka:9092
+ BASYX_CORS_ALLOWED_ORIGINS: '*'
+ BASYX_CORS_ALLOWED_METHODS: GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD
+ depends_on:
+ - kafka-example
+ networks:
+ - basyx-network
+ - kafka-network
+
+ aas-environment-example:
+ image: eclipsebasyx/aas-environment:${BASYX_VERSION}
+ hostname: aas-environment
+ container_name: aas-environment-example
+ restart: always
+ ports:
+ - 8104:8081
+ environment:
+
+ BASYX_AASREPOSITORY_FEATURE_REGISTRYINTEGRATION: http://aas-registry:8080
+ BASYX_SUBMODELREPOSITORY_FEATURE_REGISTRYINTEGRATION: http://submodel-registry:8080
+ BASYX_AASREPOSITORY_FEATURE_AASXUPLOAD_ENABLED: "true"
+ BASYX_CORS_ALLOWED_ORIGINS: '*'
+ BASYX_ENVIRONMENT: file:/application/aas/
+ BASYX_CORS_ALLOWED_METHODS: GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD
+ BASYX_EXTERNALURL: http://localhost:8104,http://aas-environment:8081
+
+ ### Kafka specific settings ###
+
+ # Enable kafka
+ # also BASYX_SUBMODELREPOSITORY_FEATURE_KAFKA_ENABLED and BASYX_AASREPOSITORY_FEATURE_KAFKA_ENABLED could be set to true
+ BASYX_FEATURE_KAFKA_ENABLED: "true"
+ # set the broker references (docker intern)
+ SPRING_KAFKA_BOOTSTRAP_SERVERS: PLAINTEXT://kafka:9092
+ # topic name -> defaults to submodel-events
+ BASYX_SUBMODELREPOSITORY_FEATURE_KAFKA_TOPIC_NAME: sm-repo-events
+ # topic name -> defaults to aas-events
+ BASYX_AASREPOSITORY_FEATURE_KAFKA_TOPIC_NAME: aas-repo-events
+
+ volumes:
+ - ./aas:/application/aas
+ depends_on:
+ aas-registry-example:
+ condition: service_healthy
+ submodel-registry-example:
+ condition: service_healthy
+ networks:
+ - basyx-network
+ - kafka-network
+
+ submodel-service-example:
+ image: eclipsebasyx/submodel-service:${BASYX_VERSION}
+ hostname: submodel-service
+ container_name: submodel-service-example
+ restart: always
+ environment:
+ BASYX_SUBMODELSERVICE_SUBMODEL_FILE: submodel.json
+ BASYX_SUBMODELSERVICE_FEATURE_OPERATION_DISPATCHER_ENABLED: "true"
+ ### Kafka specific settings ###
+
+ # Enable kafka
+ # also BASYX_SUBMODELSERVICE_FEATURE_KAFKA_ENABLED could be set to true
+ BASYX_SUBMODELSERVICE_FEATURE_KAFKA_ENABLED: "true"
+ BASYX_FEATURE_KAFKA_ENABLED: "true"
+ # set the broker references (docker intern)
+ SPRING_KAFKA_BOOTSTRAP_SERVERS: PLAINTEXT://kafka:9092
+ # topic name -> defaults to submodel-events
+ BASYX_SUBMODELSERVICE_FEATURE_KAFKA_TOPIC_NAME: sm-service-events
+ # notify on startup and teardown events
+ BASYX_SUBMODELSERVICE_FEATURE_KAFKA_SUBMODELEVENTS: true
+
+ ports:
+ - 8105:8081
+ volumes:
+ - ./submodel.json:/application/submodel.json:ro
+ networks:
+ - basyx-network
+ - kafka-network
+
+ aas-gui-example:
+ image: eclipsebasyx/aas-gui:${AAS_WEBUI_VERSION}
+ container_name: aas-gui-example
+ restart: always
+ ports:
+ - 8106:3000
+ environment:
+ CHOKIDAR_USEPOLLING: "true"
+ AAS_REGISTRY_PATH: http://localhost:8102
+ SUBMODEL_REGISTRY_PATH: http://localhost:8103
+ AAS_REPO_PATH: http://localhost:8104/shells
+ SUBMODEL_REPO_PATH: http://localhost:8104/submodels
+ CD_REPO_PATH: http://localhost:8104/concept-descriptions
+ networks:
+ - basyx-network
+ depends_on:
+ aas-registry-example:
+ condition: service_healthy
+ submodel-registry-example:
+ condition: service_healthy
+ aas-environment-example:
+ condition: service_healthy
diff --git a/examples/BaSyxKafka/submodel.json b/examples/BaSyxKafka/submodel.json
new file mode 100644
index 000000000..28485d4c6
--- /dev/null
+++ b/examples/BaSyxKafka/submodel.json
@@ -0,0 +1,6 @@
+{
+ "modelType": "Submodel",
+ "id": "http://aas.basyx.eclipse.org/sm/Submodel1",
+ "idShort": "Submodel1",
+ "kind": "Instance"
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index f00e3b01a..3366ced88 100644
--- a/pom.xml
+++ b/pom.xml
@@ -491,6 +491,18 @@
basyx.submodelservice-backend-inmemory
${revision}
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelservice-feature-kafka
+ ${revision}
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelservice-feature-kafka
+ tests
+ test
+ ${revision}
+
org.eclipse.digitaltwin.basyx
basyx.submodelservice-backend-mongodb
@@ -646,6 +658,30 @@
basyx.aasrepository-feature-mqtt
${revision}
+
+ org.eclipse.digitaltwin.basyx
+ basyx.aasrepository-feature-kafka
+ ${revision}
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.aasrepository-feature-kafka
+ ${revision}
+ tests
+ test
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelrepository-feature-kafka
+ ${revision}
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelrepository-feature-kafka
+ ${revision}
+ tests
+ test
+
org.eclipse.digitaltwin.basyx
basyx.aasrepository-feature-registry-integration