This is an automated email from the ASF dual-hosted git repository.

dominikriemer pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to refs/heads/dev by this push:
     new f8ec4d2ae3 feat(#4690): deprecate legacy plc4x s7 and modbus adapters 
(#4691)
f8ec4d2ae3 is described below

commit f8ec4d2ae363330482eaab45d961572eb7a9998e
Author: Philipp Zehnder <[email protected]>
AuthorDate: Thu Jul 16 17:25:16 2026 +0200

    feat(#4690): deprecate legacy plc4x s7 and modbus adapters (#4691)
---
 .../plc/adapter/modbus/Plc4xModbusAdapter.java     |   5 +
 .../connectors/plc/adapter/s7/Plc4xS7Adapter.java  |   5 +
 .../documentation.md                               |   3 +
 .../documentation.md                               |   4 +
 .../core/migrations/AvailableMigrations.java       |   2 +
 .../MigratePlc4xS7AdaptersToGenericAdapter.java    | 443 +++++++++++++++++++++
 ...MigratePlc4xS7AdaptersToGenericAdapterTest.java | 294 ++++++++++++++
 7 files changed, 756 insertions(+)

diff --git 
a/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/modbus/Plc4xModbusAdapter.java
 
b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/modbus/Plc4xModbusAdapter.java
index 96fc78d3e0..71408004d8 100644
--- 
a/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/modbus/Plc4xModbusAdapter.java
+++ 
b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/modbus/Plc4xModbusAdapter.java
@@ -61,6 +61,11 @@ import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 
+/**
+ * @deprecated Use the generic PLC4X Modbus adapter instead.
+ *     This concrete adapter is planned for removal after the 0.99.0 release.
+ */
+@Deprecated(since = "0.99.0", forRemoval = true)
 public class Plc4xModbusAdapter implements StreamPipesAdapter, IPullAdapter {
 
   private static final Logger LOG = 
LoggerFactory.getLogger(Plc4xModbusAdapter.class);
diff --git 
a/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/s7/Plc4xS7Adapter.java
 
b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/s7/Plc4xS7Adapter.java
index f5666fab17..95e1c86218 100644
--- 
a/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/s7/Plc4xS7Adapter.java
+++ 
b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/s7/Plc4xS7Adapter.java
@@ -54,6 +54,11 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Map;
 
+/**
+ * @deprecated Use the generic PLC4X S7 adapter instead.
+ *     This concrete adapter is planned for removal after the 0.99.0 release.
+ */
+@Deprecated(since = "0.99.0", forRemoval = true)
 public class Plc4xS7Adapter implements StreamPipesAdapter {
 
   /**
diff --git 
a/streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.modbus/documentation.md
 
b/streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.modbus/documentation.md
index aa2ad29375..b3d1114eed 100644
--- 
a/streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.modbus/documentation.md
+++ 
b/streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.modbus/documentation.md
@@ -26,6 +26,9 @@
 
 ## Description
 
+This adapter is deprecated since StreamPipes 0.99.0 and is planned for removal 
after the 0.99.0 release.
+Use the Modbus TCP adapter based on the generic PLC4X implementation instead.
+
 The Modbus adapter allows to connect to a PLC using the Modbus specification.
 
 ***
diff --git 
a/streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.s7/documentation.md
 
b/streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.s7/documentation.md
index 589cf99b72..f0dd04f055 100644
--- 
a/streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.s7/documentation.md
+++ 
b/streampipes-extensions/streampipes-connectors-plc/src/main/resources/org.apache.streampipes.connect.iiot.adapters.plc4x.s7/documentation.md
@@ -26,6 +26,10 @@
 
 ## Description
 
+This adapter is deprecated since StreamPipes 0.99.0 and is planned for removal 
after the 0.99.0 release.
+Use the Siemens S7 (Basic) adapter instead.
+Existing adapter instances are automatically migrated, so no manual effort is 
required.
+
 The adapter allows to connect with a Siemens S7 PLC.
 
 ***
diff --git 
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/AvailableMigrations.java
 
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/AvailableMigrations.java
index ab85fecaa0..f52e61a4a1 100644
--- 
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/AvailableMigrations.java
+++ 
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/AvailableMigrations.java
@@ -40,6 +40,7 @@ import 
org.apache.streampipes.service.core.migrations.v099.RemoveInternalNotific
 import 
org.apache.streampipes.service.core.migrations.v099.RemoveObsoletePrivilegesMigration;
 import 
org.apache.streampipes.service.core.migrations.v099.UniqueDashboardIdMigration;
 import 
org.apache.streampipes.service.core.migrations.v099.connect.MigrateAdaptersToUseScript;
+import 
org.apache.streampipes.service.core.migrations.v099.connect.MigratePlc4xS7AdaptersToGenericAdapter;
 import org.apache.streampipes.storage.api.connect.IAdapterStorage;
 import org.apache.streampipes.storage.api.explorer.IChartStorage;
 import org.apache.streampipes.storage.api.explorer.IDashboardStorage;
@@ -102,6 +103,7 @@ public class AvailableMigrations {
         new AddScriptTemplateViewMigration(),
         new ComputeCertificateThumbprintMigration(),
         new MigrateAdaptersToUseScript(adapterStorage),
+        new MigratePlc4xS7AdaptersToGenericAdapter(adapterStorage),
         new ModifyAssetLinkIconMigration(),
         new RemoveDuplicatedAssetPermissions(permissionStorage, assetStorage),
         new AddFunctionStateViewMigration(),
diff --git 
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/v099/connect/MigratePlc4xS7AdaptersToGenericAdapter.java
 
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/v099/connect/MigratePlc4xS7AdaptersToGenericAdapter.java
new file mode 100644
index 0000000000..3a31ce721e
--- /dev/null
+++ 
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/v099/connect/MigratePlc4xS7AdaptersToGenericAdapter.java
@@ -0,0 +1,443 @@
+/*
+ * 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.service.core.migrations.v099.connect;
+
+import org.apache.streampipes.model.connect.adapter.AdapterDescription;
+import org.apache.streampipes.model.staticproperty.CodeInputStaticProperty;
+import org.apache.streampipes.model.staticproperty.CollectionStaticProperty;
+import org.apache.streampipes.model.staticproperty.FreeTextStaticProperty;
+import org.apache.streampipes.model.staticproperty.OneOfStaticProperty;
+import org.apache.streampipes.model.staticproperty.Option;
+import 
org.apache.streampipes.model.staticproperty.RuntimeResolvableGroupStaticProperty;
+import org.apache.streampipes.model.staticproperty.StaticProperty;
+import org.apache.streampipes.model.staticproperty.StaticPropertyAlternative;
+import org.apache.streampipes.model.staticproperty.StaticPropertyAlternatives;
+import org.apache.streampipes.model.staticproperty.StaticPropertyGroup;
+import org.apache.streampipes.service.core.migrations.Migration;
+import org.apache.streampipes.storage.api.connect.IAdapterStorage;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+public class MigratePlc4xS7AdaptersToGenericAdapter implements Migration {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(MigratePlc4xS7AdaptersToGenericAdapter.class);
+
+  private static final String OLD_APP_ID = 
"org.apache.streampipes.connect.iiot.adapters.plc4x.s7";
+  private static final String NEW_APP_ID = 
"org.apache.streampipes.connect.iiot.adapters.plc4x.generic.s7";
+  private static final int GENERIC_ADAPTER_VERSION = 1;
+  private static final String DEFAULT_LOCALE = "strings.en";
+  private static final URI XSD_STRING = 
URI.create("http://www.w3.org/2001/XMLSchema#string";);
+  private static final URI XSD_INTEGER = 
URI.create("http://www.w3.org/2001/XMLSchema#integer";);
+
+  private static final String PLC_IP = "plc_ip";
+  private static final String PLC_POLLING_INTERVAL = "plc_polling_interval";
+  private static final String PLC_NODES = "plc_nodes";
+  private static final String PLC_NODE_NAME = "plc_node_name";
+  private static final String PLC_NODE_RUNTIME_NAME = "plc_node_runtime_name";
+  private static final String PLC_NODE_TYPE = "plc_node_type";
+  private static final String PLC_NODE_INPUT_ALTERNATIVES = 
"plc_node_input_alternatives";
+  private static final String PLC_NODE_INPUT_COLLECTION_ALTERNATIVE = 
"plc_node_input_collection_alternative";
+  private static final String PLC_CODE_BLOCK = "plc_code_block";
+  private static final String SUPPORTED_TRANSPORTS = "supported_transports";
+  private static final String TRANSPORT_METADATA = "transport_metadata";
+  private static final String PROTOCOL_METADATA = "protocol_metadata";
+  private static final String REQUIRED_OPTIONS = "required_options";
+  private static final String ADVANCED_OPTIONS = "advanced_options";
+  private static final String REQUIRED_GROUP_TRANSPORT = 
"required_group_transport";
+  private static final String ADVANCED_GROUP_TRANSPORT = 
"advanced_group_transport";
+  private static final String REQUIRED_GROUP_PROTOCOL = 
"required_group_protocol";
+  private static final String ADVANCED_GROUP_PROTOCOL = 
"advanced_group_protocol";
+
+  private final IAdapterStorage adapterStorage;
+
+  public MigratePlc4xS7AdaptersToGenericAdapter(IAdapterStorage 
adapterStorage) {
+    this.adapterStorage = adapterStorage;
+  }
+
+  @Override
+  public boolean shouldExecute() {
+    return adapterStorage.findAll()
+        .stream()
+        .anyMatch(adapter -> OLD_APP_ID.equals(adapter.getAppId()));
+  }
+
+  @Override
+  public void executeMigration() throws IOException {
+    adapterStorage.findAll()
+        .stream()
+        .filter(adapter -> OLD_APP_ID.equals(adapter.getAppId()))
+        .forEach(this::migrateAndUpdateAdapter);
+  }
+
+  @Override
+  public String getDescription() {
+    return "Migrates PLC4X S7 adapters to the generic PLC4X adapter.";
+  }
+
+  private void migrateAndUpdateAdapter(AdapterDescription adapter) {
+    LOG.info("Migrating PLC4X S7 adapter to generic PLC4X adapter: {}", 
adapter.getElementId());
+
+    var splitAddress = SplitPlcAddress.from(textValue(adapter, PLC_IP));
+    var codeBlock = makeCodeBlock(adapter);
+    adapter.setAppId(NEW_APP_ID);
+    adapter.setVersion(GENERIC_ADAPTER_VERSION);
+    adapter.setIncludesLocales(true);
+    adapter.setIncludedLocales(List.of(DEFAULT_LOCALE));
+    adapter.setConfig(List.of(
+        makeFreeText(PLC_IP, "PLC Address", "Example: 192.168.34.56", 
splitAddress.host(), false),
+        makeFreeText(
+            PLC_POLLING_INTERVAL,
+            "Polling Interval [ms]",
+            "Polling Interval of adapter in milliseconds. Minimum value is 
10.",
+            textValue(adapter, PLC_POLLING_INTERVAL),
+            false,
+            XSD_INTEGER
+        ),
+        makeSupportedTransports(),
+        makeTransportMetadata(),
+        makeProtocolMetadata(splitAddress.queryParameters()),
+        makeCodeBlock(codeBlock)
+    ));
+
+    adapterStorage.updateElement(adapter);
+  }
+
+  private String textValue(AdapterDescription adapter,
+                           String internalName) {
+    return adapter.getConfig()
+        .stream()
+        .filter(property -> internalName.equals(property.getInternalName()))
+        .filter(FreeTextStaticProperty.class::isInstance)
+        .map(FreeTextStaticProperty.class::cast)
+        .map(FreeTextStaticProperty::getValue)
+        .filter(Objects::nonNull)
+        .findFirst()
+        .orElse("");
+  }
+
+  private StaticProperty makeSupportedTransports() {
+    var property = new OneOfStaticProperty(
+        SUPPORTED_TRANSPORTS,
+        "Supported Transports",
+        "Select supported transport protocols"
+    );
+    property.setOptions(List.of(new Option("tcp", true)));
+    return property;
+  }
+
+  private StaticProperty makeTransportMetadata() {
+    return makeAlternatives(
+        TRANSPORT_METADATA,
+        "Transport Metadata",
+        "Apply transport metadata configurations",
+        makeAlternative(
+            REQUIRED_OPTIONS,
+            "Required options",
+            "",
+            makeRuntimeResolvableGroup(REQUIRED_GROUP_TRANSPORT),
+            true
+        ),
+        makeAlternative(
+            ADVANCED_OPTIONS,
+            "Advanced options",
+            "",
+            makeRuntimeResolvableGroup(ADVANCED_GROUP_TRANSPORT),
+            false
+        )
+    );
+  }
+
+  private StaticProperty makeRuntimeResolvableGroup(String internalName) {
+    var group = new RuntimeResolvableGroupStaticProperty(
+        internalName,
+        "",
+        "",
+        List.of(SUPPORTED_TRANSPORTS)
+    );
+    group.setStaticProperties(List.of());
+    group.setHorizontalRendering(false);
+    return group;
+  }
+
+  private StaticProperty makeProtocolMetadata(List<QueryParameter> 
queryParameters) {
+    return makeAlternatives(
+        PROTOCOL_METADATA,
+        "Protocol Metadata",
+        "Apply protocol metadata configurations",
+        makeAlternative(
+            REQUIRED_OPTIONS,
+            "Required options",
+            "",
+            makeProtocolGroup(REQUIRED_GROUP_PROTOCOL, queryParameters),
+            true
+        ),
+        makeAlternative(
+            ADVANCED_OPTIONS,
+            "Advanced options",
+            "",
+            makeProtocolGroup(ADVANCED_GROUP_PROTOCOL, List.of()),
+            false
+        )
+    );
+  }
+
+  private StaticProperty makeProtocolGroup(String internalName,
+                                           List<QueryParameter> 
queryParameters) {
+    var group = new StaticPropertyGroup(
+        internalName,
+        "",
+        ""
+    );
+    group.setStaticProperties(queryParameters.stream()
+        .map(queryParameter -> makeFreeText(
+            queryParameter.name(),
+            queryParameter.name(),
+            "",
+            queryParameter.value(),
+            true,
+            XSD_STRING
+        ))
+        .toList());
+    group.setHorizontalRendering(false);
+    return group;
+  }
+
+  private StaticProperty makeCodeBlock(MigratedCodeBlock migratedCodeBlock) {
+    var codeBlock = new CodeInputStaticProperty(
+        PLC_CODE_BLOCK,
+        "Tags",
+        "Enter the tags in the code block below, according to the described 
format"
+    );
+    codeBlock.setLanguage("None");
+    codeBlock.setCodeTemplate(migratedCodeBlock.codeTemplate());
+    codeBlock.setValue(migratedCodeBlock.value());
+    return codeBlock;
+  }
+
+  private MigratedCodeBlock makeCodeBlock(AdapterDescription adapter) {
+    var alternatives = getProperty(adapter, PLC_NODE_INPUT_ALTERNATIVES, 
StaticPropertyAlternatives.class);
+    if (alternatives.isPresent()) {
+      return makeCodeBlockFromAlternatives(alternatives.get());
+    }
+
+    var tags = getProperty(adapter, PLC_NODES, CollectionStaticProperty.class)
+        .map(this::makeTagsFromCollection)
+        .orElse("");
+    return new MigratedCodeBlock(tags, "");
+  }
+
+  private MigratedCodeBlock 
makeCodeBlockFromAlternatives(StaticPropertyAlternatives alternatives) {
+    var selectedAlternative = alternatives.getAlternatives()
+        .stream()
+        .filter(StaticPropertyAlternative::getSelected)
+        .findFirst();
+    var codeBlockTemplate = getCodeBlockFromAlternatives(alternatives)
+        .map(CodeInputStaticProperty::getCodeTemplate)
+        .orElse("");
+
+    if (selectedAlternative.isPresent()
+        && 
PLC_NODE_INPUT_COLLECTION_ALTERNATIVE.equals(selectedAlternative.get().getInternalName()))
 {
+      var tags = 
Optional.ofNullable(selectedAlternative.get().getStaticProperty())
+          .filter(CollectionStaticProperty.class::isInstance)
+          .map(CollectionStaticProperty.class::cast)
+          .map(this::makeTagsFromCollection)
+          .orElse("");
+      return new MigratedCodeBlock(tags, codeBlockTemplate);
+    }
+
+    var selectedCodeBlock = selectedAlternative
+        .map(StaticPropertyAlternative::getStaticProperty)
+        .filter(CodeInputStaticProperty.class::isInstance)
+        .map(CodeInputStaticProperty.class::cast);
+    var tags = selectedCodeBlock
+        .map(CodeInputStaticProperty::getValue)
+        .orElse("");
+    var template = selectedCodeBlock
+        .map(CodeInputStaticProperty::getCodeTemplate)
+        .orElse(codeBlockTemplate);
+    return new MigratedCodeBlock(tags, template);
+  }
+
+  private Optional<CodeInputStaticProperty> 
getCodeBlockFromAlternatives(StaticPropertyAlternatives alternatives) {
+    return alternatives.getAlternatives()
+        .stream()
+        .map(StaticPropertyAlternative::getStaticProperty)
+        .filter(CodeInputStaticProperty.class::isInstance)
+        .map(CodeInputStaticProperty.class::cast)
+        .findFirst();
+  }
+
+  private String makeTagsFromCollection(CollectionStaticProperty nodes) {
+    return nodes.getMembers()
+        .stream()
+        .filter(StaticPropertyGroup.class::isInstance)
+        .map(StaticPropertyGroup.class::cast)
+        .map(this::makeTag)
+        .filter(tag -> !tag.isBlank())
+        .toList()
+        .stream()
+        .collect(java.util.stream.Collectors.joining(System.lineSeparator()));
+  }
+
+  private String makeTag(StaticPropertyGroup node) {
+    var staticProperties = node.getStaticProperties();
+    var runtimeName = textValue(staticProperties, PLC_NODE_RUNTIME_NAME);
+    var nodeName = textValue(staticProperties, PLC_NODE_NAME);
+    var nodeType = selectedValue(staticProperties, PLC_NODE_TYPE)
+        .toUpperCase()
+        .replaceAll(" ", "_");
+
+    if (runtimeName.isBlank() || nodeName.isBlank() || nodeType.isBlank()) {
+      return "";
+    }
+
+    return "%s=%s:%s".formatted(runtimeName, nodeName, nodeType);
+  }
+
+  private <T extends StaticProperty> Optional<T> 
getProperty(AdapterDescription adapter,
+                                                            String 
internalName,
+                                                            Class<T> 
propertyClass) {
+    return adapter.getConfig()
+        .stream()
+        .filter(property -> internalName.equals(property.getInternalName()))
+        .filter(propertyClass::isInstance)
+        .map(propertyClass::cast)
+        .findFirst();
+  }
+
+  private StaticProperty makeFreeText(String internalName,
+                                      String label,
+                                      String description,
+                                      String value,
+                                      boolean optional) {
+    return makeFreeText(internalName, label, description, value, optional, 
XSD_STRING);
+  }
+
+  private StaticProperty makeFreeText(String internalName,
+                                      String label,
+                                      String description,
+                                      String value,
+                                      boolean optional,
+                                      URI datatype) {
+    var property = new FreeTextStaticProperty(
+        internalName,
+        label,
+        description,
+        datatype
+    );
+    property.setValue(value);
+    property.setOptional(optional);
+    return property;
+  }
+
+  private StaticProperty makeAlternatives(String internalName,
+                                          String label,
+                                          String description,
+                                          StaticPropertyAlternative... 
alternatives) {
+    var property = new StaticPropertyAlternatives(internalName, label, 
description);
+    property.setAlternatives(List.of(alternatives));
+    return property;
+  }
+
+  private StaticPropertyAlternative makeAlternative(String internalName,
+                                                   String label,
+                                                   String description,
+                                                   StaticProperty 
staticProperty,
+                                                   boolean selected) {
+    var alternative = new StaticPropertyAlternative(internalName, label, 
description);
+    alternative.setStaticProperty(staticProperty);
+    alternative.setSelected(selected);
+    return alternative;
+  }
+
+  private String textValue(List<StaticProperty> staticProperties,
+                           String internalName) {
+    return staticProperties
+        .stream()
+        .filter(property -> internalName.equals(property.getInternalName()))
+        .filter(FreeTextStaticProperty.class::isInstance)
+        .map(FreeTextStaticProperty.class::cast)
+        .map(FreeTextStaticProperty::getValue)
+        .filter(Objects::nonNull)
+        .findFirst()
+        .orElse("");
+  }
+
+  private String selectedValue(List<StaticProperty> staticProperties,
+                               String internalName) {
+    return staticProperties
+        .stream()
+        .filter(property -> internalName.equals(property.getInternalName()))
+        .filter(OneOfStaticProperty.class::isInstance)
+        .map(OneOfStaticProperty.class::cast)
+        .flatMap(property -> property.getOptions().stream())
+        .filter(Option::isSelected)
+        .findFirst()
+        .map(Option::getName)
+        .orElse("");
+  }
+
+  private record SplitPlcAddress(String host,
+                                 List<QueryParameter> queryParameters) {
+
+    static SplitPlcAddress from(String address) {
+      var splitAddress = address.split("\\?", 2);
+      var host = splitAddress[0];
+      var queryParameters = splitAddress.length == 2
+          ? parseQueryParameters(splitAddress[1])
+          : List.<QueryParameter>of();
+
+      return new SplitPlcAddress(host, queryParameters);
+    }
+
+    private static List<QueryParameter> parseQueryParameters(String query) {
+      if (query == null || query.isBlank()) {
+        return List.of();
+      }
+      return Arrays.stream(query.split("&"))
+          .map(QueryParameter::from)
+          .filter(Objects::nonNull)
+          .toList();
+    }
+  }
+
+  private record QueryParameter(String name,
+                                String value) {
+
+    static QueryParameter from(String queryParameter) {
+      var splitParameter = queryParameter.split("=", 2);
+      if (splitParameter.length != 2 || splitParameter[0].isBlank()) {
+        return null;
+      }
+      return new QueryParameter(splitParameter[0], splitParameter[1]);
+    }
+  }
+
+  private record MigratedCodeBlock(String value,
+                                   String codeTemplate) {
+  }
+}
diff --git 
a/streampipes-service-core/src/test/java/org/apache/streampipes/service/core/migrations/v099/connect/MigratePlc4xS7AdaptersToGenericAdapterTest.java
 
b/streampipes-service-core/src/test/java/org/apache/streampipes/service/core/migrations/v099/connect/MigratePlc4xS7AdaptersToGenericAdapterTest.java
new file mode 100644
index 0000000000..9c4f0a7600
--- /dev/null
+++ 
b/streampipes-service-core/src/test/java/org/apache/streampipes/service/core/migrations/v099/connect/MigratePlc4xS7AdaptersToGenericAdapterTest.java
@@ -0,0 +1,294 @@
+/*
+ * 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.service.core.migrations.v099.connect;
+
+import org.apache.streampipes.model.Tuple2;
+import org.apache.streampipes.model.connect.adapter.AdapterDescription;
+import org.apache.streampipes.model.staticproperty.CodeInputStaticProperty;
+import org.apache.streampipes.model.staticproperty.CollectionStaticProperty;
+import org.apache.streampipes.model.staticproperty.FreeTextStaticProperty;
+import org.apache.streampipes.model.staticproperty.OneOfStaticProperty;
+import org.apache.streampipes.model.staticproperty.Option;
+import org.apache.streampipes.model.staticproperty.StaticProperty;
+import org.apache.streampipes.model.staticproperty.StaticPropertyAlternative;
+import org.apache.streampipes.model.staticproperty.StaticPropertyAlternatives;
+import org.apache.streampipes.model.staticproperty.StaticPropertyGroup;
+import org.apache.streampipes.storage.api.connect.IAdapterStorage;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class MigratePlc4xS7AdaptersToGenericAdapterTest {
+
+  private static final String OLD_APP_ID = 
"org.apache.streampipes.connect.iiot.adapters.plc4x.s7";
+  private static final String NEW_APP_ID = 
"org.apache.streampipes.connect.iiot.adapters.plc4x.generic.s7";
+  private static final String PLC_IP = "plc_ip";
+  private static final String PLC_POLLING_INTERVAL = "plc_polling_interval";
+  private static final String PLC_NODES = "plc_nodes";
+  private static final String PLC_NODE_NAME = "plc_node_name";
+  private static final String PLC_NODE_RUNTIME_NAME = "plc_node_runtime_name";
+  private static final String PLC_NODE_TYPE = "plc_node_type";
+  private static final String PLC_NODE_INPUT_ALTERNATIVES = 
"plc_node_input_alternatives";
+  private static final String PLC_NODE_INPUT_CODE_BLOCK_ALTIVE = 
"plc_node_input_code_block_altive";
+  private static final String PLC_NODE_INPUT_COLLECTION_ALTERNATIVE = 
"plc_node_input_collection_alternative";
+  private static final String PLC_CODE_BLOCK = "plc_code_block";
+  private static final String CODE_TEMPLATE = "temperature=%I0.0:INT";
+
+  @Test
+  void migratesCollectionBasedS7Adapter() throws IOException {
+    var adapter = makeAdapter(
+        "192.168.34.56?remote-rack=0&remote-slot=3",
+        makeNodeCollection()
+    );
+    adapter.setVersion(0);
+
+    var storage = new InMemoryAdapterStorage(adapter);
+    var migration = new MigratePlc4xS7AdaptersToGenericAdapter(storage);
+
+    assertTrue(migration.shouldExecute());
+    migration.executeMigration();
+
+    var migratedAdapter = storage.updatedAdapter;
+    assertGenericAdapterMetadata(migratedAdapter);
+    assertEquals("192.168.34.56", value(migratedAdapter.getConfig().get(0)));
+    assertEquals("1000", value(migratedAdapter.getConfig().get(1)));
+    assertEquals(URI.create("http://www.w3.org/2001/XMLSchema#integer";),
+        ((FreeTextStaticProperty) 
migratedAdapter.getConfig().get(1)).getRequiredDatatype());
+    assertEquals("PLC Address", migratedAdapter.getConfig().get(0).getLabel());
+    assertEquals("Tags", migratedAdapter.getConfig().get(5).getLabel());
+    assertEquals(
+        String.join(System.lineSeparator(), "input=%I0.0:BOOL", 
"output=%Q0.4:TIME_OF_DAY"),
+        value(migratedAdapter.getConfig().get(5))
+    );
+    assertProtocolParameter(migratedAdapter, "remote-rack", "0");
+    assertProtocolParameter(migratedAdapter, "remote-slot", "3");
+  }
+
+  @Test
+  void migratesCodeBlockBasedS7Adapter() throws IOException {
+    var codeBlock = String.join(System.lineSeparator(), 
"temperature=%I0.0:INT", "pressure=%Q0.4:REAL");
+    var adapter = makeAdapter(
+        "192.168.34.56",
+        makeNodeInputAlternatives(makeNodeCollection(), 
makeCodeBlock(codeBlock), false)
+    );
+
+    var storage = new InMemoryAdapterStorage(adapter);
+    var migration = new MigratePlc4xS7AdaptersToGenericAdapter(storage);
+
+    migration.executeMigration();
+
+    var migratedAdapter = storage.updatedAdapter;
+    assertGenericAdapterMetadata(migratedAdapter);
+    assertEquals("192.168.34.56", value(migratedAdapter.getConfig().get(0)));
+    assertEquals(codeBlock, value(migratedAdapter.getConfig().get(5)));
+    assertEquals(CODE_TEMPLATE, ((CodeInputStaticProperty) 
migratedAdapter.getConfig().get(5)).getCodeTemplate());
+  }
+
+  @Test
+  void ignoresNonS7Adapters() throws IOException {
+    var adapter = makeAdapter("192.168.34.56", makeNodeCollection());
+    adapter.setAppId("other");
+
+    var storage = new InMemoryAdapterStorage(adapter);
+    var migration = new MigratePlc4xS7AdaptersToGenericAdapter(storage);
+
+    assertFalse(migration.shouldExecute());
+    migration.executeMigration();
+
+    assertEquals(0, storage.updateCount);
+  }
+
+  private void assertGenericAdapterMetadata(AdapterDescription adapter) {
+    assertEquals(NEW_APP_ID, adapter.getAppId());
+    assertEquals(1, adapter.getVersion());
+    assertTrue(adapter.isIncludesLocales());
+    assertEquals(List.of("strings.en"), adapter.getIncludedLocales());
+    assertEquals(List.of(
+        PLC_IP,
+        PLC_POLLING_INTERVAL,
+        "supported_transports",
+        "transport_metadata",
+        "protocol_metadata",
+        PLC_CODE_BLOCK
+    ), 
adapter.getConfig().stream().map(StaticProperty::getInternalName).toList());
+  }
+
+  private void assertProtocolParameter(AdapterDescription adapter,
+                                       String name,
+                                       String expectedValue) {
+    var protocolMetadata = (StaticPropertyAlternatives) 
adapter.getConfig().get(4);
+    var requiredOptions = protocolMetadata.getAlternatives().get(0);
+    var requiredGroup = (StaticPropertyGroup) 
requiredOptions.getStaticProperty();
+    var parameter = requiredGroup.getStaticProperties()
+        .stream()
+        .filter(property -> name.equals(property.getInternalName()))
+        .map(FreeTextStaticProperty.class::cast)
+        .findFirst()
+        .orElseThrow();
+
+    assertTrue(parameter.isOptional());
+    assertEquals(expectedValue, parameter.getValue());
+  }
+
+  private AdapterDescription makeAdapter(String ipAddress,
+                                         StaticProperty nodeInput) {
+    var adapter = new AdapterDescription();
+    adapter.setAppId(OLD_APP_ID);
+    adapter.setVersion(1);
+    adapter.setConfig(List.of(
+        makeFreeText(PLC_IP, ipAddress),
+        makeFreeText(PLC_POLLING_INTERVAL, "1000"),
+        nodeInput
+    ));
+    return adapter;
+  }
+
+  private CollectionStaticProperty makeNodeCollection() {
+    var collection = new CollectionStaticProperty();
+    collection.setInternalName(PLC_NODES);
+    collection.setMembers(new ArrayList<>(List.of(
+        makeNode("input", "%I0.0", "Bool"),
+        makeNode("output", "%Q0.4", "Time of day")
+    )));
+    return collection;
+  }
+
+  private StaticProperty makeNodeInputAlternatives(CollectionStaticProperty 
collection,
+                                                  CodeInputStaticProperty 
codeBlock,
+                                                  boolean collectionSelected) {
+    var alternatives = new 
StaticPropertyAlternatives(PLC_NODE_INPUT_ALTERNATIVES, "", "");
+    alternatives.setAlternatives(List.of(
+        makeAlternative(PLC_NODE_INPUT_COLLECTION_ALTERNATIVE, collection, 
collectionSelected),
+        makeAlternative(PLC_NODE_INPUT_CODE_BLOCK_ALTIVE, codeBlock, 
!collectionSelected)
+    ));
+    return alternatives;
+  }
+
+  private StaticPropertyAlternative makeAlternative(String internalName,
+                                                   StaticProperty 
staticProperty,
+                                                   boolean selected) {
+    var alternative = new StaticPropertyAlternative(internalName, "", "");
+    alternative.setStaticProperty(staticProperty);
+    alternative.setSelected(selected);
+    return alternative;
+  }
+
+  private StaticPropertyGroup makeNode(String runtimeName,
+                                       String nodeName,
+                                       String type) {
+    var group = new StaticPropertyGroup();
+    group.setStaticProperties(List.of(
+        makeFreeText(PLC_NODE_RUNTIME_NAME, runtimeName),
+        makeFreeText(PLC_NODE_NAME, nodeName),
+        makeSelection(PLC_NODE_TYPE, type)
+    ));
+    return group;
+  }
+
+  private FreeTextStaticProperty makeFreeText(String internalName,
+                                             String value) {
+    var property = new FreeTextStaticProperty();
+    property.setInternalName(internalName);
+    property.setValue(value);
+    return property;
+  }
+
+  private OneOfStaticProperty makeSelection(String internalName,
+                                            String selectedValue) {
+    var property = new OneOfStaticProperty();
+    property.setInternalName(internalName);
+    property.setOptions(List.of(new Option(selectedValue, true)));
+    return property;
+  }
+
+  private CodeInputStaticProperty makeCodeBlock(String value) {
+    var codeBlock = new CodeInputStaticProperty();
+    codeBlock.setInternalName(PLC_CODE_BLOCK);
+    codeBlock.setCodeTemplate(CODE_TEMPLATE);
+    codeBlock.setValue(value);
+    return codeBlock;
+  }
+
+  private String value(StaticProperty property) {
+    if (property instanceof FreeTextStaticProperty freeTextStaticProperty) {
+      return freeTextStaticProperty.getValue();
+    } else if (property instanceof CodeInputStaticProperty 
codeInputStaticProperty) {
+      return codeInputStaticProperty.getValue();
+    } else {
+      throw new IllegalArgumentException("Unsupported property type");
+    }
+  }
+
+  private static class InMemoryAdapterStorage implements IAdapterStorage {
+
+    private final List<AdapterDescription> adapters;
+    private AdapterDescription updatedAdapter;
+    private int updateCount;
+
+    private InMemoryAdapterStorage(AdapterDescription... adapters) {
+      this.adapters = new ArrayList<>(List.of(adapters));
+    }
+
+    @Override
+    public AdapterDescription getFirstAdapterByAppId(String appId) {
+      return getAdaptersByAppId(appId).stream().findFirst().orElse(null);
+    }
+
+    @Override
+    public List<AdapterDescription> getAdaptersByAppId(String appId) {
+      return adapters.stream().filter(adapter -> 
appId.equals(adapter.getAppId())).toList();
+    }
+
+    @Override
+    public List<AdapterDescription> findAll() {
+      return adapters;
+    }
+
+    @Override
+    public Tuple2<Boolean, String> persist(AdapterDescription element) {
+      adapters.add(element);
+      return new Tuple2<>(true, "");
+    }
+
+    @Override
+    public AdapterDescription getElementById(String id) {
+      return adapters.stream().filter(adapter -> 
id.equals(adapter.getElementId())).findFirst().orElse(null);
+    }
+
+    @Override
+    public AdapterDescription updateElement(AdapterDescription element) {
+      updatedAdapter = element;
+      updateCount++;
+      return element;
+    }
+
+    @Override
+    public void deleteElement(AdapterDescription element) {
+      adapters.remove(element);
+    }
+  }
+}


Reply via email to