Repository: apache/streampipes
Pull request #2481 (2024-02-21 00:18:46)

chore(deps): bump org.apache.commons:commons-compress from 1.25.0 to 1.26.0
Bumps org.apache.commons:commons-compress from 1.25.0 to 1.26.0.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.apache.commons:commons-compress&package-manager=maven&previous-version=1.25.0&new-version=1.26.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/apache/streampipes/network/alerts).

</details>


Patch:
diff --git a/VULNERABILITY.md b/VULNERABILITY.md
index 4c34c8dd25..7cd9e5b27b 100644
--- a/VULNERABILITY.md
+++ b/VULNERABILITY.md
@@ -2,5 +2,13 @@
 | OSV URL | CVSS | Ecosystem | Package | Version | Source |
 | --- | --- | --- | --- | --- | --- |
 | https://osv.dev/GHSA-6mjq-h674-j845 | 6.5 | Maven | io.netty:netty-handler | 4.1.72.Final | pom.xml |
+| https://osv.dev/GHSA-4265-ccf5-phj5 | 7.5 | Maven | org.apache.commons:commons-compress | 1.25.0 | pom.xml |
+| https://osv.dev/GHSA-4g9r-vxhx-9pgx | 8.1 | Maven | org.apache.commons:commons-compress | 1.25.0 | pom.xml |
+| https://osv.dev/GHSA-24rp-q3w6-vc56 | 10.0 | Maven | org.postgresql:postgresql | 42.7.0 | pom.xml |
+| https://osv.dev/GHSA-w3w6-26f2-p474 | 7.4 | Maven | org.springframework.security:spring-security-core | 6.2.0 | pom.xml |
+| https://osv.dev/GHSA-6vqw-3v5j-54x4 | 7.5 | PyPI | cryptography | 42.0.0 | streampipes-client-python/poetry.lock |
+| https://osv.dev/GHSA-9v9h-cgj8-h64p | 5.5 | PyPI | cryptography | 42.0.0 | streampipes-client-python/poetry.lock |
 | https://osv.dev/GHSA-w596-4wvx-j9j6<br/>https://osv.dev/PYSEC-2022-42969 | 7.5 | PyPI | py | 1.11.0 | streampipes-client-python/poetry.lock |
+| https://osv.dev/GHSA-78xj-cgh5-2h22 |  | npm | ip (dev) | 2.0.0 | ui/package-lock.json |
 | https://osv.dev/GHSA-4943-9vgg-gr5r | 6.1 | npm | quill | 1.3.7 | ui/package-lock.json |
+| https://osv.dev/GHSA-3787-6prv-h9w3 | 3.9 | npm | undici | 5.27.2 | ui/package-lock.json |
diff --git a/pom.xml b/pom.xml
index 06c4dec88e..43f36ba9dd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -48,7 +48,7 @@
         <checker-qual.version>3.42.0</checker-qual.version>
         <commons-codec.version>1.16.0</commons-codec.version>
         <commons-collections.version>3.2.2</commons-collections.version>
-        <commons-compress.version>1.25.0</commons-compress.version>
+        <commons-compress.version>1.26.0</commons-compress.version>
         <commons-io.version>2.15.1</commons-io.version>
         <commons-lang3.version>3.14.0</commons-lang3.version>
         <commons-pool2.version>2.12.0</commons-pool2.version>
@@ -105,7 +105,7 @@
         <plexus-component-annotations.version>2.1.1</plexus-component-annotations.version>
         <plexus-interactivity-api.version>1.1</plexus-interactivity-api.version>
         <plexus-utils.version>4.0.0</plexus-utils.version>
-        <postgresql.version>42.7.0</postgresql.version>
+        <postgresql.version>42.7.2</postgresql.version>
         <protobuf.version>3.25.0</protobuf.version>
         <pulsar.version>3.1.1</pulsar.version>
         <quartz.version>2.3.2</quartz.version>
diff --git a/streampipes-connect-shared/src/main/java/org/apache/streampipes/connect/shared/preprocessing/convert/ToTransformedSchemaConverter.java b/streampipes-connect-shared/src/main/java/org/apache/streampipes/connect/shared/preprocessing/convert/ToTransformedSchemaConverter.java
index f42c6a55df..b8bec63fa6 100644
--- a/streampipes-connect-shared/src/main/java/org/apache/streampipes/connect/shared/preprocessing/convert/ToTransformedSchemaConverter.java
+++ b/streampipes-connect-shared/src/main/java/org/apache/streampipes/connect/shared/preprocessing/convert/ToTransformedSchemaConverter.java
@@ -18,6 +18,7 @@
 
 package org.apache.streampipes.connect.shared.preprocessing.convert;
 
+import org.apache.streampipes.commons.random.UUIDGenerator;
 import org.apache.streampipes.connect.shared.preprocessing.utils.Utils;
 import org.apache.streampipes.model.connect.rules.ITransformationRuleVisitor;
 import org.apache.streampipes.model.connect.rules.schema.CreateNestedRuleDescription;
@@ -49,6 +50,9 @@ import static org.apache.streampipes.connect.shared.preprocessing.utils.Conversi
 
 public class ToTransformedSchemaConverter implements ITransformationRuleVisitor, ProvidesConversionResult {
 
+  private static final String TIMESTAMP_ID_PREFIX = "http://eventProperty.de/timestamp/";
+  private static final String STATIC_VALUE_ID_PREFIX = "http://eventProperty.de/staticValue/";
+
   private final List<EventProperty> properties;
 
   public ToTransformedSchemaConverter(List<EventProperty> properties) {
@@ -102,13 +106,15 @@ public class ToTransformedSchemaConverter implements ITransformationRuleVisitor,
 
   @Override
   public void visit(AddTimestampRuleDescription rule) {
-    this.properties.add(EpProperties.timestampProperty(rule.getRuntimeKey()));
+    var timestampProperty = EpProperties.timestampProperty(rule.getRuntimeKey());
+    timestampProperty.setElementId(TIMESTAMP_ID_PREFIX + UUIDGenerator.generateUuid());
+    this.properties.add(timestampProperty);
   }
 
   @Override
   public void visit(AddValueTransformationRuleDescription rule) {
     var property = new EventPropertyPrimitive();
-    property.setElementId("http://eventProperty.de/staticValue/" + rule.getStaticValue());
+    property.setElementId(STATIC_VALUE_ID_PREFIX + rule.getStaticValue());
     property.setRuntimeName(rule.getRuntimeKey());
     property.setRuntimeType(rule.getDatatype());
     property.setLabel(rule.getLabel());
diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaAdapter.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaAdapter.java
index 6af0bd28fd..c4e397d718 100644
--- a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaAdapter.java
+++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaAdapter.java
@@ -226,7 +226,7 @@ public class OpcUaAdapter implements StreamPipesAdapter, IPullAdapter, SupportsR
   @Override
   public StaticProperty resolveConfiguration(String staticPropertyInternalName,
                                              IStaticPropertyExtractor extractor) throws SpConfigurationException {
-    return OpcUaUtil.resolveConfiguration(staticPropertyInternalName, extractor);
+    return OpcUaUtil.resolveConfig(staticPropertyInternalName, extractor);
   }
 
   @Override
diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/SpOpcUaConfigExtractor.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/SpOpcUaConfigExtractor.java
index 612b3e4905..1145cb3501 100644
--- a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/SpOpcUaConfigExtractor.java
+++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/SpOpcUaConfigExtractor.java
@@ -84,9 +84,9 @@ public class SpOpcUaConfigExtractor {
     if (useURL) {
       String serverAddress =
           extractor.singleValueParameter(OPC_SERVER_URL.name(), String.class);
-      config.setOpcServerURL(OpcUaUtil.formatServerAddress(serverAddress));
+      config.setOpcServerURL(OpcUaUtil.addOpcPrefixIfNotExists(serverAddress));
     } else {
-      String serverAddress = OpcUaUtil.formatServerAddress(
+      String serverAddress = OpcUaUtil.addOpcPrefixIfNotExists(
           extractor.singleValueParameter(OPC_SERVER_HOST.name(), String.class)
       );
       int port = extractor.singleValueParameter(OPC_SERVER_PORT.name(), int.class);
diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/sink/OpcUaSink.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/sink/OpcUaSink.java
index bbe6a29005..0dfd7dade9 100644
--- a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/sink/OpcUaSink.java
+++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/sink/OpcUaSink.java
@@ -106,6 +106,6 @@ public class OpcUaSink implements IStreamPipesDataSink, SupportsRuntimeConfig {
   @Override
   public StaticProperty resolveConfiguration(String staticPropertyInternalName,
                                              IStaticPropertyExtractor extractor) throws SpConfigurationException {
-    return OpcUaUtil.resolveConfiguration(staticPropertyInternalName, extractor);
+    return OpcUaUtil.resolveConfig(staticPropertyInternalName, extractor);
   }
 }
diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtil.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtil.java
index 19e8d7c1c6..f7a3a2029d 100644
--- a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtil.java
+++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtil.java
@@ -38,11 +38,11 @@ import org.apache.streampipes.sdk.builder.PrimitivePropertyBuilder;
 import org.apache.streampipes.sdk.builder.adapter.GuessSchemaBuilder;
 
 import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
+import org.eclipse.milo.opcua.stack.core.AttributeId;
 import org.eclipse.milo.opcua.stack.core.UaException;
 import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
 import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
 import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode;
-import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
 import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
 
 import java.net.URI;
@@ -60,18 +60,15 @@ import java.util.stream.Collectors;
  */
 public class OpcUaUtil {
 
+  private static final String OPC_TCP_PREFIX = "opc.tcp://";
+
   /***
    * Ensures server address starts with {@code opc.tcp://}
    * @param serverAddress server address as given by user
    * @return correctly formated server address
    */
-  public static String formatServerAddress(String serverAddress) {
-
-    if (!serverAddress.startsWith("opc.tcp://")) {
-      serverAddress = "opc.tcp://" + serverAddress;
-    }
-
-    return serverAddress;
+  public static String addOpcPrefixIfNotExists(String serverAddress) {
+    return serverAddress.startsWith(OPC_TCP_PREFIX) ? serverAddress : OPC_TCP_PREFIX + serverAddress;
   }
 
   /***
@@ -103,21 +100,24 @@ public class OpcUaUtil {
         for (OpcNode opcNode : selectedNodes) {
           if (opcNode.hasUnitId()) {
             allProperties.add(PrimitivePropertyBuilder
-                .create(opcNode.getType(), opcNode.getLabel())
-                .label(opcNode.getLabel())
-                .measurementUnit(new URI(opcNode.getQudtURI()))
-                .build());
+                                  .create(opcNode.getType(), opcNode.getLabel())
+                                  .label(opcNode.getLabel())
+                                  .measurementUnit(new URI(opcNode.getQudtURI()))
+                                  .build());
           } else {
             allProperties.add(PrimitivePropertyBuilder
-                .create(opcNode.getType(), opcNode.getLabel())
-                .label(opcNode.getLabel())
-                .build());
+                                  .create(opcNode.getType(), opcNode.getLabel())
+                                  .label(opcNode.getLabel())
+                                  .build());
           }
         }
       }
 
-      var nodeIds = selectedNodes.stream().map(OpcNode::getNodeId).collect(Collectors.toList());
-      var response = spOpcUaClient.getClient().readValues(0, TimestampsToReturn.Both, nodeIds);
+      var nodeIds = selectedNodes.stream()
+                                 .map(OpcNode::getNodeId)
+                                 .collect(Collectors.toList());
+      var response = spOpcUaClient.getClient()
+                                  .readValues(0, TimestampsToReturn.Both, nodeIds);
 
       var returnValues = response.get();
 
@@ -138,20 +138,25 @@ public class OpcUaUtil {
     return builder.build();
   }
 
-  private static void makeEventPreview(List<OpcNode> selectedNodes,
-                                       Map<String, Object> eventPreview,
-                                       Map<String, FieldStatusInfo> fieldStatusInfos,
-                                       List<DataValue> dataValues) {
+  private static void makeEventPreview(
+      List<OpcNode> selectedNodes,
+      Map<String, Object> eventPreview,
+      Map<String, FieldStatusInfo> fieldStatusInfos,
+      List<DataValue> dataValues
+  ) {
 
     for (int i = 0; i < dataValues.size(); i++) {
       var dv = dataValues.get(i);
-      String label = selectedNodes.get(i).getLabel();
+      String label = selectedNodes.get(i)
+                                  .getLabel();
       if (StatusCode.GOOD.equals(dv.getStatusCode())) {
-        var value = dv.getValue().getValue();
+        var value = dv.getValue()
+                      .getValue();
         eventPreview.put(label, value);
         fieldStatusInfos.put(label, FieldStatusInfo.good());
       } else {
-        String additionalInfo = dv.getStatusCode() != null ? dv.getStatusCode().toString() : "Status code is null";
+        String additionalInfo = dv.getStatusCode() != null ? dv.getStatusCode()
+                                                               .toString() : "Status code is null";
         fieldStatusInfos.put(label, FieldStatusInfo.bad(additionalInfo, false));
       }
     }
@@ -165,9 +170,8 @@ public class OpcUaUtil {
    * @param parameterExtractor to extract parameters from the OPC UA config
    * @return {@code List<Option>} with available node names for the given OPC UA configuration
    */
-  public static RuntimeResolvableTreeInputStaticProperty
-      resolveConfiguration(String internalName,
-                           IStaticPropertyExtractor parameterExtractor)
+  public static RuntimeResolvableTreeInputStaticProperty resolveConfig(String internalName,
+                                                                       IStaticPropertyExtractor parameterExtractor)
       throws SpConfigurationException {
 
     RuntimeResolvableTreeInputStaticProperty config = parameterExtractor
@@ -196,6 +200,12 @@ public class OpcUaUtil {
         config.setLatestFetchedNodes(nodes);
       }
 
+      if (!config.getSelectedNodesInternalNames().isEmpty()) {
+        config.setSelectedNodesInternalNames(
+            filterMissingNodes(spOpcUaClient.getClient(), config.getSelectedNodesInternalNames())
+        );
+      }
+
 
       return config;
     } catch (UaException e) {
@@ -209,38 +219,17 @@ public class OpcUaUtil {
     }
   }
 
-  public static String getRuntimeNameOfNode(NodeId nodeId) {
-    String[] keys = nodeId.getIdentifier().toString().split("\\.");
-    String key;
-
-    if (keys.length > 0) {
-      key = keys[keys.length - 1];
-    } else {
-      key = nodeId.getIdentifier().toString();
-    }
-
-    return key;
-  }
-
-  /**
-   * connects to each node individually and updates the data type in accordance to the data from the server.
-   *
-   * @param opcNodes List of opcNodes where the data type is not determined appropriately
-   */
-  public static void retrieveDataTypesFromServer(OpcUaClient client, List<OpcNode> opcNodes) throws AdapterException {
-
-    for (OpcNode opcNode : opcNodes) {
+  public static List<String> filterMissingNodes(OpcUaClient opcUaClient,
+                                                List<String> selectedNodes) {
+    return selectedNodes.stream().filter(selectedNode -> {
       try {
-        UInteger dataTypeId =
-            (UInteger) client.getAddressSpace().getVariableNode(opcNode.getNodeId()).getDataType()
-                .getIdentifier();
-        OpcUaTypes.getType(dataTypeId);
-        opcNode.setType(OpcUaTypes.getType(dataTypeId));
+        var node = opcUaClient.getAddressSpace().getNode(NodeId.parse(selectedNode));
+        var value = node.readAttribute(AttributeId.Value);
+        var statusCode = value.getStatusCode();
+        return statusCode != null && statusCode.isGood();
       } catch (UaException e) {
-        throw new AdapterException("Could not guess schema for opc node! " + e.getMessage());
+        return false;
       }
-    }
+    }).toList();
   }
-
-
 }
diff --git a/streampipes-mail/src/main/java/org/apache/streampipes/mail/template/generation/MailTemplateBuilder.java b/streampipes-mail/src/main/java/org/apache/streampipes/mail/template/generation/MailTemplateBuilder.java
index 2ebd7f0f63..9016607139 100644
--- a/streampipes-mail/src/main/java/org/apache/streampipes/mail/template/generation/MailTemplateBuilder.java
+++ b/streampipes-mail/src/main/java/org/apache/streampipes/mail/template/generation/MailTemplateBuilder.java
@@ -74,11 +74,9 @@ public class MailTemplateBuilder {
 
     for (String key : placeholders.keySet()) {
       String placeholder = makeKey(key);
-      if (placeholders.containsKey(placeholder)) {
-        var value = placeholders.get(key);
-        if (Objects.nonNull(value)) {
-          fullTemplate = fullTemplate.replaceAll(placeholder, value);
-        }
+      var value = placeholders.get(key);
+      if (Objects.nonNull(value)) {
+        fullTemplate = fullTemplate.replaceAll(placeholder, value);
       }
     }
 
@@ -90,6 +88,7 @@ public class MailTemplateBuilder {
   }
 
   private String applyInnerTemplate(String content) {
+    innerPart = innerPart.replace("\\", "\\\\").replace("$", "\\$");
     return content.replaceAll(makeKey(DefaultPlaceholders.INNER.key()), innerPart);
   }
 
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/ResetManagement.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/ResetManagement.java
index 31f3fbe720..77fb60bb42 100644
--- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/ResetManagement.java
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/ResetManagement.java
@@ -18,6 +18,7 @@
 
 package org.apache.streampipes.rest;
 
+import org.apache.streampipes.commons.exceptions.SpRuntimeException;
 import org.apache.streampipes.commons.exceptions.connect.AdapterException;
 import org.apache.streampipes.commons.prometheus.adapter.AdapterMetricsManager;
 import org.apache.streampipes.connect.management.management.AdapterMasterManagement;
@@ -36,12 +37,15 @@ import org.apache.streampipes.resource.management.UserResourceManager;
 import org.apache.streampipes.storage.api.IDashboardStorage;
 import org.apache.streampipes.storage.api.IDashboardWidgetStorage;
 import org.apache.streampipes.storage.api.IDataExplorerWidgetStorage;
+import org.apache.streampipes.storage.api.IGenericStorage;
 import org.apache.streampipes.storage.management.StorageDispatcher;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.List;
+import java.util.Map;
 
 public class ResetManagement {
   // This class should be moved into another package. I moved it here because I got a cyclic maven
@@ -52,28 +56,56 @@ public class ResetManagement {
 
   /**
    * Remove all configurations for this user. This includes:
-   * [pipeline assembly cache, pipelines, adapters, files]
+   * [pipeline assembly cache, pipelines, adapters, files, assets]
    *
-   * @param username
+   * @param username of the user to delte the resources
    */
   public static void reset(String username) {
     logger.info("Start resetting the system");
 
-    // Set hide tutorial to false for user
+    setHideTutorialToFalse(username);
+
+    clearPipelineAssemblyCache(username);
+
+    stopAndDeleteAllPipelines();
+
+    stopAndDeleteAllAdapters();
+
+    deleteAllFiles();
+
+    removeAllDataInDataLake();
+
+    removeAllDataViewWidgets();
+
+    removeAllDataViews();
+
+    removeAllDashboardWidgets();
+
+    removeAllDashboards();
+
+    removeAllAssets(username);
+
+    logger.info("Resetting the system was completed");
+  }
+
+  private static void setHideTutorialToFalse(String username) {
     UserResourceManager.setHideTutorial(username, true);
+  }
 
-    // Clear pipeline assembly Cache
+  private static void clearPipelineAssemblyCache(String username) {
     PipelineCacheManager.removeCachedPipeline(username);
     PipelineCanvasMetadataCacheManager.removeCanvasMetadataFromCache(username);
+  }
 
-    // Stop and delete all pipelines
+  private static void stopAndDeleteAllPipelines() {
     List<Pipeline> allPipelines = PipelineManager.getAllPipelines();
     allPipelines.forEach(pipeline -> {
       PipelineManager.stopPipeline(pipeline.getPipelineId(), true);
       PipelineManager.deletePipeline(pipeline.getPipelineId());
     });
+  }
 
-    // Stop and delete all adapters
+  private static void stopAndDeleteAllAdapters() {
     AdapterMasterManagement adapterMasterManagement = new AdapterMasterManagement(
         StorageDispatcher.INSTANCE.getNoSqlStore()
                                   .getAdapterInstanceStorage(),
@@ -94,14 +126,14 @@ public class ResetManagement {
     } catch (AdapterException e) {
       logger.error("Failed to load all adapter descriptions", e);
     }
+  }
 
-    // Stop and delete all files
+  private static void deleteAllFiles() {
     List<FileMetadata> allFiles = FileManager.getAllFiles();
-    allFiles.forEach(fileMetadata -> {
-      FileManager.deleteFile(fileMetadata.getFileId());
-    });
+    allFiles.forEach(fileMetadata -> FileManager.deleteFile(fileMetadata.getFileId()));
+  }
 
-    // Remove all data in data lake
+  private static void removeAllDataInDataLake() {
     var dataLakeStorage = StorageDispatcher.INSTANCE
         .getNoSqlStore()
         .getDataLakeStorage();
@@ -116,34 +148,48 @@ public class ResetManagement {
         dataLakeMeasureManagement.deleteMeasurementByName(measurement.getMeasureName());
       }
     });
+  }
 
-    // Remove all data views widgets
+  private static void removeAllDataViewWidgets() {
     IDataExplorerWidgetStorage widgetStorage =
-        StorageDispatcher.INSTANCE.getNoSqlStore().getDataExplorerWidgetStorage();
-    widgetStorage.getAllDataExplorerWidgets().forEach(widget -> {
-      widgetStorage.deleteDataExplorerWidget(widget.getId());
-    });
+        StorageDispatcher.INSTANCE.getNoSqlStore()
+                                  .getDataExplorerWidgetStorage();
+    widgetStorage.getAllDataExplorerWidgets()
+                 .forEach(widget -> widgetStorage.deleteDataExplorerWidget(widget.getId()));
+  }
 
-    // Remove all data views
+  private static void removeAllDataViews() {
     IDashboardStorage dataLakeDashboardStorage =
-        StorageDispatcher.INSTANCE.getNoSqlStore().getDataExplorerDashboardStorage();
-    dataLakeDashboardStorage.getAllDashboards().forEach(dashboard -> {
-      dataLakeDashboardStorage.deleteDashboard(dashboard.getCouchDbId());
-    });
+        StorageDispatcher.INSTANCE.getNoSqlStore()
+                                  .getDataExplorerDashboardStorage();
+    dataLakeDashboardStorage.getAllDashboards()
+                            .forEach(dashboard -> dataLakeDashboardStorage.deleteDashboard(dashboard.getCouchDbId()));
+  }
 
-    // Remove all dashboard widgets
+  private static void removeAllDashboardWidgets() {
     IDashboardWidgetStorage dashobardWidgetStorage =
-        StorageDispatcher.INSTANCE.getNoSqlStore().getDashboardWidgetStorage();
-    dashobardWidgetStorage.getAllDashboardWidgets().forEach(widget -> {
-      dashobardWidgetStorage.deleteDashboardWidget(widget.getId());
-    });
+        StorageDispatcher.INSTANCE.getNoSqlStore()
+                                  .getDashboardWidgetStorage();
+    dashobardWidgetStorage.getAllDashboardWidgets()
+                          .forEach(widget -> dashobardWidgetStorage.deleteDashboardWidget(widget.getId()));
+  }
 
-    // Remove all dashboards
-    IDashboardStorage dashboardStorage = StorageDispatcher.INSTANCE.getNoSqlStore().getDashboardStorage();
-    dashboardStorage.getAllDashboards().forEach(dashboard -> {
-      dashboardStorage.deleteDashboard(dashboard.getCouchDbId());
-    });
+  private static void removeAllDashboards() {
+    IDashboardStorage dashboardStorage = StorageDispatcher.INSTANCE.getNoSqlStore()
+                                                                   .getDashboardStorage();
+    dashboardStorage.getAllDashboards()
+                    .forEach(dashboard -> dashboardStorage.deleteDashboard(dashboard.getCouchDbId()));
+  }
 
-    logger.info("Resetting the system was completed");
+  private static void removeAllAssets(String username) {
+    IGenericStorage genericStorage = StorageDispatcher.INSTANCE.getNoSqlStore()
+                                                               .getGenericStorage();
+    try {
+      for (Map<String, Object> asset : genericStorage.findAll("asset-management")) {
+        genericStorage.delete((String) asset.get("_id"), (String) asset.get("_rev"));
+      }
+    } catch (IOException e) {
+      throw new SpRuntimeException("Could not delete assets of user %s".formatted(username));
+    }
   }
 }
diff --git a/streampipes-sdk/src/main/java/org/apache/streampipes/sdk/stream/SchemaBuilder.java b/streampipes-sdk/src/main/java/org/apache/streampipes/sdk/stream/SchemaBuilder.java
deleted file mode 100644
index 9f0e7a9af9..0000000000
--- a/streampipes-sdk/src/main/java/org/apache/streampipes/sdk/stream/SchemaBuilder.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package org.apache.streampipes.sdk.stream;
-
-import org.apache.streampipes.commons.Utils;
-import org.apache.streampipes.model.schema.EventProperty;
-import org.apache.streampipes.model.schema.EventPropertyPrimitive;
-import org.apache.streampipes.model.schema.EventSchema;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@Deprecated
-public class SchemaBuilder {
-
-  private EventSchema schema;
-  private List<EventProperty> properties;
-
-  private SchemaBuilder() {
-    this.schema = new EventSchema();
-    this.properties = new ArrayList<>();
-  }
-
-  public static SchemaBuilder create() {
-    return new SchemaBuilder();
-  }
-
-  public SchemaBuilder simpleProperty(String label, String description, String runtimeName, String subPropertyOf,
-                                      String dataType) {
-    EventPropertyPrimitive primitive =
-        new EventPropertyPrimitive(dataType, runtimeName, "", Utils.createURI(subPropertyOf));
-    primitive.setDescription(description);
-    primitive.setLabel(label);
-    properties.add(primitive);
-    return this;
-  }
-
-  public SchemaBuilder properties(List<EventProperty> properties) {
-    this.properties.addAll(properties);
-    return this;
-  }
-
-  public EventSchema build() {
-    schema.setEventProperties(properties);
-    return schema;
-  }
-
-
-}
diff --git a/streampipes-sdk/src/main/java/org/apache/streampipes/sdk/stream/StreamBuilder.java b/streampipes-sdk/src/main/java/org/apache/streampipes/sdk/stream/StreamBuilder.java
deleted file mode 100644
index f213f02a21..0000000000
--- a/streampipes-sdk/src/main/java/org/apache/streampipes/sdk/stream/StreamBuilder.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package org.apache.streampipes.sdk.stream;
-
-import org.apache.streampipes.commons.Utils;
-import org.apache.streampipes.model.SpDataStream;
-import org.apache.streampipes.model.grounding.EventGrounding;
-import org.apache.streampipes.model.schema.EventPropertyPrimitive;
-import org.apache.streampipes.model.schema.EventSchema;
-
-import java.net.URI;
-
-@Deprecated
-public class StreamBuilder {
-
-  private SpDataStream stream;
-  private EventSchema schema;
-
-  private StreamBuilder(String name, String description, String elementId) {
-    stream = new SpDataStream();
-    stream.setName(name);
-    stream.setDescription(description);
-    stream.setElementId(elementId);
-  }
-
-  private StreamBuilder(String elementId) {
-    stream = new SpDataStream();
-    stream.setElementId(elementId);
-    schema = new EventSchema();
-  }
-
-  public static StreamBuilder createStream(String name, String description, String elementId) {
-    return new StreamBuilder(name, description, elementId);
-  }
-
-  public static StreamBuilder createStreamRestriction(String elementId) {
-    return new StreamBuilder(elementId);
-  }
-
-  public StreamBuilder property(String propertyName, URI propertyType, URI subclassOf) {
-    schema.addEventProperty(
-        new EventPropertyPrimitive(propertyType.toString(), propertyName, "", Utils.createURI(subclassOf.toString())));
-    return this;
-  }
-
-  public StreamBuilder icon(String iconUrl) {
-    stream.setIconUrl(iconUrl);
-    return this;
-  }
-
-  public StreamBuilder schema(EventSchema eventSchema) {
-    stream.setEventSchema(eventSchema);
-    return this;
-  }
-
-  public StreamBuilder grounding(EventGrounding eventGrounding) {
-    stream.setEventGrounding(eventGrounding);
-    return this;
-  }
-
-  public SpDataStream build() {
-    return stream;
-  }
-
-}
diff --git a/ui/src/app/connect/components/configuration-group/configuration-group.component.html b/ui/src/app/connect/components/configuration-group/configuration-group.component.html
index 482e5f7765..a09b11b734 100644
--- a/ui/src/app/connect/components/configuration-group/configuration-group.component.html
+++ b/ui/src/app/connect/components/configuration-group/configuration-group.component.html
@@ -17,7 +17,11 @@
   -->
 
 <div style="padding: 15px">
-    <form [formGroup]="configurationGroup" fxFlex="100">
+    <form
+        [formGroup]="configurationGroup"
+        (keydown.enter)="$event.preventDefault()"
+        fxFlex="100"
+    >
         <div fxFlex="100">
             <sp-app-static-property
                 *ngFor="let config of configuration"
diff --git a/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.html b/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.html
index a33906c48e..cf16da3273 100644
--- a/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.html
+++ b/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.html
@@ -43,7 +43,7 @@
                     *ngIf="!permission.publicElement"
                 >
                     <span class="general-options-header">Users</span>
-                    <mat-form-field class="example-chip-list" color="accent">
+                    <mat-form-field color="accent">
                         <mat-label>Authorized Users</mat-label>
                         <mat-chip-grid #chipList aria-label="User selection">
                             <mat-chip-row
@@ -88,7 +88,7 @@
                     *ngIf="!permission.publicElement"
                 >
                     <span class="general-options-header">Groups</span>
-                    <mat-form-field class="example-chip-list" color="accent">
+                    <mat-form-field color="accent">
                         <mat-label>Authorized Groups</mat-label>
                         <mat-chip-grid #chipList aria-label="Group selection">
                             <mat-chip-row
diff --git a/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.ts b/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.ts
index 61207745d1..1f21a19d03 100644
--- a/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.ts
+++ b/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.ts
@@ -26,15 +26,14 @@ import {
     Validators,
 } from '@angular/forms';
 import {
-    PermissionsService,
     Group,
     Permission,
     PermissionEntry,
+    PermissionsService,
     ServiceAccount,
     UserAccount,
-    UserService,
-    UserGroupService,
     UserAdminService,
+    UserGroupService,
 } from '@streampipes/platform-services';
 import { MatChipInputEvent } from '@angular/material/chips';
 import { Observable, zip } from 'rxjs';
@@ -80,7 +79,6 @@ export class ObjectPermissionDialogComponent implements OnInit {
         private fb: UntypedFormBuilder,
         private dialogRef: DialogRef<ObjectPermissionDialogComponent>,
         private permissionsService: PermissionsService,
-        private userService: UserService,
         private userAdminService: UserAdminService,
         private groupService: UserGroupService,
     ) {
@@ -198,6 +196,7 @@ export class ObjectPermissionDialogComponent implements OnInit {
             u => u.principalId === user.principalId,
         );
         this.grantedUserAuthorities.splice(currentIndex, 1);
+        this.userCtrl.setValue(null);
     }
 
     removeGroup(group: Group) {
@@ -205,6 +204,7 @@ export class ObjectPermissionDialogComponent implements OnInit {
             u => u.groupId === group.groupId,
         );
         this.grantedGroupAuthorities.splice(currentIndex, 1);
+        this.groupCtrl.setValue(null);
     }
 
     addUser(event: MatChipInputEvent) {
diff --git a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input.component.html b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input.component.html
index 4ac42a35b8..beabe6de63 100644
--- a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input.component.html
+++ b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input.component.html
@@ -29,6 +29,16 @@
             >
                 <span>Clear selection & reload</span>
             </button>
+            <button
+                mat-raised-button
+                color="accent"
+                class="small-button"
+                (click)="reload()"
+                style="margin-right: 10px"
+                [disabled]="!showOptions"
+            >
+                <span>Reload</span>
+            </button>
         </div>
         <div fxLayout="column" *ngIf="loading" class="mt-10">
             <mat-spinner
diff --git a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input.component.ts b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input.component.ts
index f1a8d60581..f736e4a139 100644
--- a/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input.component.ts
+++ b/ui/src/app/core-ui/static-properties/static-runtime-resolvable-tree-input/static-tree-input.component.ts
@@ -90,7 +90,9 @@ export class StaticRuntimeResolvableTreeInputComponent
         ) {
             this.staticProperty.latestFetchedNodes =
                 staticProperty.latestFetchedNodes;
-            node.children = staticProperty.latestFetchedNodes;
+            if (node) {
+                node.children = staticProperty.latestFetchedNodes;
+            }
         } else {
             this.staticProperty.nodes = staticProperty.nodes;
             this.dataSource.data = this.staticProperty.nodes;
@@ -138,6 +140,10 @@ export class StaticRuntimeResolvableTreeInputComponent
         this.loadOptionsFromRestApi();
     }
 
+    reload(): void {
+        this.loadOptionsFromRestApi();
+    }
+
     loadChildren(node: TreeInputNode, expanded: boolean): void {
         this.staticProperty.nextBaseNodeToResolve = node.internalNodeName;
         if (expanded) {


Test patch:
diff --git a/streampipes-commons/src/test/java/org/apache/streampipes/commons/parser/BooleanParserTest.java b/streampipes-commons/src/test/java/org/apache/streampipes/commons/parser/BooleanParserTest.java
new file mode 100644
index 0000000000..e0f142bdf1
--- /dev/null
+++ b/streampipes-commons/src/test/java/org/apache/streampipes/commons/parser/BooleanParserTest.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.streampipes.commons.parser;
+
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class BooleanParserTest {
+
+  private BooleanParser parser;
+
+  @Before
+  public void setup() {
+    parser = new BooleanParser();
+  }
+
+  @Test
+  public void parseTrue() {
+    assertTrue(parser.parse("true"));
+  }
+
+  @Test
+  public void parseFalse() {
+    assertFalse(parser.parse("false"));
+  }
+
+  @Test
+  public void parseNotABoolean() {
+    assertFalse(parser.parse("not a boolean"));
+  }
+
+
+}
\ No newline at end of file
diff --git a/streampipes-commons/src/test/java/org/apache/streampipes/commons/parser/FloatParserTest.java b/streampipes-commons/src/test/java/org/apache/streampipes/commons/parser/FloatParserTest.java
new file mode 100644
index 0000000000..1fce65e5aa
--- /dev/null
+++ b/streampipes-commons/src/test/java/org/apache/streampipes/commons/parser/FloatParserTest.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.streampipes.commons.parser;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+public class FloatParserTest {
+  private FloatParser parser;
+
+  @Before
+  public void setup() {
+    parser = new FloatParser();
+  }
+
+  @Test
+  public void parseValidFloat() {
+    assertEquals(123.45f, parser.parse("123.45"), 0.0001);
+  }
+
+  @Test
+  public void parseInvalidFloat() {
+    assertThrows(NumberFormatException.class, () -> parser.parse("stringValue"));
+  }
+}
diff --git a/streampipes-commons/src/test/java/org/apache/streampipes/commons/parser/IntegerParserTest.java b/streampipes-commons/src/test/java/org/apache/streampipes/commons/parser/IntegerParserTest.java
new file mode 100644
index 0000000000..c127f2fbc2
--- /dev/null
+++ b/streampipes-commons/src/test/java/org/apache/streampipes/commons/parser/IntegerParserTest.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.streampipes.commons.parser;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+public class IntegerParserTest {
+  private IntegerParser parser;
+
+  @Before
+  public void setup() {
+    parser = new IntegerParser();
+  }
+
+  @Test
+  public void parseValidInteger() {
+    assertEquals(1, (int) parser.parse("1"));
+  }
+
+  @Test
+  public void parseInvalidInteger() {
+    assertThrows(NumberFormatException.class, () -> parser.parse("stringValue"));
+  }
+}
diff --git a/streampipes-commons/src/test/java/org/apache/streampipes/commons/parser/StringParserTest.java b/streampipes-commons/src/test/java/org/apache/streampipes/commons/parser/StringParserTest.java
new file mode 100644
index 0000000000..bfcc171c29
--- /dev/null
+++ b/streampipes-commons/src/test/java/org/apache/streampipes/commons/parser/StringParserTest.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.streampipes.commons.parser;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class StringParserTest {
+
+  @Test
+  public void testParse() {
+    StringParser stringParser = new StringParser();
+
+    String input = "test string";
+    assertEquals(input, stringParser.parse(input));
+  }
+}
diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/test/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtilTest.java b/streampipes-extensions/streampipes-connectors-opcua/src/test/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtilTest.java
new file mode 100644
index 0000000000..5dda95b53e
--- /dev/null
+++ b/streampipes-extensions/streampipes-connectors-opcua/src/test/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtilTest.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.streampipes.extensions.connectors.opcua.utils;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class OpcUaUtilTest {
+
+  private static final String SERVER_ADDRESS_WITH_OPC_PREFIX = "opc.tcp://example.com";
+
+  @Test
+  public void testAddOpcPrefixIfNotExistsWithPrefix() {
+    var result = OpcUaUtil.addOpcPrefixIfNotExists(SERVER_ADDRESS_WITH_OPC_PREFIX);
+    assertEquals(SERVER_ADDRESS_WITH_OPC_PREFIX, result);
+  }
+
+  @Test
+  public void testAddOpcPrefixIfNotExistsNoPrefix() {
+    var result = OpcUaUtil.addOpcPrefixIfNotExists("example.com");
+    assertEquals(SERVER_ADDRESS_WITH_OPC_PREFIX, result);
+  }
+}
\ No newline at end of file