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);
+ }
+ }
+}