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

chrisdutz pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git


The following commit(s) were added to refs/heads/develop by this push:
     new 537d756954 refactor(plc4j/spi): Updated the connection base to not 
blidnly report supporting all 4 types of connection-operations.
537d756954 is described below

commit 537d756954df4f08306373eb8a5cf0981d402e5d
Author: Christofer Dutz <[email protected]>
AuthorDate: Sun Aug 16 15:58:07 2026 +0200

    refactor(plc4j/spi): Updated the connection base to not blidnly report 
supporting all 4 types of connection-operations.
---
 .../plc4x/java/spi/drivers/ConnectionBase.java     |  56 +++++-
 .../spi/drivers/ConnectionBaseMetadataTest.java    | 190 +++++++++++++++++++++
 2 files changed, 242 insertions(+), 4 deletions(-)

diff --git 
a/plc4j/spi/drivers/src/main/java/org/apache/plc4x/java/spi/drivers/ConnectionBase.java
 
b/plc4j/spi/drivers/src/main/java/org/apache/plc4x/java/spi/drivers/ConnectionBase.java
index 1d958f9e9a..5382941202 100644
--- 
a/plc4j/spi/drivers/src/main/java/org/apache/plc4x/java/spi/drivers/ConnectionBase.java
+++ 
b/plc4j/spi/drivers/src/main/java/org/apache/plc4x/java/spi/drivers/ConnectionBase.java
@@ -239,31 +239,79 @@ public abstract class ConnectionBase<C extends 
Configuration> implements PlcConn
         return authentication;
     }
 
+    /**
+     * Derived from the operations the driver actually implements.
+     * <p>
+     * Every {@code on*} hook below has a default that fails the request with 
"... not supported",
+     * so a driver only supports an operation if it overrides the 
corresponding hook. Reporting
+     * a fixed {@code true} here - as this class used to - told every caller 
that every driver
+     * supports everything, which is wrong for most of them: a driver that 
connects but implements
+     * nothing claimed full support, and callers that check the metadata 
before deciding what to do
+     * were sent into requests that could only fail.
+     * <p>
+     * The lookup walks the class hierarchy up to (but excluding) {@link 
ConnectionBase}, so a hook
+     * inherited from an intermediate base counts as implemented - {@code 
PollingSubscriptionConnectionBase}
+     * for instance provides {@code onSubscribe} for drivers whose protocol 
has no native subscriptions.
+     * <p>
+     * What this can tell is what the <em>driver</em> implements, which is a 
property of the class.
+     * Where the answer depends on the device at the other end - a controller 
that does not offer an
+     * operation the driver otherwise speaks - only the driver can know, so it 
should override this
+     * method and answer from what it learned during connect. This derivation 
is the default for
+     * drivers that have no such distinction to make, and the result is 
deliberately computed per
+     * call so an override is free to vary per connection.
+     */
     @Override
     public PlcConnectionMetadata getMetadata() {
+        return deriveMetadata(getClass());
+    }
+
+    private static PlcConnectionMetadata deriveMetadata(Class<?> 
connectionClass) {
+        boolean read = overridesHook(connectionClass, "onRead", 
PlcReadRequest.class);
+        boolean write = overridesHook(connectionClass, "onWrite", 
PlcWriteRequest.class);
+        boolean subscribe = overridesHook(connectionClass, "onSubscribe", 
PlcSubscriptionRequest.class);
+        boolean browse = overridesHook(connectionClass, "onBrowse", 
PlcBrowseRequest.class)
+            || overridesHook(connectionClass, "onBrowseWithInterceptor", 
PlcBrowseRequest.class, PlcBrowseRequestInterceptor.class);
+        LOGGER.debug("Derived metadata for {}: read={}, write={}, 
subscribe={}, browse={}",
+            connectionClass.getSimpleName(), read, write, subscribe, browse);
         return new PlcConnectionMetadata() {
             @Override
             public boolean isReadSupported() {
-                return true;
+                return read;
             }
 
             @Override
             public boolean isWriteSupported() {
-                return true;
+                return write;
             }
 
             @Override
             public boolean isSubscribeSupported() {
-                return true;
+                return subscribe;
             }
 
             @Override
             public boolean isBrowseSupported() {
-                return true;
+                return browse;
             }
         };
     }
 
+    /**
+     * Whether {@code connectionClass} or any class between it and {@link 
ConnectionBase} declares
+     * the given hook - i.e. whether the default "not supported" 
implementation has been replaced.
+     */
+    private static boolean overridesHook(Class<?> connectionClass, String 
name, Class<?>... parameterTypes) {
+        for (Class<?> current = connectionClass; (current != null) && (current 
!= ConnectionBase.class); current = current.getSuperclass()) {
+            try {
+                current.getDeclaredMethod(name, parameterTypes);
+                return true;
+            } catch (NoSuchMethodException e) {
+                // Not declared here - keep walking up towards ConnectionBase.
+            }
+        }
+        return false;
+    }
+
     @Override
     public String getProtocolCode() {
         return protocolCode;
diff --git 
a/plc4j/spi/drivers/src/test/java/org/apache/plc4x/java/spi/drivers/ConnectionBaseMetadataTest.java
 
b/plc4j/spi/drivers/src/test/java/org/apache/plc4x/java/spi/drivers/ConnectionBaseMetadataTest.java
new file mode 100644
index 0000000000..5c58967fbb
--- /dev/null
+++ 
b/plc4j/spi/drivers/src/test/java/org/apache/plc4x/java/spi/drivers/ConnectionBaseMetadataTest.java
@@ -0,0 +1,190 @@
+/*
+ * 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
+ *
+ *   https://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.plc4x.java.spi.drivers;
+
+import org.apache.plc4x.java.api.messages.PlcBrowseRequest;
+import org.apache.plc4x.java.api.messages.PlcBrowseResponse;
+import org.apache.plc4x.java.api.messages.PlcReadRequest;
+import org.apache.plc4x.java.api.messages.PlcReadResponse;
+import org.apache.plc4x.java.api.messages.PlcSubscriptionRequest;
+import org.apache.plc4x.java.api.messages.PlcSubscriptionResponse;
+import org.apache.plc4x.java.api.messages.PlcWriteRequest;
+import org.apache.plc4x.java.api.messages.PlcWriteResponse;
+import org.apache.plc4x.java.api.metadata.PlcConnectionMetadata;
+import org.apache.plc4x.java.spi.config.Configuration;
+import org.apache.plc4x.java.spi.drivers.tags.PlcTagHandler;
+import org.apache.plc4x.java.spi.values.PlcValueHandler;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.CompletableFuture;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The connection metadata is derived from the operations a driver implements. 
It used to be a fixed
+ * "everything is supported", which meant a driver that implements nothing 
still claimed to support
+ * reading, writing, subscribing and browsing.
+ */
+class ConnectionBaseMetadataTest {
+
+    /**
+     * A driver that implements none of the operations - it can connect and 
nothing else.
+     */
+    @Test
+    void reportsNothingForAConnectionThatImplementsNoOperation() {
+        PlcConnectionMetadata metadata = new EmptyConnection().getMetadata();
+
+        assertFalse(metadata.isReadSupported());
+        assertFalse(metadata.isWriteSupported());
+        assertFalse(metadata.isSubscribeSupported());
+        assertFalse(metadata.isBrowseSupported());
+    }
+
+    @Test
+    void reportsExactlyTheOperationsTheDriverImplements() {
+        PlcConnectionMetadata metadata = new 
ReadWriteConnection().getMetadata();
+
+        assertTrue(metadata.isReadSupported());
+        assertTrue(metadata.isWriteSupported());
+        assertFalse(metadata.isSubscribeSupported(), "onSubscribe is not 
overridden");
+        assertFalse(metadata.isBrowseSupported(), "onBrowse is not 
overridden");
+    }
+
+    /**
+     * An operation provided by an intermediate base class counts as 
implemented. This is how
+     * PollingSubscriptionConnectionBase gives protocols without native 
subscriptions (Modbus and
+     * friends) a working subscribe.
+     */
+    @Test
+    void countsAnOperationInheritedFromAnIntermediateBase() {
+        PlcConnectionMetadata metadata = new 
InheritsSubscribeConnection().getMetadata();
+
+        assertTrue(metadata.isSubscribeSupported(), "onSubscribe comes from 
the intermediate base");
+        assertFalse(metadata.isReadSupported());
+    }
+
+    /**
+     * Browsing counts when either browse hook is implemented.
+     */
+    @Test
+    void countsBrowseFromTheInterceptorHookAsWell() {
+        assertTrue(new BrowseConnection().getMetadata().isBrowseSupported());
+    }
+
+    /**
+     * A driver whose capabilities depend on the device it connected to 
overrides this and answers
+     * from what it learned during connect. Nothing in the base class may 
cache across connections
+     * and defeat that.
+     */
+    @Test
+    void letsADriverAnswerPerConnection() {
+        DeviceDependentConnection limited = new 
DeviceDependentConnection(false);
+        DeviceDependentConnection capable = new 
DeviceDependentConnection(true);
+
+        assertFalse(limited.getMetadata().isWriteSupported());
+        assertTrue(capable.getMetadata().isWriteSupported(),
+            "two connections of the same class must be able to answer 
differently");
+    }
+
+    private abstract static class TestConnection extends 
ConnectionBase<Configuration> {
+        TestConnection() {
+            super(null, null, null);
+        }
+
+        @Override
+        public PlcTagHandler getTagHandler() {
+            return null;
+        }
+
+        @Override
+        public PlcValueHandler getValueHandler() {
+            return null;
+        }
+    }
+
+    private static class EmptyConnection extends TestConnection {
+    }
+
+    private static class ReadWriteConnection extends TestConnection {
+        @Override
+        protected CompletableFuture<PlcReadResponse> onRead(PlcReadRequest 
readRequest) {
+            return CompletableFuture.completedFuture(null);
+        }
+
+        @Override
+        protected CompletableFuture<PlcWriteResponse> onWrite(PlcWriteRequest 
writeRequest) {
+            return CompletableFuture.completedFuture(null);
+        }
+    }
+
+    private abstract static class SubscribingBase extends TestConnection {
+        @Override
+        protected CompletableFuture<PlcSubscriptionResponse> 
onSubscribe(PlcSubscriptionRequest subscriptionRequest) {
+            return CompletableFuture.completedFuture(null);
+        }
+    }
+
+    private static class InheritsSubscribeConnection extends SubscribingBase {
+    }
+
+    private static class BrowseConnection extends TestConnection {
+        @Override
+        protected CompletableFuture<PlcBrowseResponse> 
onBrowse(PlcBrowseRequest browseRequest) {
+            return CompletableFuture.completedFuture(null);
+        }
+    }
+
+    /**
+     * Speaks writing, but whether the device accepts it is only known once 
connected.
+     */
+    private static class DeviceDependentConnection extends ReadWriteConnection 
{
+        private final boolean deviceAllowsWriting;
+
+        DeviceDependentConnection(boolean deviceAllowsWriting) {
+            this.deviceAllowsWriting = deviceAllowsWriting;
+        }
+
+        @Override
+        public PlcConnectionMetadata getMetadata() {
+            PlcConnectionMetadata derived = super.getMetadata();
+            return new PlcConnectionMetadata() {
+                @Override
+                public boolean isReadSupported() {
+                    return derived.isReadSupported();
+                }
+
+                @Override
+                public boolean isWriteSupported() {
+                    return derived.isWriteSupported() && deviceAllowsWriting;
+                }
+
+                @Override
+                public boolean isSubscribeSupported() {
+                    return derived.isSubscribeSupported();
+                }
+
+                @Override
+                public boolean isBrowseSupported() {
+                    return derived.isBrowseSupported();
+                }
+            };
+        }
+    }
+}

Reply via email to