From b34d232befa2920e214332a2e1d260d21d2a93b9 Mon Sep 17 00:00:00 2001 From: Masud Rana <129751857+masud-svg@users.noreply.github.com> Date: Tue, 27 Aug 2024 12:58:05 +0200 Subject: [PATCH] MQTT for SubmodelService (#339) * submodelservice-feature-mqtt added Signed-off-by: rana * submodelservice-feature-mqtt read.me file removed Signed-off-by: rana * submodelservice-feature-mqtt restructred Signed-off-by: rana * submodelservice-feature-mqtt changed according to feedback Signed-off-by: rana * submodelservice-feature-mqtt code-format changed Signed-off-by: rana * submodelservice-feature-mqtt pom.xml chnaged Signed-off-by: rana --------- Signed-off-by: rana --- .../pom.xml | 50 +++++ .../feature/mqtt/MqttSubmodelService.java | 192 ++++++++++++++++++ .../MqttSubmodelServiceConfiguration.java | 68 +++++++ .../mqtt/MqttSubmodelServiceFactory.java | 54 +++++ .../mqtt/MqttSubmodelServiceFeature.java | 81 ++++++++ .../mqtt/MqttSubmodelServiceTopicFactory.java | 83 ++++++++ .../feature/mqtt/MqttTestListener.java | 97 +++++++++ .../mqtt/TestMqttSubmodelObserver.java | 191 +++++++++++++++++ .../src/test/resources/config/moquette.conf | 6 + basyx.submodelservice/pom.xml | 1 + 10 files changed, 823 insertions(+) create mode 100644 basyx.submodelservice/basyx.submodelservice-feature-mqtt/pom.xml create mode 100644 basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelService.java create mode 100644 basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceConfiguration.java create mode 100644 basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceFactory.java create mode 100644 basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceFeature.java create mode 100644 basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceTopicFactory.java create mode 100644 basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/MqttTestListener.java create mode 100644 basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/TestMqttSubmodelObserver.java create mode 100644 basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/resources/config/moquette.conf diff --git a/basyx.submodelservice/basyx.submodelservice-feature-mqtt/pom.xml b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/pom.xml new file mode 100644 index 000000000..aa75f6c5d --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/pom.xml @@ -0,0 +1,50 @@ + + 4.0.0 + + org.eclipse.digitaltwin.basyx + basyx.submodelservice + ${revision} + + basyx.submodelservice-feature-mqtt + BaSyx submodelservice-feature-mqtt + BaSyx submodelservice-feature-mqtt + + + + org.eclipse.digitaltwin.basyx + basyx.submodelservice-core + + + org.eclipse.digitaltwin.basyx + basyx.mqttcore + + + org.eclipse.digitaltwin.basyx + basyx.submodelservice-core + tests + test + + + org.eclipse.digitaltwin.basyx + basyx.submodelservice-backend-inmemory + test + + + org.springframework.boot + spring-boot-starter + + + io.moquette + moquette-broker + test + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + + + org.eclipse.digitaltwin.basyx + basyx.submodelservice-backend-inmemory + + + \ No newline at end of file diff --git a/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelService.java b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelService.java new file mode 100644 index 000000000..69e41f74c --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelService.java @@ -0,0 +1,192 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * 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.mqtt; + +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.common.mqttcore.serializer.SubmodelElementSerializer; +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.value.SubmodelElementValue; +import org.eclipse.paho.client.mqttv3.IMqttClient; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.MqttPersistenceException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Service decorator for the MQTT eventing on the submodel level. + * + * @author rana + */ +public class MqttSubmodelService implements SubmodelService { + + private static Logger logger = LoggerFactory.getLogger(MqttSubmodelService.class); + private MqttSubmodelServiceTopicFactory topicFactory; + private SubmodelService decorated; + private IMqttClient mqttClient; + + public MqttSubmodelService(SubmodelService decorated, IMqttClient mqttClient, MqttSubmodelServiceTopicFactory topicFactory) { + this.topicFactory = topicFactory; + this.decorated = decorated; + this.mqttClient = mqttClient; + } + + @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); + submodelElementUpdated(submodelElement, idShortPath); + } + + @Override + public void createSubmodelElement(SubmodelElement submodelElement) { + decorated.createSubmodelElement(submodelElement); + SubmodelElement smElement = decorated.getSubmodelElement(submodelElement.getIdShort()); + submodelElementCreated(submodelElement, smElement.getIdShort()); + } + + @Override + public void createSubmodelElement(String idShortPath, SubmodelElement submodelElement) throws ElementDoesNotExistException { + + decorated.createSubmodelElement(idShortPath, submodelElement); + + SubmodelElement smElement = decorated.getSubmodelElement(submodelElement.getIdShort()); + submodelElementCreated(smElement, idShortPath); + } + + @Override + public void updateSubmodelElement(String idShortPath, SubmodelElement submodelElement) throws ElementDoesNotExistException { + + decorated.updateSubmodelElement(idShortPath, submodelElement); + SubmodelElement smElement = decorated.getSubmodelElement(submodelElement.getIdShort()); + submodelElementUpdated(smElement, submodelElement.getIdShort()); + } + + @Override + public void deleteSubmodelElement(String idShortPath) throws ElementDoesNotExistException { + + SubmodelElement smElement = decorated.getSubmodelElement(idShortPath); + decorated.deleteSubmodelElement(idShortPath); + submodelElementDeleted(smElement, idShortPath); + } + + @Override + public void patchSubmodelElements(List submodelElementList) { + decorated.patchSubmodelElements(submodelElementList); + } + + @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); + } + + private void submodelElementCreated(SubmodelElement submodelElement, String idShort) { + sendMqttMessage(topicFactory.createCreateSubmodelElementTopic(idShort), SubmodelElementSerializer.serializeSubmodelElement(submodelElement)); + } + + private void submodelElementUpdated(SubmodelElement submodelElement, String idShortPath) { + sendMqttMessage(topicFactory.createUpdateSubmodelElementTopic(idShortPath), SubmodelElementSerializer.serializeSubmodelElement(submodelElement)); + } + + private void submodelElementDeleted(SubmodelElement submodelElement, String idShort) { + sendMqttMessage(topicFactory.createDeleteSubmodelElementTopic(idShort), SubmodelElementSerializer.serializeSubmodelElement(submodelElement)); + } + + /** + * Sends MQTT message to connected broker + * + * @param topic + * in which the message will be published + * @param payload + * the actual message + */ + private void sendMqttMessage(String topic, String payload) { + MqttMessage msg = createMqttMessage(payload); + + try { + logger.debug("Send MQTT message to " + topic + ": " + payload); + mqttClient.publish(topic, msg); + } catch (MqttPersistenceException e) { + logger.error("Could not persist mqtt message", e); + } catch (MqttException e) { + logger.error("Could not send mqtt message", e); + } + } + + private MqttMessage createMqttMessage(String payload) { + if (payload == null) { + return new MqttMessage(); + } else { + return new MqttMessage(payload.getBytes()); + } + } +} diff --git a/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceConfiguration.java b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceConfiguration.java new file mode 100644 index 000000000..1984dfb94 --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceConfiguration.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * 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.mqtt; + +import org.eclipse.paho.client.mqttv3.IMqttClient; +import org.eclipse.paho.client.mqttv3.MqttClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * MQTT configuration to allow for the automatic enable of the feature using the + * config file. + * + * @author rana + */ +@ConditionalOnExpression("#{${" + MqttSubmodelServiceFeature.FEATURENAME + ".enabled:false} or ${basyx.feature.mqtt.enabled:false}}") +@Configuration +public class MqttSubmodelServiceConfiguration { + + @ConditionalOnMissingBean + @Bean + public IMqttClient mqttClient(@Value("${mqtt.clientId}") String clientId, @Value("${mqtt.hostname}") String hostname, @Value("${mqtt.port}") int port) throws MqttException { + IMqttClient mqttClient = new MqttClient("tcp://" + hostname + ":" + port, clientId, new MemoryPersistence()); + + mqttClient.connect(mqttConnectOptions()); + + return mqttClient; + } + + @ConditionalOnMissingBean + @Bean + @ConfigurationProperties(prefix = "mqtt") + public MqttConnectOptions mqttConnectOptions() { + MqttConnectOptions mqttConceptOptions = new MqttConnectOptions(); + mqttConceptOptions.setAutomaticReconnect(true); + return mqttConceptOptions; + } +} diff --git a/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceFactory.java b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceFactory.java new file mode 100644 index 000000000..9e3a0f1ca --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceFactory.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * 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.mqtt; + +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.paho.client.mqttv3.IMqttClient; + +/** + * Service factory for the MQTT eventing on the submodel level. + * + * @author rana + */ +public class MqttSubmodelServiceFactory implements SubmodelServiceFactory { + + private SubmodelServiceFactory decorated; + private IMqttClient client; + private MqttSubmodelServiceTopicFactory topicFactory; + + public MqttSubmodelServiceFactory(SubmodelServiceFactory decorated, IMqttClient client, MqttSubmodelServiceTopicFactory topicFactory) { + this.decorated = decorated; + this.client = client; + this.topicFactory = topicFactory; + } + + @Override + public SubmodelService create(Submodel submodel) { + return new MqttSubmodelService(decorated.create(submodel), client, topicFactory); + } +} diff --git a/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceFeature.java b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceFeature.java new file mode 100644 index 000000000..7e056450b --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceFeature.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * 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.mqtt; + +import org.eclipse.digitaltwin.basyx.common.mqttcore.encoding.Base64URLEncoder; +import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelServiceFactory; +import org.eclipse.digitaltwin.basyx.submodelservice.feature.SubmodelServiceFeature; +import org.eclipse.paho.client.mqttv3.IMqttClient; +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; + +/** + * Service feature for the MQTT eventing on the submodel level. + * + * @author rana + */ +@ConditionalOnExpression("#{${" + MqttSubmodelServiceFeature.FEATURENAME + ".enabled:false} or ${basyx.feature.mqtt.enabled:false}}") +@Component +public class MqttSubmodelServiceFeature implements SubmodelServiceFeature { + + public final static String FEATURENAME = "basyx.submodelservice.feature.mqtt"; + + @Value("#{${" + FEATURENAME + ".enabled:false} or ${basyx.feature.mqtt.enabled:false}}") + private boolean enabled; + + private IMqttClient mqttClient; + + @Autowired + public MqttSubmodelServiceFeature(IMqttClient mqttClient) { + this.mqttClient = mqttClient; + } + + @Override + public SubmodelServiceFactory decorate(SubmodelServiceFactory component) { + return new MqttSubmodelServiceFactory(component, mqttClient, new MqttSubmodelServiceTopicFactory(new Base64URLEncoder())); + } + + @Override + public void initialize() { + } + + @Override + public void cleanUp() { + + } + + @Override + public String getName() { + return "SubmodelService MQTT"; + } + + @Override + public boolean isEnabled() { + return enabled; + } +} diff --git a/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceTopicFactory.java b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceTopicFactory.java new file mode 100644 index 000000000..31296f4d0 --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceTopicFactory.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * 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.mqtt; + +import java.util.StringJoiner; + +import org.eclipse.digitaltwin.basyx.common.mqttcore.AbstractMqttTopicFactory; +import org.eclipse.digitaltwin.basyx.common.mqttcore.encoding.Encoder; + +/** + * MQTT topic factory for the eventing on the submodel level. + * + * @author rana + */ +public class MqttSubmodelServiceTopicFactory extends AbstractMqttTopicFactory { + + private static final String SUBMODEL_SERVICE = "sm-service"; + private static final String SUBMODELS = "submodels"; + private static final String CREATED = "created"; + private static final String UPDATED = "updated"; + private static final String DELETED = "deleted"; + private static final String SUBMODEL_ELEMENTS = "submodelElements"; + + /** + * Used for encoding the idShort + * + * @param encoder + */ + public MqttSubmodelServiceTopicFactory(Encoder encoder) { + super(encoder); + } + + /** + * Creates the hierarchical topic for the create event of submodelElements + * + * @param idShort + */ + public String createCreateSubmodelElementTopic(String idShort) { + return new StringJoiner("/", "", "").add(SUBMODEL_SERVICE).add(SUBMODELS).add(SUBMODEL_ELEMENTS).add(idShort).add(CREATED).toString(); + } + + /** + * Creates the hierarchical topic for the update event of submodelElements + * + * @param idShort + */ + public String createUpdateSubmodelElementTopic(String idShort) { + + return new StringJoiner("/", "", "").add(SUBMODEL_SERVICE).add(SUBMODELS).add(SUBMODEL_ELEMENTS).add(idShort).add(UPDATED).toString(); + } + + /** + * Creates the hierarchical topic for the delete event of submodelElements + * + * @param idShort + */ + public String createDeleteSubmodelElementTopic(String idShort) { + return new StringJoiner("/", "", "").add(SUBMODEL_SERVICE).add(SUBMODELS).add(SUBMODEL_ELEMENTS).add(idShort).add(DELETED).toString(); + } +} diff --git a/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/MqttTestListener.java b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/MqttTestListener.java new file mode 100644 index 000000000..c7cc88b3b --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/MqttTestListener.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * 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.mqtt; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; + +import io.moquette.interception.InterceptHandler; +import io.moquette.interception.messages.InterceptAcknowledgedMessage; +import io.moquette.interception.messages.InterceptConnectMessage; +import io.moquette.interception.messages.InterceptConnectionLostMessage; +import io.moquette.interception.messages.InterceptDisconnectMessage; +import io.moquette.interception.messages.InterceptPublishMessage; +import io.moquette.interception.messages.InterceptSubscribeMessage; +import io.moquette.interception.messages.InterceptUnsubscribeMessage; + +/** + * Very simple MQTT broker listener for testing API events. Stores the last + * received event and makes its topic and payload available for reading. + * + * @author espen + * + */ +public class MqttTestListener implements InterceptHandler { + // Topic and payload of the most recent event + public String lastTopic; + public String lastPayload; + private ArrayList topics = new ArrayList<>(); + + @Override + public String getID() { + return null; + } + + @Override + public Class[] getInterceptedMessageTypes() { + return null; + } + + @Override + public void onConnect(InterceptConnectMessage arg0) { + } + + @Override + public void onConnectionLost(InterceptConnectionLostMessage arg0) { + } + + @Override + public void onDisconnect(InterceptDisconnectMessage arg0) { + } + + @Override + public void onMessageAcknowledged(InterceptAcknowledgedMessage arg0) { + } + + @Override + public synchronized void onPublish(InterceptPublishMessage msg) { + topics.add(msg.getTopicName()); + lastTopic = msg.getTopicName(); + lastPayload = msg.getPayload().toString(StandardCharsets.UTF_8); + } + + @Override + public void onSubscribe(InterceptSubscribeMessage arg0) { + } + + @Override + public void onUnsubscribe(InterceptUnsubscribeMessage arg0) { + } + + public ArrayList getTopics() { + return topics; + } +} diff --git a/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/TestMqttSubmodelObserver.java b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/TestMqttSubmodelObserver.java new file mode 100644 index 000000000..75eab2b75 --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/TestMqttSubmodelObserver.java @@ -0,0 +1,191 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * 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.mqtt; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.DeserializationException; +import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonDeserializer; +import org.eclipse.digitaltwin.aas4j.v3.model.Property; +import org.eclipse.digitaltwin.aas4j.v3.model.Qualifier; +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.DefaultQualifier; +import org.eclipse.digitaltwin.basyx.common.mqttcore.encoding.Base64URLEncoder; +import org.eclipse.digitaltwin.basyx.common.mqttcore.serializer.SubmodelElementSerializer; +import org.eclipse.digitaltwin.basyx.core.filerepository.InMemoryFileRepository; +import org.eclipse.digitaltwin.basyx.submodelservice.DummySubmodelFactory; +import org.eclipse.digitaltwin.basyx.submodelservice.InMemorySubmodelServiceFactory; +import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelService; +import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelServiceFactory; +import org.eclipse.digitaltwin.basyx.submodelservice.feature.mqtt.MqttSubmodelServiceFactory; +import org.eclipse.digitaltwin.basyx.submodelservice.feature.mqtt.MqttSubmodelServiceTopicFactory; +import org.eclipse.digitaltwin.basyx.submodelservice.value.PropertyValue; +import org.eclipse.digitaltwin.basyx.submodelservice.value.SubmodelElementValue; +import org.eclipse.paho.client.mqttv3.MqttClient; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttSecurityException; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import io.moquette.broker.Server; +import io.moquette.broker.config.ClasspathResourceLoader; +import io.moquette.broker.config.IConfig; +import io.moquette.broker.config.IResourceLoader; +import io.moquette.broker.config.ResourceLoaderConfig; + +/** + * Tests events for submodelElements in SM Service + * + * @author rana + */ +public class TestMqttSubmodelObserver { + + private static Server mqttBroker; + private static MqttClient mqttClient; + private static MqttTestListener listener; + private static MqttSubmodelServiceTopicFactory topicFactory = new MqttSubmodelServiceTopicFactory(new Base64URLEncoder()); + private static SubmodelService submodelService; + + @BeforeClass + public static void setUpClass() throws MqttException, IOException { + mqttBroker = startBroker(); + + listener = configureInterceptListener(mqttBroker); + + mqttClient = createAndConnectClient(); + + submodelService = createMqttSubmodelService(mqttClient); + } + + @AfterClass + public static void tearDownClass() { + mqttBroker.removeInterceptHandler(listener); + mqttBroker.stopServer(); + } + + @Test + public void createSubmodelElementEvent() throws DeserializationException { + + SubmodelElement submodelElement = createSubmodelElementDummy("createSubmodelElementEventId"); + submodelService.createSubmodelElement(submodelElement); + + assertEquals(topicFactory.createCreateSubmodelElementTopic(submodelElement.getIdShort()), listener.lastTopic); + assertEquals(submodelElement, deserializeSubmodelElementPayload(listener.lastPayload)); + } + + @Test + public void updateSubmodelElementEvent() throws DeserializationException { + + SubmodelElement submodelElement = createSubmodelElementDummy("updateSubmodelElementEventId"); + submodelService.createSubmodelElement(submodelElement); + + SubmodelElementValue value = new PropertyValue("updatedValue"); + submodelService.setSubmodelElementValue(submodelElement.getIdShort(), value); + + assertEquals(topicFactory.createUpdateSubmodelElementTopic(submodelElement.getIdShort()), listener.lastTopic); + assertEquals(submodelElement, deserializeSubmodelElementPayload(listener.lastPayload)); + } + + @Test + public void deleteSubmodelElementEvent() throws DeserializationException { + + SubmodelElement submodelElement = createSubmodelElementDummy("deleteSubmodelElementEventId"); + submodelService.createSubmodelElement(submodelElement); + submodelService.deleteSubmodelElement(submodelElement.getIdShort()); + + assertEquals(topicFactory.createDeleteSubmodelElementTopic(submodelElement.getIdShort()), listener.lastTopic); + assertEquals(submodelElement, deserializeSubmodelElementPayload(listener.lastPayload)); + } + + @Test + public void createSubmodelElementWithoutValueEvent() throws DeserializationException { + + SubmodelElement submodelElement = createSubmodelElementDummy("noValueSubmodelElementEventId"); + List qualifierList = createNoValueQualifierList(); + submodelElement.setQualifiers(qualifierList); + submodelService.createSubmodelElement(submodelElement); + + assertEquals(topicFactory.createCreateSubmodelElementTopic(submodelElement.getIdShort()), listener.lastTopic); + assertNotEquals(submodelElement, deserializeSubmodelElementPayload(listener.lastPayload)); + + ((Property) submodelElement).setValue(null); + assertEquals(submodelElement, deserializeSubmodelElementPayload(listener.lastPayload)); + } + + private List createNoValueQualifierList() { + + Qualifier emptyValueQualifier = new DefaultQualifier.Builder().type(SubmodelElementSerializer.EMPTYVALUEUPDATE_TYPE).value("true").build(); + return Arrays.asList(emptyValueQualifier); + } + + private static SubmodelElement deserializeSubmodelElementPayload(String payload) throws DeserializationException { + + return new JsonDeserializer().read(payload, SubmodelElement.class); + } + + private SubmodelElement createSubmodelElementDummy(String submodelElementId) { + return new DefaultProperty.Builder().idShort(submodelElementId).value("defaultValue").build(); + } + + private static SubmodelService createMqttSubmodelService(MqttClient client) { + + SubmodelServiceFactory repoFactory = new InMemorySubmodelServiceFactory(new InMemoryFileRepository()); + return new MqttSubmodelServiceFactory(repoFactory, client, new MqttSubmodelServiceTopicFactory(new Base64URLEncoder())).create(DummySubmodelFactory.createSubmodelWithAllSubmodelElements()); + } + + private static MqttTestListener configureInterceptListener(Server broker) { + + MqttTestListener testListener = new MqttTestListener(); + broker.addInterceptHandler(testListener); + + return testListener; + } + + private static MqttClient createAndConnectClient() throws MqttException, MqttSecurityException { + + MqttClient client = new MqttClient("tcp://localhost:1884", "testClient"); + client.connect(); + return client; + } + + private static Server startBroker() throws IOException { + + Server broker = new Server(); + IResourceLoader classpathLoader = new ClasspathResourceLoader(); + + IConfig classPathConfig = new ResourceLoaderConfig(classpathLoader); + broker.startServer(classPathConfig); + + return broker; + } +} diff --git a/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/resources/config/moquette.conf b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/resources/config/moquette.conf new file mode 100644 index 000000000..8511afcd1 --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/resources/config/moquette.conf @@ -0,0 +1,6 @@ +# Moquette Java Broker configuration file for testing + +# Do not use the default 1883 port +port 1884 +host 0.0.0.0 +allow_anonymous true \ No newline at end of file diff --git a/basyx.submodelservice/pom.xml b/basyx.submodelservice/pom.xml index dc199230c..f5916f017 100644 --- a/basyx.submodelservice/pom.xml +++ b/basyx.submodelservice/pom.xml @@ -18,6 +18,7 @@ basyx.submodelservice-core basyx.submodelservice-http basyx.submodelservice-backend-inmemory + basyx.submodelservice-feature-mqtt basyx.submodelservice.example basyx.submodelservice-client