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 2631a7ccb6 feat: Added browse support for PLC4J OPC-UA
2631a7ccb6 is described below

commit 2631a7ccb65dc75911e306bd9be133e2a1f89750
Author: Christofer Dutz <[email protected]>
AuthorDate: Mon Jul 13 16:33:18 2026 +0200

    feat: Added browse support for PLC4J OPC-UA
---
 .../apache/plc4x/java/opcua/OpcuaConnection.java   | 209 ++++++++++++++++++++-
 .../apache/plc4x/java/opcua/OpcuaPlcDriver.java    |   5 +
 .../plc4x/java/opcua/tag/OpcuaPlcTagHandler.java   |   3 +-
 .../{OpcuaPlcTagHandler.java => OpcuaQuery.java}   |  26 ++-
 .../plc4x/java/opcua/OpcuaPlcDriverTest.java       |  64 +++++++
 .../opcua/manual/ManualOpcUaS71500NewFWBrowse.java |  84 +++++++++
 6 files changed, 375 insertions(+), 16 deletions(-)

diff --git 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaConnection.java
 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaConnection.java
index ddd84e6d3a..d2612e9fad 100644
--- 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaConnection.java
+++ 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaConnection.java
@@ -27,18 +27,17 @@ import java.util.Map.Entry;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
 import org.apache.plc4x.java.api.authentication.PlcAuthentication;
 import 
org.apache.plc4x.java.api.authentication.PlcUsernamePasswordAuthentication;
 import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
 import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
 import org.apache.plc4x.java.api.messages.*;
-import org.apache.plc4x.java.api.metadata.Metadata;
-import org.apache.plc4x.java.api.metadata.time.TimeSource;
 import org.apache.plc4x.java.api.model.PlcConsumerRegistration;
+import org.apache.plc4x.java.api.model.PlcQuery;
 import org.apache.plc4x.java.api.model.PlcSubscriptionHandle;
 import org.apache.plc4x.java.api.model.PlcTag;
 import org.apache.plc4x.java.api.types.PlcResponseCode;
+import org.apache.plc4x.java.api.types.PlcSubscriptionType;
 import org.apache.plc4x.java.api.types.PlcValueType;
 import org.apache.plc4x.java.api.value.PlcValue;
 import org.apache.plc4x.java.opcua.config.OpcuaConfiguration;
@@ -49,9 +48,8 @@ import org.apache.plc4x.java.opcua.context.SecureChannel;
 import org.apache.plc4x.java.opcua.protocol.OpcuaSubscriptionHandle;
 import org.apache.plc4x.java.opcua.readwrite.*;
 import org.apache.plc4x.java.opcua.tag.OpcuaPlcTagHandler;
-import org.apache.plc4x.java.opcua.tag.OpcuaQualityStatus;
+import org.apache.plc4x.java.opcua.tag.OpcuaQuery;
 import org.apache.plc4x.java.opcua.tag.OpcuaTag;
-import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException;
 import org.apache.plc4x.java.spi.drivers.ConnectionBase;
 import org.apache.plc4x.java.spi.drivers.exceptions.MessageCodecException;
 import org.apache.plc4x.java.spi.drivers.tags.PlcTagHandler;
@@ -74,6 +72,7 @@ import java.util.*;
 import java.util.concurrent.CompletableFuture;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 public class OpcuaConnection extends ConnectionBase<OpcuaConfiguration> 
implements OpcuaWire {
 
@@ -374,6 +373,206 @@ public class OpcuaConnection extends 
ConnectionBase<OpcuaConfiguration> implemen
         return response;
     }
 
+    // The standard "Objects" folder — the usual entry point into a server's 
address space.
+    private static final String OBJECTS_FOLDER_ADDRESS = "ns=0;i=85";
+    // HierarchicalReferences (i=33): browsing only these (plus subtypes) 
yields the clean
+    // containment tree (Organizes / HasComponent / HasProperty, ...) instead 
of every
+    // reference type (which would drag in type definitions and other 
non-containment noise).
+    private static final NodeId HIERARCHICAL_REFERENCES = new NodeId(new 
NodeIdNumeric(0, 33L));
+    // resultMask 0x3F -> return all reference fields (referenceType, 
isForward, nodeClass,
+    // browseName, displayName, typeDefinition).
+    private static final long BROWSE_RESULT_MASK_ALL = 0x3FL;
+
+    @Override
+    protected CompletableFuture<PlcBrowseResponse> onBrowse(PlcBrowseRequest 
browseRequest) {
+        return onBrowseWithInterceptor(browseRequest, (queryName, query, item) 
-> true);
+    }
+
+    @Override
+    protected CompletableFuture<PlcBrowseResponse> 
onBrowseWithInterceptor(PlcBrowseRequest browseRequest,
+                                                                           
PlcBrowseRequestInterceptor interceptor) {
+        Map<String, PlcResponseCode> responseCodes = new ConcurrentHashMap<>();
+        Map<String, List<PlcBrowseItem>> values = new ConcurrentHashMap<>();
+        List<CompletableFuture<?>> queryFutures = new ArrayList<>();
+
+        for (String queryName : browseRequest.getQueryNames()) {
+            PlcQuery query = browseRequest.getQuery(queryName);
+            String startAddress = query.getQueryString();
+            // An empty query or the "**" wildcard means "browse everything": 
start at the
+            // standard Objects folder and recurse the whole sub-tree. 
Otherwise the query is
+            // the address of the node to start browsing from.
+            if (startAddress == null || startAddress.isBlank() || 
"**".equals(startAddress.trim())) {
+                startAddress = OBJECTS_FOLDER_ADDRESS;
+            }
+            NodeId startNodeId;
+            try {
+                startNodeId = generateNodeId(OpcuaTag.of(startAddress));
+            } catch (Exception e) {
+                LOGGER.warn("Invalid browse start node '{}' for query '{}'", 
startAddress, queryName, e);
+                responseCodes.put(queryName, PlcResponseCode.INVALID_ADDRESS);
+                values.put(queryName, Collections.emptyList());
+                continue;
+            }
+            // A shared, global set of already-visited nodes provides cycle 
detection (a node
+            // is never expanded twice) and keeps a well-formed tree from 
being duplicated.
+            Set<String> visited = ConcurrentHashMap.newKeySet();
+            queryFutures.add(
+                browseNode(startNodeId, visited, queryName, query, interceptor)
+                    .handle((items, error) -> {
+                        if (error != null) {
+                            LOGGER.warn("Browsing failed for query '{}'", 
queryName, error);
+                            responseCodes.put(queryName, 
PlcResponseCode.INTERNAL_ERROR);
+                            values.put(queryName, Collections.emptyList());
+                        } else {
+                            responseCodes.put(queryName, PlcResponseCode.OK);
+                            values.put(queryName, items);
+                        }
+                        return null;
+                    }));
+        }
+
+        return CompletableFuture.allOf(queryFutures.toArray(new 
CompletableFuture[0]))
+            .thenApply(v -> new DefaultPlcBrowseResponse(browseRequest, 
responseCodes, values));
+    }
+
+    /**
+     * Browses the forward hierarchical references of {@code nodeId} and 
returns each referenced
+     * node as a {@link PlcBrowseItem}, recursing into every not-yet-visited 
child so the full
+     * sub-tree is returned. {@code visited} guards against reference cycles.
+     */
+    private CompletableFuture<List<PlcBrowseItem>> browseNode(NodeId nodeId, 
Set<String> visited, String queryName,
+                                                              PlcQuery query, 
PlcBrowseRequestInterceptor interceptor) {
+        return browseReferences(nodeId, null, new 
ArrayList<>()).thenCompose(references -> {
+            List<CompletableFuture<PlcBrowseItem>> itemFutures = new 
ArrayList<>();
+            for (ReferenceDescription reference : references) {
+                String childAddress = addressOf(reference.getNodeId());
+                if (childAddress == null) {
+                    // Node identifier form we can't turn back into an address 
(e.g. opaque) — skip.
+                    continue;
+                }
+                CompletableFuture<Map<String, PlcBrowseItem>> childrenFuture;
+                if (visited.add(childAddress)) {
+                    childrenFuture = 
browseNode(generateNodeId(OpcuaTag.of(childAddress)),
+                        visited, queryName, query, interceptor)
+                        .thenApply(childItems -> childItems.stream()
+                            .collect(Collectors.toMap(PlcBrowseItem::getName, 
item -> item,
+                                (a, b) -> a, LinkedHashMap::new)));
+                } else {
+                    // Already visited (cycle or shared node) — list it but 
don't expand again.
+                    childrenFuture = 
CompletableFuture.completedFuture(Collections.emptyMap());
+                }
+                itemFutures.add(childrenFuture.thenApply(children -> 
buildBrowseItem(reference, childAddress, children)));
+            }
+            return CompletableFuture.allOf(itemFutures.toArray(new 
CompletableFuture[0]))
+                .thenApply(v -> itemFutures.stream()
+                    .map(CompletableFuture::join)
+                    .filter(item -> interceptor.intercept(queryName, query, 
item))
+                    .collect(Collectors.toList()));
+        });
+    }
+
+    /**
+     * Issues a single Browse (or BrowseNext when {@code continuationPoint} is 
set) and follows
+     * continuation points until the server has returned all references of the 
node.
+     */
+    private CompletableFuture<List<ReferenceDescription>> 
browseReferences(NodeId nodeId, PascalByteString continuationPoint,
+                                                                           
List<ReferenceDescription> accumulator) {
+        CompletableFuture<BrowseResult> resultFuture;
+        if (continuationPoint == null) {
+            BrowseDescription description = new BrowseDescription(
+                nodeId,
+                BrowseDirection.browseDirectionForward,
+                HIERARCHICAL_REFERENCES,
+                true,               // include reference subtypes
+                0L,                 // nodeClassMask 0 -> all node classes
+                BROWSE_RESULT_MASK_ALL);
+            BrowseRequest request = new BrowseRequest(
+                conversation.createRequestHeader(),
+                new ViewDescription(new NodeId(new NodeIdTwoByte((short) 0)), 
0L, 0L),
+                0L,                 // requestedMaxReferencesPerNode 0 -> 
server decides
+                Collections.singletonList(description));
+            resultFuture = conversation.submit(request, BrowseResponse.class)
+                .thenApply(response -> response.getResults().get(0));
+        } else {
+            BrowseNextRequest request = new BrowseNextRequest(
+                conversation.createRequestHeader(),
+                false,              // don't release the continuation point, 
we want the next batch
+                Collections.singletonList(continuationPoint));
+            resultFuture = conversation.submit(request, 
BrowseNextResponse.class)
+                .thenApply(response -> response.getResults().get(0));
+        }
+        return resultFuture.thenCompose(result -> {
+            if (result.getReferences() != null) {
+                accumulator.addAll(result.getReferences());
+            }
+            PascalByteString nextContinuationPoint = 
result.getContinuationPoint();
+            if (nextContinuationPoint != null && 
nextContinuationPoint.getStringLength() > 0) {
+                return browseReferences(nodeId, nextContinuationPoint, 
accumulator);
+            }
+            return CompletableFuture.completedFuture(accumulator);
+        });
+    }
+
+    private PlcBrowseItem buildBrowseItem(ReferenceDescription reference, 
String address, Map<String, PlcBrowseItem> children) {
+        OpcuaTag tag = OpcuaTag.of(address);
+        NodeClass nodeClass = reference.getNodeClass();
+        boolean isVariable = nodeClass == NodeClass.nodeClassVariable;
+        String name = localizedTextValue(reference.getDisplayName());
+        if (name == null || name.isEmpty()) {
+            name = qualifiedNameValue(reference.getBrowseName());
+        }
+
+        Map<String, PlcValue> options = new HashMap<>();
+        if (nodeClass != null) {
+            options.put("node-class", new PlcSTRING(nodeClass.name()));
+        }
+        String browseName = qualifiedNameValue(reference.getBrowseName());
+        if (browseName != null) {
+            options.put("browse-name", new PlcSTRING(browseName));
+        }
+
+        // Phase 1: only variable nodes are readable/writable. The actual 
access rights (and the
+        // data type) are refined once server-side type resolution is wired in.
+        return new DefaultPlcBrowseItem(tag, name, isVariable, isVariable,
+            Collections.<PlcSubscriptionType>emptySet(), false, 
Collections.emptyList(), children, options);
+    }
+
+    /** Formats the target node of a reference back into an OpcuaTag address 
(or null if unsupported). */
+    private static String addressOf(ExpandedNodeId expandedNodeId) {
+        if (expandedNodeId == null) {
+            return null;
+        }
+        NodeIdTypeDefinition nodeId = expandedNodeId.getNodeId();
+        String identifier = nodeId.getIdentifier();
+        if (nodeId instanceof NodeIdTwoByte) {
+            return "ns=0;i=" + identifier;
+        } else if (nodeId instanceof NodeIdFourByte fourByte) {
+            return "ns=" + fourByte.getNamespaceIndex() + ";i=" + identifier;
+        } else if (nodeId instanceof NodeIdNumeric numeric) {
+            return "ns=" + numeric.getNamespaceIndex() + ";i=" + identifier;
+        } else if (nodeId instanceof NodeIdString string) {
+            return "ns=" + string.getNamespaceIndex() + ";s=" + identifier;
+        }
+        // GUID and opaque (ByteString) node identifiers are not round-tripped 
in Phase 1:
+        // NodeIdGuid#getIdentifier() returns the raw byte-array toString(), 
which the tag
+        // parser can't turn back into a usable node id. Such nodes are 
skipped for now.
+        return null;
+    }
+
+    private static String localizedTextValue(LocalizedText localizedText) {
+        if (localizedText == null || !localizedText.getTextSpecified() || 
localizedText.getText() == null) {
+            return null;
+        }
+        return localizedText.getText().getStringValue();
+    }
+
+    private static String qualifiedNameValue(QualifiedName qualifiedName) {
+        if (qualifiedName == null || qualifiedName.getName() == null) {
+            return null;
+        }
+        return qualifiedName.getName().getStringValue();
+    }
+
     public static PlcValue variantToPlcValue(PlcTag tag, Variant variant) {
         PlcValue value = null;
         if (variant instanceof VariantBoolean) {
diff --git 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaPlcDriver.java
 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaPlcDriver.java
index fc1747a1fa..051fee7d34 100644
--- 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaPlcDriver.java
+++ 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaPlcDriver.java
@@ -71,6 +71,11 @@ public class OpcuaPlcDriver extends DriverBase {
         return true;
     }
 
+    @Override
+    protected boolean canBrowse() {
+        return true;
+    }
+
     @Override
     protected ConnectionBase<?> getConnection(Configuration configuration,
                                               TransportInstance<?> 
transportInstance,
diff --git 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaPlcTagHandler.java
 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaPlcTagHandler.java
index 0dfb984644..0e85d0ec95 100644
--- 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaPlcTagHandler.java
+++ 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaPlcTagHandler.java
@@ -34,7 +34,8 @@ public class OpcuaPlcTagHandler implements PlcTagHandler {
 
     @Override
     public PlcQuery parseQuery(String query) {
-        throw new UnsupportedOperationException("This driver doesn't support 
browsing");
+        // The query is the address of the node to start browsing from (empty 
=> Objects folder).
+        return new OpcuaQuery(query);
     }
 
 }
diff --git 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaPlcTagHandler.java
 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaQuery.java
similarity index 63%
copy from 
plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaPlcTagHandler.java
copy to 
plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaQuery.java
index 0dfb984644..5996ca225a 100644
--- 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaPlcTagHandler.java
+++ 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaQuery.java
@@ -18,23 +18,29 @@
  */
 package org.apache.plc4x.java.opcua.tag;
 
-import org.apache.plc4x.java.api.exceptions.PlcInvalidTagException;
 import org.apache.plc4x.java.api.model.PlcQuery;
-import org.apache.plc4x.java.spi.drivers.tags.PlcTagHandler;
 
-public class OpcuaPlcTagHandler implements PlcTagHandler {
+/**
+ * A browse query for the OPC UA driver. The query string is the address of 
the node to
+ * start browsing from (e.g. {@code ns=2;s=HelloWorld}); an empty query starts 
at the
+ * standard {@code Objects} folder.
+ */
+public class OpcuaQuery implements PlcQuery {
+
+    private final String queryString;
+
+    public OpcuaQuery(String queryString) {
+        this.queryString = queryString;
+    }
 
     @Override
-    public OpcuaTag parseTag(String tagAddress) {
-        if (OpcuaTag.matches(tagAddress)) {
-            return OpcuaTag.of(tagAddress);
-        }
-        throw new PlcInvalidTagException(tagAddress);
+    public String getQueryString() {
+        return queryString;
     }
 
     @Override
-    public PlcQuery parseQuery(String query) {
-        throw new UnsupportedOperationException("This driver doesn't support 
browsing");
+    public String toString() {
+        return "OpcuaQuery{" + queryString + '}';
     }
 
 }
diff --git 
a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaPlcDriverTest.java
 
b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaPlcDriverTest.java
index 492451ba09..3ce07f31fa 100644
--- 
a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaPlcDriverTest.java
+++ 
b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaPlcDriverTest.java
@@ -212,6 +212,70 @@ public class OpcuaPlcDriverTest {
         connectionStringValidSet = List.of(tcpConnectionAddress);
     }
 
+    @Test
+    void browseWildcardDiscoversWholeAddressSpace() throws Exception {
+        try (PlcConnection connection = new 
DefaultPlcDriverManager().getConnection(tcpConnectionAddress)) {
+            // "**" is the "browse everything" wildcard: it starts at the 
Objects folder and
+            // recurses the whole sub-tree.
+            org.apache.plc4x.java.api.messages.PlcBrowseResponse response = 
connection.browseRequestBuilder()
+                .addQuery("all", "**")
+                .build().execute().get(60, TimeUnit.SECONDS);
+            
assertThat(response.getResponseCode("all")).isEqualTo(PlcResponseCode.OK);
+
+            java.util.List<org.apache.plc4x.java.api.messages.PlcBrowseItem> 
top = response.getValues("all");
+            // The example server exposes the 'HelloWorld' folder under 
Objects.
+            
assertThat(top.stream().map(org.apache.plc4x.java.api.messages.PlcBrowseItem::getName)).contains("HelloWorld");
+
+            java.util.List<org.apache.plc4x.java.api.messages.PlcBrowseItem> 
all = new ArrayList<>();
+            collectBrowseItems(top, all);
+            assertThat(all.size()).isGreaterThan(50);
+        }
+    }
+
+    @Test
+    void browseDiscoversAddressSpace() throws Exception {
+        try (PlcConnection connection = new 
DefaultPlcDriverManager().getConnection(tcpConnectionAddress)) {
+            assertThat(connection.getMetadata().isBrowseSupported()).isTrue();
+
+            org.apache.plc4x.java.api.messages.PlcBrowseResponse response = 
connection.browseRequestBuilder()
+                .addQuery("hw", "ns=2;s=HelloWorld")
+                .build().execute().get(30, TimeUnit.SECONDS);
+            
assertThat(response.getResponseCode("hw")).isEqualTo(PlcResponseCode.OK);
+
+            java.util.List<org.apache.plc4x.java.api.messages.PlcBrowseItem> 
top = response.getValues("hw");
+            assertThat(top).isNotEmpty();
+
+            // 'ScalarTypes' is an Object (container) node: discovered, has 
children, not readable.
+            org.apache.plc4x.java.api.messages.PlcBrowseItem scalarTypes = 
top.stream()
+                .filter(i -> 
"ScalarTypes".equals(i.getName())).findFirst().orElse(null);
+            assertThat(scalarTypes).as("ScalarTypes folder").isNotNull();
+            assertThat(scalarTypes.isReadable()).isFalse();
+            assertThat(scalarTypes.getChildren()).isNotEmpty();
+
+            // A concrete variable underneath it is readable and writable, 
with a usable address.
+            org.apache.plc4x.java.api.messages.PlcBrowseItem boolVar = 
scalarTypes.getChildren().values().stream()
+                .filter(i -> 
"Boolean".equals(i.getName())).findFirst().orElse(null);
+            assertThat(boolVar).as("ScalarTypes/Boolean variable").isNotNull();
+            assertThat(boolVar.isReadable()).isTrue();
+            assertThat(boolVar.isWritable()).isTrue();
+            
assertThat(boolVar.getTag().getAddressString()).contains("ns=2;s=HelloWorld/ScalarTypes/Boolean");
+
+            // Full-subtree recursion reached deeply-nested property nodes 
(AnalogValue -> EURange).
+            java.util.List<org.apache.plc4x.java.api.messages.PlcBrowseItem> 
all = new ArrayList<>();
+            collectBrowseItems(top, all);
+            assertThat(all).anyMatch(i -> "EURange".equals(i.getName()));
+            assertThat(all.size()).isGreaterThan(40);
+        }
+    }
+
+    private static void 
collectBrowseItems(java.util.List<org.apache.plc4x.java.api.messages.PlcBrowseItem>
 items,
+                                           
java.util.List<org.apache.plc4x.java.api.messages.PlcBrowseItem> out) {
+        for (org.apache.plc4x.java.api.messages.PlcBrowseItem item : items) {
+            out.add(item);
+            collectBrowseItems(new ArrayList<>(item.getChildren().values()), 
out);
+        }
+    }
+
     @Nested
     class SmokeTest {
         @Test
diff --git 
a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/manual/ManualOpcUaS71500NewFWBrowse.java
 
b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/manual/ManualOpcUaS71500NewFWBrowse.java
new file mode 100644
index 0000000000..e09b7ec533
--- /dev/null
+++ 
b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/manual/ManualOpcUaS71500NewFWBrowse.java
@@ -0,0 +1,84 @@
+/*
+ * 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.plc4x.java.opcua.manual;
+
+import org.apache.plc4x.java.api.PlcConnection;
+import org.apache.plc4x.java.api.PlcDriverManager;
+import org.apache.plc4x.java.api.messages.PlcBrowseItem;
+import org.apache.plc4x.java.api.messages.PlcBrowseResponse;
+import org.apache.plc4x.java.api.model.PlcQuery;
+import org.apache.plc4x.java.api.types.PlcResponseCode;
+
+import java.util.stream.Collectors;
+
+public class ManualOpcUaS71500NewFWBrowse {
+
+    public static void main(String[] args) throws Exception {
+
+        long startTime = System.currentTimeMillis();
+        try (PlcConnection connection = 
PlcDriverManager.getDefault().getConnectionManager().getConnection("opcua://192.168.24.66:4840")){
+            PlcBrowseResponse plcBrowseResponse = 
connection.browseRequestBuilder()
+                .addQuery("all", "**")
+                .build().executeWithInterceptor((queryName, query, item) -> {
+                    outputItem(queryName, query, item, 0);
+                    return true;
+                }).get();
+
+            long endTime = System.currentTimeMillis();
+            int numNodes = 0;
+            for (String queryName : plcBrowseResponse.getQueryNames()) {
+                if (plcBrowseResponse.getResponseCode(queryName) != 
PlcResponseCode.OK) {
+                    continue;
+                }
+
+                for (PlcBrowseItem value : 
plcBrowseResponse.getValues(queryName)) {
+                    numNodes += countNodes(value);
+                }
+            }
+            System.out.printf("Took %dms returned %d nodes%n", endTime - 
startTime, numNodes);
+        }
+    }
+
+    protected static void outputItem(String queryName, PlcQuery query, 
PlcBrowseItem item, int level) {
+        System.out.printf("%s- %s: name: %s address: %s - type: %s%s%s%n",
+            "  ".repeat(level),
+            queryName,
+            item.getName(),
+            item.getTag().getAddressString(),
+            item.getTag().getPlcValueType(),
+            (item.getArrayInformation() != null) && 
!item.getArrayInformation().isEmpty() ? " " + 
item.getArrayInformation().stream().map(arrayInfo -> "[" + 
arrayInfo.getLowerBound() + ".." + arrayInfo.getUpperBound() + 
"]").collect(Collectors.joining()) : "",
+            (item.getOptions() != null) && !item.getOptions().isEmpty() ? " {" 
+ item.getOptions().entrySet().stream().map(stringPlcValueEntry -> 
stringPlcValueEntry.getKey() + ": \"" + 
stringPlcValueEntry.getValue().toString() + "\"").collect(Collectors.joining(", 
")) + "}" : "");
+        if ((item.getChildren() != null) && !item.getChildren().isEmpty()) {
+            item.getChildren().forEach((s, plcBrowseItem) -> 
outputItem(queryName, query, plcBrowseItem, level + 1));
+        }
+    }
+
+    protected static int countNodes(PlcBrowseItem browseItem) {
+        if(browseItem.getChildren().isEmpty()) {
+            return 1;
+        }
+
+        int nodes = 1;
+        for (PlcBrowseItem childBrowseItem : 
browseItem.getChildren().values()) {
+            nodes += countNodes(childBrowseItem);
+        }
+        return nodes;
+    }
+
+}

Reply via email to