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

tenthe pushed a commit to branch 
4690-deprecate-legacy-plc4x-s7-and-modbus-adapters
in repository https://gitbox.apache.org/repos/asf/streampipes.git

commit 177787060ef3a0ca292c2533d093370fd51dca6a
Author: Philipp Zehnder <[email protected]>
AuthorDate: Tue Jul 7 13:15:20 2026 +0000

    feat(#4690): migrate PLC4X S7 to generic adapter
---
 .../connectors/plc/PlcConnectorsModuleExport.java  |   2 +
 .../Plc4xS7ToGenericAdapterMigration.java          | 231 +++++++++++++++++++++
 .../Plc4xS7ToGenericAdapterMigrationTest.java      | 200 ++++++++++++++++++
 3 files changed, 433 insertions(+)

diff --git 
a/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/PlcConnectorsModuleExport.java
 
b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/PlcConnectorsModuleExport.java
index 6f53414919..73ca2d664f 100644
--- 
a/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/PlcConnectorsModuleExport.java
+++ 
b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/PlcConnectorsModuleExport.java
@@ -26,6 +26,7 @@ import 
org.apache.streampipes.extensions.api.pe.IStreamPipesPipelineElement;
 import 
org.apache.streampipes.extensions.connectors.plc.adapter.GenericAdapterGenerator;
 import 
org.apache.streampipes.extensions.connectors.plc.adapter.migration.Plc4xModbusAdapterMigrationV1;
 import 
org.apache.streampipes.extensions.connectors.plc.adapter.migration.Plc4xS7AdapterMigrationV1;
+import 
org.apache.streampipes.extensions.connectors.plc.adapter.migration.Plc4xS7ToGenericAdapterMigration;
 import 
org.apache.streampipes.extensions.connectors.plc.adapter.modbus.Plc4xModbusAdapter;
 import 
org.apache.streampipes.extensions.connectors.plc.adapter.s7.Plc4xS7Adapter;
 import 
org.apache.streampipes.extensions.connectors.plc.cache.SpCachedPlcConnectionManager;
@@ -65,6 +66,7 @@ public class PlcConnectorsModuleExport implements 
IExtensionModuleExport {
   public List<IModelMigrator<?, ?>> migrators() {
     return List.of(
         new Plc4xS7AdapterMigrationV1(),
+        new Plc4xS7ToGenericAdapterMigration(),
         new Plc4xModbusAdapterMigrationV1()
     );
   }
diff --git 
a/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/migration/Plc4xS7ToGenericAdapterMigration.java
 
b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/migration/Plc4xS7ToGenericAdapterMigration.java
new file mode 100644
index 0000000000..78c86f9ef5
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/migration/Plc4xS7ToGenericAdapterMigration.java
@@ -0,0 +1,231 @@
+/*
+ * 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.plc.adapter.migration;
+
+import 
org.apache.streampipes.extensions.api.extractor.IStaticPropertyExtractor;
+import org.apache.streampipes.extensions.api.migration.IAdapterMigrator;
+import 
org.apache.streampipes.extensions.connectors.plc.adapter.generic.config.AdapterConfigurationProvider;
+import 
org.apache.streampipes.extensions.connectors.plc.adapter.generic.model.Plc4xLabels;
+import org.apache.streampipes.model.connect.adapter.AdapterDescription;
+import org.apache.streampipes.model.extensions.svcdiscovery.SpServiceTagPrefix;
+import org.apache.streampipes.model.migration.MigrationResult;
+import org.apache.streampipes.model.migration.ModelMigratorConfig;
+import org.apache.streampipes.model.staticproperty.CollectionStaticProperty;
+import org.apache.streampipes.model.staticproperty.Option;
+import org.apache.streampipes.model.staticproperty.StaticProperty;
+import org.apache.streampipes.model.staticproperty.StaticPropertyGroup;
+import org.apache.streampipes.sdk.StaticProperties;
+import org.apache.streampipes.sdk.extractor.StaticPropertyExtractor;
+import org.apache.streampipes.sdk.helpers.Alternatives;
+import org.apache.streampipes.sdk.helpers.CodeLanguage;
+import org.apache.streampipes.sdk.helpers.Labels;
+import org.apache.streampipes.sdk.utils.Datatypes;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+public class Plc4xS7ToGenericAdapterMigration implements IAdapterMigrator {
+
+  private static final String OLD_APP_ID = 
"org.apache.streampipes.connect.iiot.adapters.plc4x.s7";
+  static final String NEW_APP_ID = AdapterConfigurationProvider.ID + "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_COLLECTION_ALTERNATIVE = 
"plc_node_input_collection_alternative";
+
+  @Override
+  public ModelMigratorConfig config() {
+    return new ModelMigratorConfig(
+        OLD_APP_ID,
+        SpServiceTagPrefix.ADAPTER,
+        1,
+        1
+    );
+  }
+
+  @Override
+  public MigrationResult<AdapterDescription> migrate(AdapterDescription 
element,
+                                                     IStaticPropertyExtractor 
extractor) throws RuntimeException {
+    var ipAddress = extractor.singleValueParameter(PLC_IP, String.class);
+    var pollingInterval = extractor.singleValueParameter(PLC_POLLING_INTERVAL, 
Integer.class);
+
+    var splitAddress = SplitPlcAddress.from(ipAddress);
+
+    element.setAppId(NEW_APP_ID);
+    element.setConfig(List.of(
+        
StaticProperties.stringFreeTextProperty(Labels.withId(Plc4xLabels.PLC_IP), 
splitAddress.host()),
+        
StaticProperties.integerFreeTextProperty(Labels.withId(Plc4xLabels.PLC_POLLING_INTERVAL),
 pollingInterval),
+        makeSupportedTransports(),
+        makeTransportMetadata(),
+        makeProtocolMetadata(splitAddress.queryParameters()),
+        makeCodeBlock(extractor)
+    ));
+
+    return MigrationResult.success(element);
+  }
+
+  private StaticProperty makeSupportedTransports() {
+    return StaticProperties.singleValueSelection(
+        Labels.withId(Plc4xLabels.SUPPORTED_TRANSPORTS),
+        List.of(new Option("tcp", true))
+    );
+  }
+
+  private StaticProperty makeTransportMetadata() {
+    return StaticProperties.alternatives(
+        Labels.withId(Plc4xLabels.TRANSPORT_METADATA),
+        Alternatives.from(
+            Labels.withId(Plc4xLabels.REQUIRED_OPTIONS),
+            makeRuntimeResolvableGroup(Plc4xLabels.REQUIRED_GROUP_TRANSPORT),
+            true
+        ),
+        Alternatives.from(
+            Labels.withId(Plc4xLabels.ADVANCED_OPTIONS),
+            makeRuntimeResolvableGroup(Plc4xLabels.ADVANCED_GROUP_TRANSPORT)
+        )
+    );
+  }
+
+  private StaticProperty makeRuntimeResolvableGroup(String internalName) {
+    var group = StaticProperties.runtimeResolvableGroupStaticProperty(
+        Labels.withId(internalName),
+        List.of(Plc4xLabels.SUPPORTED_TRANSPORTS)
+    );
+    group.setStaticProperties(List.of());
+    group.setHorizontalRendering(false);
+    return group;
+  }
+
+  private StaticProperty makeProtocolMetadata(List<QueryParameter> 
queryParameters) {
+    return StaticProperties.alternatives(
+        Labels.withId(Plc4xLabels.PROTOCOL_METADATA),
+        Alternatives.from(
+            Labels.withId(Plc4xLabels.REQUIRED_OPTIONS),
+            makeProtocolGroup(Plc4xLabels.REQUIRED_GROUP_PROTOCOL, 
queryParameters),
+            true
+        ),
+        Alternatives.from(
+            Labels.withId(Plc4xLabels.ADVANCED_OPTIONS),
+            makeProtocolGroup(Plc4xLabels.ADVANCED_GROUP_PROTOCOL, List.of())
+        )
+    );
+  }
+
+  private StaticProperty makeProtocolGroup(String internalName,
+                                           List<QueryParameter> 
queryParameters) {
+    var group = StaticProperties.group(
+        Labels.withId(internalName),
+        false,
+        queryParameters.stream()
+            .map(this::makeProtocolParameter)
+            .toArray(StaticProperty[]::new)
+    );
+    group.setHorizontalRendering(false);
+    return group;
+  }
+
+  private StaticProperty makeProtocolParameter(QueryParameter queryParameter) {
+    var property = StaticProperties.freeTextProperty(
+        Labels.from(queryParameter.name(), queryParameter.name(), ""),
+        Datatypes.String
+    );
+    property.setOptional(true);
+    property.setValue(queryParameter.value());
+    return property;
+  }
+
+  private StaticProperty makeCodeBlock(IStaticPropertyExtractor extractor) {
+    var codeBlock = StaticProperties.codeStaticProperty(
+        Labels.withId(Plc4xLabels.PLC_CODE_BLOCK),
+        CodeLanguage.None,
+        ""
+    );
+    codeBlock.setValue(makeTags(extractor));
+    return codeBlock;
+  }
+
+  private String makeTags(IStaticPropertyExtractor extractor) {
+    var selectedAlternative = 
extractor.selectedAlternativeInternalId(PLC_NODE_INPUT_ALTERNATIVES);
+    if (Objects.equals(selectedAlternative, 
PLC_NODE_INPUT_COLLECTION_ALTERNATIVE)) {
+      var nodes = extractor.getStaticPropertyByName(PLC_NODES, 
CollectionStaticProperty.class);
+      return makeTagsFromCollection(nodes);
+    } else {
+      return extractor.codeblockValue(Plc4xLabels.PLC_CODE_BLOCK);
+    }
+  }
+
+  private String makeTagsFromCollection(CollectionStaticProperty nodes) {
+    var tags = new ArrayList<String>();
+    for (StaticProperty member : nodes.getMembers()) {
+      var memberExtractor =
+          StaticPropertyExtractor.from(((StaticPropertyGroup) 
member).getStaticProperties(), new ArrayList<>());
+      tags.add("%s=%s:%s".formatted(
+          memberExtractor.textParameter(PLC_NODE_RUNTIME_NAME),
+          memberExtractor.textParameter(PLC_NODE_NAME),
+          memberExtractor.selectedSingleValue(PLC_NODE_TYPE, String.class)
+              .toUpperCase()
+              .replaceAll(" ", "_")
+      ));
+    }
+    return String.join(System.lineSeparator(), tags);
+  }
+
+  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]);
+    }
+  }
+}
diff --git 
a/streampipes-extensions/streampipes-connectors-plc/src/test/java/org/apache/streampipes/extensions/connectors/plc/adapter/migration/Plc4xS7ToGenericAdapterMigrationTest.java
 
b/streampipes-extensions/streampipes-connectors-plc/src/test/java/org/apache/streampipes/extensions/connectors/plc/adapter/migration/Plc4xS7ToGenericAdapterMigrationTest.java
new file mode 100644
index 0000000000..9909db9877
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-plc/src/test/java/org/apache/streampipes/extensions/connectors/plc/adapter/migration/Plc4xS7ToGenericAdapterMigrationTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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.plc.adapter.migration;
+
+import 
org.apache.streampipes.extensions.connectors.plc.adapter.generic.model.Plc4xConnectionExtractor;
+import 
org.apache.streampipes.extensions.connectors.plc.adapter.generic.model.Plc4xLabels;
+import org.apache.streampipes.model.connect.adapter.AdapterDescription;
+import org.apache.streampipes.model.staticproperty.Option;
+import org.apache.streampipes.model.staticproperty.StaticProperty;
+import org.apache.streampipes.sdk.StaticProperties;
+import org.apache.streampipes.sdk.extractor.StaticPropertyExtractor;
+import org.apache.streampipes.sdk.helpers.Alternatives;
+import org.apache.streampipes.sdk.helpers.CodeLanguage;
+import org.apache.streampipes.sdk.helpers.Labels;
+import org.apache.streampipes.sdk.helpers.Options;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class Plc4xS7ToGenericAdapterMigrationTest {
+
+  private static final String OLD_APP_ID = 
"org.apache.streampipes.connect.iiot.adapters.plc4x.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 Plc4xS7ToGenericAdapterMigration migration;
+
+  @BeforeEach
+  void setUp() {
+    this.migration = new Plc4xS7ToGenericAdapterMigration();
+  }
+
+  @Test
+  void migrateSimpleCollectionToGenericS7Adapter() {
+    var adapter = makeAdapter(
+        "192.168.34.56?remote-rack=0&remote-slot=3",
+        makeCollectionAlternative()
+    );
+
+    var result = migration.migrate(adapter, 
StaticPropertyExtractor.from(adapter.getConfig()));
+    var migratedAdapter = result.element();
+    var settings = new Plc4xConnectionExtractor(
+        StaticPropertyExtractor.from(migratedAdapter.getConfig()),
+        "s7"
+    ).makeSettings();
+
+    assertEquals(Plc4xS7ToGenericAdapterMigration.NEW_APP_ID, 
migratedAdapter.getAppId());
+    
assertTrue(settings.connectionString().startsWith("s7-light:tcp://192.168.34.56?"));
+    assertTrue(Arrays.asList(settings.connectionString().split("\\?", 
2)[1].split("&"))
+        .containsAll(List.of("remote-rack=0", "remote-slot=3")));
+    assertEquals(1000, settings.pollingInterval());
+    assertEquals("%I0.0:BOOL", settings.nodes().get("input"));
+    assertEquals("%Q0.4:TIME_OF_DAY", settings.nodes().get("output"));
+  }
+
+  @Test
+  void migrateAdvancedCodeBlockToGenericS7Adapter() {
+    var codeBlock = """
+        temperature=%I0.0:INT
+        pressure=%Q0.4:REAL
+        """;
+    var adapter = makeAdapter(
+        "192.168.34.56",
+        makeCodeBlockAlternative(codeBlock)
+    );
+
+    var result = migration.migrate(adapter, 
StaticPropertyExtractor.from(adapter.getConfig()));
+    var migratedAdapter = result.element();
+    var settings = new Plc4xConnectionExtractor(
+        StaticPropertyExtractor.from(migratedAdapter.getConfig()),
+        "s7"
+    ).makeSettings();
+
+    assertEquals(Plc4xS7ToGenericAdapterMigration.NEW_APP_ID, 
migratedAdapter.getAppId());
+    assertEquals("s7-light:tcp://192.168.34.56", settings.connectionString());
+    assertEquals("%I0.0:INT", settings.nodes().get("temperature"));
+    assertEquals("%Q0.4:REAL", settings.nodes().get("pressure"));
+  }
+
+  private AdapterDescription makeAdapter(StaticProperty nodeInputAlternative) {
+    return makeAdapter("192.168.34.56", nodeInputAlternative);
+  }
+
+  private AdapterDescription makeAdapter(String ipAddress,
+                                         StaticProperty nodeInputAlternative) {
+    var adapter = new AdapterDescription();
+    adapter.setAppId(OLD_APP_ID);
+    adapter.setVersion(1);
+    adapter.setConfig(List.of(
+        StaticProperties.stringFreeTextProperty(Labels.withId(PLC_IP), 
ipAddress),
+        
StaticProperties.integerFreeTextProperty(Labels.withId(PLC_POLLING_INTERVAL), 
1000),
+        nodeInputAlternative
+    ));
+    return adapter;
+  }
+
+  private StaticProperty makeCollectionAlternative() {
+    var collection = StaticProperties.collection(
+        Labels.withId(PLC_NODES),
+        
StaticProperties.stringFreeTextProperty(Labels.withId(PLC_NODE_RUNTIME_NAME)),
+        StaticProperties.stringFreeTextProperty(Labels.withId(PLC_NODE_NAME)),
+        StaticProperties.singleValueSelection(
+            Labels.withId(PLC_NODE_TYPE),
+            Options.from("Bool", "Time of day")
+        )
+    );
+    collection.setMembers(new ArrayList<>(List.of(
+        makeNode("input", "%I0.0", "Bool"),
+        makeNode("output", "%Q0.4", "Time of day")
+    )));
+
+    return StaticProperties.alternatives(
+        Labels.withId(PLC_NODE_INPUT_ALTERNATIVES),
+        Alternatives.from(
+            Labels.withId(PLC_NODE_INPUT_COLLECTION_ALTERNATIVE),
+            collection,
+            true
+        ),
+        Alternatives.from(
+            Labels.withId(PLC_NODE_INPUT_CODE_BLOCK_ALTIVE),
+            StaticProperties.codeStaticProperty(
+                Labels.withId(Plc4xLabels.PLC_CODE_BLOCK),
+                CodeLanguage.None,
+                ""
+            )
+        )
+    );
+  }
+
+  private StaticProperty makeCodeBlockAlternative(String codeBlockValue) {
+    var codeBlock = StaticProperties.codeStaticProperty(
+        Labels.withId(Plc4xLabels.PLC_CODE_BLOCK),
+        CodeLanguage.None,
+        ""
+    );
+    codeBlock.setValue(codeBlockValue);
+
+    return StaticProperties.alternatives(
+        Labels.withId(PLC_NODE_INPUT_ALTERNATIVES),
+        Alternatives.from(
+            Labels.withId(PLC_NODE_INPUT_COLLECTION_ALTERNATIVE),
+            StaticProperties.collection(
+                Labels.withId(PLC_NODES),
+                
StaticProperties.stringFreeTextProperty(Labels.withId(PLC_NODE_RUNTIME_NAME))
+            )
+        ),
+        Alternatives.from(
+            Labels.withId(PLC_NODE_INPUT_CODE_BLOCK_ALTIVE),
+            codeBlock,
+            true
+        )
+    );
+  }
+
+  private StaticProperty makeNode(String runtimeName,
+                                  String nodeName,
+                                  String type) {
+    return StaticProperties.group(
+        Labels.withId("node"),
+        
StaticProperties.stringFreeTextProperty(Labels.withId(PLC_NODE_RUNTIME_NAME), 
runtimeName),
+        StaticProperties.stringFreeTextProperty(Labels.withId(PLC_NODE_NAME), 
nodeName),
+        StaticProperties.singleValueSelection(
+            Labels.withId(PLC_NODE_TYPE),
+            List.of(
+                new Option(type, true)
+            )
+        )
+    );
+  }
+}

Reply via email to