You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2021/12/25 15:09:16 UTC
[camel] branch main updated: CAMEL-17319: Camel Milo: Browsing functionality (#6526)
This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new acc3024 CAMEL-17319: Camel Milo: Browsing functionality (#6526)
acc3024 is described below
commit acc3024827c69a33297b4a385e9ff4cf0348b6b7
Author: Andreas Klug <an...@de.bosch.com>
AuthorDate: Sat Dec 25 16:08:47 2021 +0100
CAMEL-17319: Camel Milo: Browsing functionality (#6526)
* camel-milo: Adding Milo browsing functionality
* CAMEL-17319: Add support for milo browsing functionality
* CAMEL-17319: Add support for milo browsing - changes based on review comments
* CAMEL-17319: Add support for milo browsing - changes based on review comments (affecting existing client component)
Co-authored-by: Klug Andreas (CI/XDM1) <kg...@bosch.com>
---
components/camel-milo/pom.xml | 16 -
.../org/apache/camel/component/milo-browse | 2 +
.../src/main/docs/milo-browse-component.adoc | 93 +++++
.../org/apache/camel/component/milo/NodeIds.java | 3 +
.../component/milo/browse/MiloBrowseComponent.java | 100 ++++++
.../component/milo/browse/MiloBrowseEndpoint.java | 247 +++++++++++++
.../component/milo/browse/MiloBrowseProducer.java | 149 ++++++++
.../milo/client/MiloClientConnection.java | 11 +
.../component/milo/client/MiloClientProducer.java | 4 +-
.../milo/client/internal/SubscriptionManager.java | 223 +++++++++++-
.../component/milo/AbstractMiloServerTest.java | 20 ++
.../component/milo/browse/BrowseServerTest.java | 381 +++++++++++++++++++++
.../component/milo/call/MockCamelNamespace.java | 1 -
.../src/test/resources/log4j2.properties | 6 +
14 files changed, 1228 insertions(+), 28 deletions(-)
diff --git a/components/camel-milo/pom.xml b/components/camel-milo/pom.xml
index 2a7adb3..0f69b4c 100644
--- a/components/camel-milo/pom.xml
+++ b/components/camel-milo/pom.xml
@@ -80,20 +80,4 @@
</dependency>
</dependencies>
- <build>
- <plugins>
- <plugin>
- <artifactId>maven-surefire-plugin</artifactId>
- <configuration>
- <childDelegation>false</childDelegation>
- <useFile>true</useFile>
- <forkCount>1</forkCount>
- <!-- required due to issue eclipse/milo#23 -->
- <reuseForks>false</reuseForks>
- <forkedProcessTimeoutInSeconds>300</forkedProcessTimeoutInSeconds>
- </configuration>
- </plugin>
- </plugins>
- </build>
-
</project>
diff --git a/components/camel-milo/src/generated/resources/META-INF/services/org/apache/camel/component/milo-browse b/components/camel-milo/src/generated/resources/META-INF/services/org/apache/camel/component/milo-browse
new file mode 100644
index 0000000..c8d6d89
--- /dev/null
+++ b/components/camel-milo/src/generated/resources/META-INF/services/org/apache/camel/component/milo-browse
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.component.milo.browse.MiloBrowseComponent
diff --git a/components/camel-milo/src/main/docs/milo-browse-component.adoc b/components/camel-milo/src/main/docs/milo-browse-component.adoc
new file mode 100644
index 0000000..9bf4ddc
--- /dev/null
+++ b/components/camel-milo/src/main/docs/milo-browse-component.adoc
@@ -0,0 +1,93 @@
+= OPC UA Browser Component
+:doctitle: OPC UA Browser
+:shortname: milo-browse
+:artifactid: camel-milo
+:description: Connect to OPC UA servers using the binary protocol for browsing the node tree.
+:since: 3.15
+:supportlevel: Preview
+:component-header: Only producer is supported
+include::{cq-version}@camel-quarkus:ROOT:partial$reference/components/milo-browse.adoc[opts=optional]
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The Milo Client component provides access to OPC UA servers using the
+http://eclipse.org/milo[Eclipse Milo™] implementation.
+
+*Java 11+*: This component requires Java 11+ at runtime.
+
+Maven users will need to add the following dependency to their `pom.xml`
+for this component:
+
+[source,xml]
+------------------------------------------------------------
+<dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-milo</artifactId>
+ <version>x.x.x</version>
+ <!-- use the same version as your Camel core version -->
+</dependency>
+------------------------------------------------------------
+
+
+== URI format
+
+The URI syntax of the endpoint is:
+
+------------------------
+milo-browse:opc.tcp://[user:password@]host:port/path/to/service?node=RAW(nsu=urn:foo:bar;s=item-1)
+------------------------
+
+Please refer to the milo-client component for further details about the construction of the URI.
+
+// component-configure options: START
+
+// component-configure options: END
+
+// component options: START
+include::partial$component-configure-options.adoc[]
+include::partial$component-endpoint-options.adoc[]
+// component options: END
+
+// endpoint options: START
+
+// endpoint options: END
+
+
+=== Client
+
+The browse component shares the same base options like the Camel Milo Client component, e. g. concerning topics like discovery,
+security policies, the construction of node ids, etc.
+
+Please refer to the documentation of the Camel Milo Client component for further details.
+
+=== Browsing
+
+The main use of this component is to be able to determine the nodes values to be retrieved or to be written by first browsing
+the node tree of the OPC-UA server, e. g. to avoid hard-coding a significant number of node ids within the configuration of
+Camel routes. The component is designed to work in conjunction with the Camel Milo Client component as illustrated in the
+following example:
+
+[source,java]
+----
+from("direct:start")
+
+ // Browse sub tree
+ .setHeader("CamelMiloNodeIds", constant(Arrays.asList("ns=1;s=folder-id")))
+ .enrich("milo-browse:opc.tcp://localhost:4334", (oldExchange, newExchange) -> newExchange)
+
+ // Filter specific ids
+ .filter(...)
+
+ // Retrieve the values for the nodes of interest
+ .enrich("milo-client:opc.tcp://localhost:4334", (oldExchange, newExchange) -> newExchange)
+
+----
+
+=== Recursion
+
+Dependent to the OPC-UA server there it might be required to browse a hierarchy of nodes.
+Be aware that this is potentially a very expensive operation.
+
+include::{page-component-version}@camel-spring-boot::page$milo-starter.adoc[]
diff --git a/components/camel-milo/src/main/java/org/apache/camel/component/milo/NodeIds.java b/components/camel-milo/src/main/java/org/apache/camel/component/milo/NodeIds.java
index b8ae81a..f964cbd 100644
--- a/components/camel-milo/src/main/java/org/apache/camel/component/milo/NodeIds.java
+++ b/components/camel-milo/src/main/java/org/apache/camel/component/milo/NodeIds.java
@@ -33,6 +33,9 @@ import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.
* Helper class to work with node IDs
*/
public final class NodeIds {
+
+ public static final String HEADER_NODE_IDS = "CamelMiloNodeIds";
+
private NodeIds() {
}
diff --git a/components/camel-milo/src/main/java/org/apache/camel/component/milo/browse/MiloBrowseComponent.java b/components/camel-milo/src/main/java/org/apache/camel/component/milo/browse/MiloBrowseComponent.java
new file mode 100644
index 0000000..2e32eb3
--- /dev/null
+++ b/components/camel-milo/src/main/java/org/apache/camel/component/milo/browse/MiloBrowseComponent.java
@@ -0,0 +1,100 @@
+/*
+ * 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.camel.component.milo.browse;
+
+import java.util.Map;
+
+import org.apache.camel.Endpoint;
+import org.apache.camel.component.milo.client.MiloClientCachingConnectionManager;
+import org.apache.camel.component.milo.client.MiloClientConfiguration;
+import org.apache.camel.component.milo.client.MiloClientConnectionManager;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.annotations.Component;
+import org.apache.camel.support.DefaultComponent;
+
+@Component("milo-browse")
+public class MiloBrowseComponent extends DefaultComponent {
+
+ @Metadata
+ private MiloClientConfiguration configuration = new MiloClientConfiguration();
+
+ @Metadata(autowired = true, label = "client", description = "Instance for managing client connections")
+ private MiloClientConnectionManager miloClientConnectionManager = new MiloClientCachingConnectionManager();
+
+ @Override
+ protected Endpoint createEndpoint(final String uri, final String remaining, final Map<String, Object> parameters)
+ throws Exception {
+
+ final MiloClientConfiguration browseConfiguration = new MiloClientConfiguration(getConfiguration());
+ browseConfiguration.setEndpointUri(remaining);
+
+ final MiloBrowseEndpoint endpoint
+ = new MiloBrowseEndpoint(uri, this, browseConfiguration.getEndpointUri(), getMiloClientConnectionManager());
+ endpoint.setConfiguration(browseConfiguration);
+ setProperties(endpoint, parameters);
+
+ return endpoint;
+
+ }
+
+ public MiloClientConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ /**
+ * All default options for client configurations
+ */
+ public void setConfiguration(final MiloClientConfiguration configuration) {
+ this.configuration = configuration;
+ }
+
+ /**
+ * Default application name
+ */
+ public void setApplicationName(final String applicationName) {
+ this.configuration.setApplicationName(applicationName);
+ }
+
+ /**
+ * Default application URI
+ */
+ public void setApplicationUri(final String applicationUri) {
+ this.configuration.setApplicationUri(applicationUri);
+ }
+
+ /**
+ * Default product URI
+ */
+ public void setProductUri(final String productUri) {
+ this.configuration.setProductUri(productUri);
+ }
+
+ /**
+ * Default reconnect timeout
+ */
+ public void setReconnectTimeout(final Long reconnectTimeout) {
+ this.configuration.setRequestTimeout(reconnectTimeout);
+ }
+
+ public MiloClientConnectionManager getMiloClientConnectionManager() {
+ return miloClientConnectionManager;
+ }
+
+ public void setMiloClientConnectionManager(MiloClientConnectionManager miloClientConnectionManager) {
+ this.miloClientConnectionManager = miloClientConnectionManager;
+ }
+}
diff --git a/components/camel-milo/src/main/java/org/apache/camel/component/milo/browse/MiloBrowseEndpoint.java b/components/camel-milo/src/main/java/org/apache/camel/component/milo/browse/MiloBrowseEndpoint.java
new file mode 100644
index 0000000..6a3c95a
--- /dev/null
+++ b/components/camel-milo/src/main/java/org/apache/camel/component/milo/browse/MiloBrowseEndpoint.java
@@ -0,0 +1,247 @@
+/*
+ * 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.camel.component.milo.browse;
+
+import java.util.Objects;
+
+import org.apache.camel.Category;
+import org.apache.camel.Consumer;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.component.milo.client.MiloClientConfiguration;
+import org.apache.camel.component.milo.client.MiloClientConnection;
+import org.apache.camel.component.milo.client.MiloClientConnectionManager;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.UriEndpoint;
+import org.apache.camel.spi.UriParam;
+import org.apache.camel.spi.UriPath;
+import org.apache.camel.support.DefaultEndpoint;
+import org.eclipse.milo.opcua.stack.core.Identifiers;
+import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
+import org.eclipse.milo.opcua.stack.core.types.enumerated.BrowseDirection;
+import org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Connect to OPC UA servers using the binary protocol for browsing the node tree.
+ */
+@UriEndpoint(firstVersion = "3.15.0", scheme = "milo-browse", syntax = "milo-browse:endpointUri", title = "OPC UA Browser",
+ category = { Category.IOT }, producerOnly = true)
+public class MiloBrowseEndpoint extends DefaultEndpoint {
+
+ private static final Logger LOG = LoggerFactory.getLogger(MiloBrowseEndpoint.class);
+
+ private final MiloClientConnectionManager connectionManager;
+
+ /**
+ * The OPC UA server endpoint
+ */
+ @UriPath
+ @Metadata(required = true)
+ private final String endpointUri;
+
+ /**
+ * The node definition (see Node ID)
+ */
+ @UriParam(defaultValue = "ns=0;id=84", defaultValueNote = "Root folder as per OPC-UA spec")
+ private String node = Identifiers.RootFolder.toParseableString();
+
+ /**
+ * The direction to browse (forward, inverse, ...)
+ */
+ @UriParam(defaultValue = "Forward",
+ enums = "Forward,Inverse,Both",
+ defaultValueNote = "The direction to browse; see org.eclipse.milo.opcua.stack.core.types.enumerated.BrowseDirection")
+ private BrowseDirection direction = BrowseDirection.Forward;
+
+ /**
+ * Whether to include sub-types for browsing; only applicable for non-recursive browsing
+ */
+ @UriParam(defaultValue = "true")
+ private boolean includeSubTypes = true;
+
+ /**
+ * The mask indicating the node classes of interest in browsing
+ */
+ @UriParam(defaultValue = "Variable,Object,DataType",
+ defaultValueNote = "Comma-separated node class list; see org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass")
+ private String nodeClasses = NodeClass.Variable + "," + NodeClass.Object + "," + NodeClass.DataType;
+
+ private int nodeClassMask = NodeClass.Variable.getValue() | NodeClass.Object.getValue() | NodeClass.DataType.getValue();
+
+ /**
+ * Whether to browse recursively into sub-types, ignores includeSubTypes setting as it's implied to be set to true
+ */
+ @UriParam(defaultValue = "false",
+ defaultValueNote = "Whether to recursively browse sub-types: true|false")
+ private boolean recursive;
+
+ /**
+ * When browsing recursively into sub-types, what's the maximum search depth for diving into the tree
+ */
+ @UriParam(defaultValue = "3", defaultValueNote = "Maximum depth for browsing recursively (only if recursive = true)")
+ private int depth = 3;
+
+ /**
+ * Filter out node ids to limit browsing
+ */
+ @UriParam(defaultValue = "None", defaultValueNote = "Regular filter expression matching node ids")
+ private String filter;
+
+ /**
+ * The maximum number node ids requested per server call
+ */
+ @UriParam(defaultValue = "10",
+ defaultValueNote = "Maximum number of node ids requested per browse call (applies to browsing sub-types only; only if recursive = true)")
+ private int maxNodeIdsPerRequest = 10;
+
+ /**
+ * The client configuration
+ */
+ @UriParam
+ private MiloClientConfiguration configuration;
+
+ public MiloBrowseEndpoint(final String uri, final MiloBrowseComponent component, final String endpointUri,
+ final MiloClientConnectionManager connectionManager) {
+ super(uri, component);
+
+ Objects.requireNonNull(component);
+ Objects.requireNonNull(endpointUri);
+ Objects.requireNonNull(connectionManager);
+
+ this.endpointUri = endpointUri;
+ this.connectionManager = connectionManager;
+ }
+
+ public void setConfiguration(MiloClientConfiguration configuration) {
+ this.configuration = configuration;
+ }
+
+ public MiloClientConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ @Override
+ public Producer createProducer() throws Exception {
+ return new MiloBrowseProducer(this);
+ }
+
+ @Override
+ public Consumer createConsumer(final Processor processor) throws Exception {
+ throw new UnsupportedOperationException(MiloBrowseEndpoint.class.getName() + " doesn't support a consumer");
+ }
+
+ public MiloClientConnection createConnection() {
+ return this.connectionManager.createConnection(configuration, null);
+ }
+
+ public void releaseConnection(MiloClientConnection connection) {
+ this.connectionManager.releaseConnection(connection);
+ }
+
+ public void setNode(final String node) {
+ this.node = node;
+ }
+
+ public String getNode() {
+ return node;
+ }
+
+ NodeId getNodeId() {
+ return getNodeId(this.node);
+ }
+
+ NodeId getNodeId(String nodeId) {
+ if (nodeId != null) {
+ return NodeId.parse(nodeId);
+ } else {
+ return null;
+ }
+ }
+
+ public BrowseDirection getDirection() {
+ return direction;
+ }
+
+ public boolean isIncludeSubTypes() {
+ return includeSubTypes;
+ }
+
+ public void setIncludeSubTypes(boolean includeSubTypes) {
+ this.includeSubTypes = includeSubTypes;
+ }
+
+ public String getNodeClasses() {
+ return nodeClasses;
+ }
+
+ public void setNodeClasses(String nodeClasses) {
+ this.nodeClasses = nodeClasses;
+ final String[] nodeClassArray = nodeClasses.split(",");
+ int mask = 0;
+ try {
+ for (String nodeClass : nodeClassArray) {
+ mask |= NodeClass.valueOf(nodeClass).getValue();
+ }
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Invalid node class specified: " + nodeClasses, e);
+ }
+ LOG.debug("Node class list conversion {} -> {}", nodeClasses, mask);
+ nodeClassMask = mask;
+ }
+
+ public int getNodeClassMask() {
+ return nodeClassMask;
+ }
+
+ public void setDirection(BrowseDirection direction) {
+ this.direction = direction;
+ }
+
+ public boolean isRecursive() {
+ return recursive;
+ }
+
+ public void setRecursive(boolean recursive) {
+ this.recursive = recursive;
+ }
+
+ public int getDepth() {
+ return depth;
+ }
+
+ public void setDepth(int depth) {
+ this.depth = depth;
+ }
+
+ public String getFilter() {
+ return filter;
+ }
+
+ public void setFilter(String filter) {
+ this.filter = filter;
+ }
+
+ public int getMaxNodeIdsPerRequest() {
+ return maxNodeIdsPerRequest;
+ }
+
+ public void setMaxNodeIdsPerRequest(int maxNodeIdsPerRequest) {
+ this.maxNodeIdsPerRequest = maxNodeIdsPerRequest;
+ }
+}
diff --git a/components/camel-milo/src/main/java/org/apache/camel/component/milo/browse/MiloBrowseProducer.java b/components/camel-milo/src/main/java/org/apache/camel/component/milo/browse/MiloBrowseProducer.java
new file mode 100644
index 0000000..bfbbf4d
--- /dev/null
+++ b/components/camel-milo/src/main/java/org/apache/camel/component/milo/browse/MiloBrowseProducer.java
@@ -0,0 +1,149 @@
+/*
+ * 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.camel.component.milo.browse;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.camel.AsyncCallback;
+import org.apache.camel.Exchange;
+import org.apache.camel.Message;
+import org.apache.camel.component.milo.client.MiloClientConnection;
+import org.apache.camel.support.DefaultAsyncProducer;
+import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
+import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
+import org.eclipse.milo.opcua.stack.core.types.structured.BrowseResult;
+import org.eclipse.milo.opcua.stack.core.types.structured.ReferenceDescription;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.camel.component.milo.NodeIds.HEADER_NODE_IDS;
+
+public class MiloBrowseProducer extends DefaultAsyncProducer {
+
+ private static final Logger LOG = LoggerFactory.getLogger(MiloBrowseProducer.class);
+
+ private MiloClientConnection connection;
+
+ public MiloBrowseProducer(final MiloBrowseEndpoint endpoint) {
+
+ super(endpoint);
+ }
+
+ @Override
+ public MiloBrowseEndpoint getEndpoint() {
+ return (MiloBrowseEndpoint) super.getEndpoint();
+ }
+
+ @Override
+ protected void doStart() throws Exception {
+ super.doStart();
+ this.connection = getEndpoint().createConnection();
+ }
+
+ @Override
+ protected void doStop() throws Exception {
+ if (null != this.connection) {
+ getEndpoint().releaseConnection(connection);
+ }
+ super.doStop();
+ }
+
+ private ExpandedNodeId tryParse(String nodeString) {
+ final Optional<NodeId> nodeId = NodeId.parseSafe(nodeString);
+ return nodeId.map(NodeId::expanded).orElseGet(() -> ExpandedNodeId.parse(nodeString));
+ }
+
+ @Override
+ public boolean process(Exchange exchange, AsyncCallback async) {
+
+ final Message message = exchange.getMessage();
+ final List<ExpandedNodeId> expandedNodeIds = new ArrayList<>();
+
+ if (message.getHeaders().containsKey(HEADER_NODE_IDS)) {
+
+ final List<?> nodes
+ = message.getHeader(HEADER_NODE_IDS, Collections.singletonList(getEndpoint().getNode()), List.class);
+ message.removeHeader(HEADER_NODE_IDS);
+ if (null == nodes) {
+
+ LOG.warn("Browse nodes: No node ids specified");
+ async.done(true);
+ return true;
+ }
+
+ for (final Object node : nodes) {
+ expandedNodeIds.add(tryParse(node.toString()));
+ }
+ } else {
+
+ expandedNodeIds.add(tryParse(this.getEndpoint().getNode()));
+ }
+
+ final MiloBrowseEndpoint endpoint = this.getEndpoint();
+ final int depth = endpoint.isRecursive() ? endpoint.getDepth() : -1;
+ final boolean subTypes = endpoint.isIncludeSubTypes() || endpoint.isRecursive();
+
+ final CompletableFuture<?> future = this.connection
+ .browse(expandedNodeIds, endpoint.getDirection(), endpoint.getNodeClassMask(), depth, endpoint.getFilter(),
+ subTypes, endpoint.getMaxNodeIdsPerRequest())
+
+ .thenApply(browseResults -> {
+
+ final List<String> expandedNodes = browseResults.values().stream()
+ .map(BrowseResult::getReferences)
+ .flatMap(Stream::of)
+ .map(ReferenceDescription::getNodeId)
+ .map(ExpandedNodeId::toParseableString)
+ .collect(Collectors.toList());
+
+ // For convenience, to be used with the milo-client producer
+ exchange.getMessage().setHeader(HEADER_NODE_IDS, expandedNodes);
+
+ exchange.getMessage().setBody(browseResults);
+
+ return browseResults;
+ })
+
+ .whenComplete((actual, error) -> {
+
+ final String expandedNodeIdsString = expandedNodeIds.stream()
+ .map(ExpandedNodeId::toParseableString)
+ .collect(Collectors.joining(", "));
+
+ if (actual != null) {
+
+ LOG.debug("Browse node(s) {} -> {} result(s)", expandedNodeIdsString, actual.size());
+
+ } else {
+
+ LOG.error("Browse node(s) {} -> failed: {}", expandedNodeIdsString, error.getMessage());
+ exchange.setException(error);
+ }
+
+ async.done(false);
+ });
+
+ return false;
+ }
+
+}
diff --git a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConnection.java b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConnection.java
index 9837a78..9bd1b93 100644
--- a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConnection.java
+++ b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConnection.java
@@ -17,6 +17,7 @@
package org.apache.camel.component.milo.client;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
@@ -28,6 +29,8 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode;
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
+import org.eclipse.milo.opcua.stack.core.types.enumerated.BrowseDirection;
+import org.eclipse.milo.opcua.stack.core.types.structured.BrowseResult;
import org.eclipse.milo.opcua.stack.core.types.structured.CallMethodResult;
import static java.util.Objects.requireNonNull;
@@ -156,4 +159,12 @@ public class MiloClientConnection implements AutoCloseable {
return new DataValue(new Variant(value), StatusCode.GOOD, null, null);
}
+ public CompletableFuture<Map<ExpandedNodeId, BrowseResult>> browse(
+ final List<ExpandedNodeId> expandedNodeIds, final BrowseDirection direction, final int nodeClasses,
+ final int maxDepth, String filter, boolean includeSubTypes, int maxNodesPerRequest) {
+ checkInit();
+
+ return this.manager.browse(expandedNodeIds, direction, nodeClasses, maxDepth, filter, includeSubTypes,
+ maxNodesPerRequest);
+ }
}
diff --git a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientProducer.java b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientProducer.java
index 9212a0ea..2b2e82c 100644
--- a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientProducer.java
+++ b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientProducer.java
@@ -27,11 +27,10 @@ import org.apache.camel.support.DefaultAsyncProducer;
import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
import static java.lang.Boolean.TRUE;
+import static org.apache.camel.component.milo.NodeIds.HEADER_NODE_IDS;
public class MiloClientProducer extends DefaultAsyncProducer {
- private static final String HEADER_NODE_IDS = "CamelMiloNodeIds";
-
private MiloClientConnection connection;
private final ExpandedNodeId nodeId;
@@ -94,6 +93,7 @@ public class MiloClientProducer extends DefaultAsyncProducer {
future.whenComplete((v, ex) -> async.done(false));
return false;
} else {
+ async.done(true);
return true;
}
}
diff --git a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java
index dceca11..0627485 100644
--- a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java
+++ b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java
@@ -19,6 +19,7 @@ package org.apache.camel.component.milo.client.internal;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
@@ -34,10 +35,12 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Predicate;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.component.milo.client.MiloClientConfiguration;
import org.apache.camel.component.milo.client.MonitorFilterConfiguration;
@@ -55,6 +58,7 @@ import org.eclipse.milo.opcua.stack.core.AttributeId;
import org.eclipse.milo.opcua.stack.core.Identifiers;
import org.eclipse.milo.opcua.stack.core.StatusCodes;
import org.eclipse.milo.opcua.stack.core.UaException;
+import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString;
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime;
import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
@@ -66,8 +70,12 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned;
+import org.eclipse.milo.opcua.stack.core.types.enumerated.BrowseDirection;
+import org.eclipse.milo.opcua.stack.core.types.enumerated.BrowseResultMask;
import org.eclipse.milo.opcua.stack.core.types.enumerated.MonitoringMode;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
+import org.eclipse.milo.opcua.stack.core.types.structured.BrowseDescription;
+import org.eclipse.milo.opcua.stack.core.types.structured.BrowseResult;
import org.eclipse.milo.opcua.stack.core.types.structured.CallMethodRequest;
import org.eclipse.milo.opcua.stack.core.types.structured.CallMethodResult;
import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
@@ -75,11 +83,14 @@ import org.eclipse.milo.opcua.stack.core.types.structured.MonitoredItemCreateReq
import org.eclipse.milo.opcua.stack.core.types.structured.MonitoringFilter;
import org.eclipse.milo.opcua.stack.core.types.structured.MonitoringParameters;
import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId;
+import org.eclipse.milo.opcua.stack.core.types.structured.ReferenceDescription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import static java.lang.String.format;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static org.apache.camel.component.milo.NodeIds.toNodeId;
+import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;
public class SubscriptionManager {
@@ -93,7 +104,7 @@ public class SubscriptionManager {
LOG.info("Transfer failed {} : {}", subscription.getSubscriptionId(), statusCode);
// we simply tear it down and build it up again
- handleConnectionFailue(new RuntimeCamelException("Subscription failed to reconnect"));
+ handleConnectionFailure(new RuntimeCamelException("Subscription failed to reconnect"));
}
@Override
@@ -234,7 +245,7 @@ public class SubscriptionManager {
try {
putSubscriptions(subscriptions);
} catch (final Exception e) {
- handleConnectionFailue(e);
+ handleConnectionFailure(e);
}
}
@@ -368,6 +379,180 @@ public class SubscriptionManager {
return this.client.readValues(0, TimestampsToReturn.Server, nodeIds);
});
}
+
+ private BrowseResult filter(final BrowseResult browseResult, final Pattern pattern) {
+
+ final ReferenceDescription[] references = browseResult.getReferences();
+
+ if (null == references || null == pattern) {
+ return browseResult;
+ }
+
+ final List<ReferenceDescription> filteredReferences = new ArrayList<>();
+ for (final ReferenceDescription reference : references) {
+ final String id = reference.getNodeId().toParseableString();
+ if (!(pattern.matcher(id).matches())) {
+ LOG.trace("Node {} excluded by filter", id);
+ continue;
+ }
+ filteredReferences.add(reference);
+ }
+
+ return new BrowseResult(
+ browseResult.getStatusCode(), browseResult.getContinuationPoint(),
+ filteredReferences.toArray(new ReferenceDescription[0]));
+ }
+
+ private CompletableFuture<Map<ExpandedNodeId, BrowseResult>> flatten(
+ List<CompletableFuture<Map<ExpandedNodeId, BrowseResult>>> browseResults) {
+ return CompletableFuture.allOf(browseResults.toArray(new CompletableFuture[0]))
+ .thenApply(__ -> browseResults
+ .stream()
+ .map(CompletableFuture::join)
+ .map(Map::entrySet)
+ .flatMap(Set::stream)
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e2)));
+ }
+
+ // Browse at continuation point if any
+ public CompletableFuture<BrowseResult> browse(BrowseResult previousBrowseResult) {
+
+ final ByteString continuationPoint = previousBrowseResult.getContinuationPoint();
+
+ if (previousBrowseResult.getStatusCode().isGood() && continuationPoint.isNotNull()) {
+
+ return this.client.browseNext(false, continuationPoint)
+
+ .thenCompose(browseResult -> {
+
+ final ReferenceDescription[] previousReferences = previousBrowseResult.getReferences();
+ final ReferenceDescription[] references = browseResult.getReferences();
+
+ if (null == references) {
+
+ LOG.info("Browse continuation point -> no references");
+ return completedFuture(previousBrowseResult);
+ } else if (null == previousReferences) {
+
+ LOG.info("Browse continuation point -> previous references not obtained");
+ return completedFuture(browseResult);
+ }
+
+ final ReferenceDescription[] combined
+ = Arrays.copyOf(previousReferences, previousReferences.length + references.length);
+ System.arraycopy(references, 0, combined, previousReferences.length, references.length);
+
+ LOG.debug("Browse continuation point -> {}: {} reference(s); total: {} reference(s)",
+ browseResult.getStatusCode(), references.length, combined.length);
+
+ return browse(new BrowseResult(
+ browseResult.getStatusCode(), browseResult.getContinuationPoint(), combined));
+ });
+
+ } else {
+
+ return completedFuture(previousBrowseResult);
+ }
+ }
+
+ // Browse a single node, retrieve additional results, filter node ids and eventually browse deeper into the tree
+ public CompletableFuture<Map<ExpandedNodeId, BrowseResult>> browse(
+ BrowseDescription browseDescription, BrowseResult browseResult, int depth, int maxDepth, Pattern pattern,
+ int maxNodesPerRequest) {
+
+ return browse(browseResult)
+
+ .thenCompose(preliminary -> completedFuture(filter(preliminary, pattern)))
+
+ .thenCompose(filtered -> {
+
+ final ExpandedNodeId expandedNodeId = browseDescription.getNodeId().expanded();
+ final Map<ExpandedNodeId, BrowseResult> root = Collections.singletonMap(expandedNodeId, filtered);
+ final CompletableFuture<Map<ExpandedNodeId, BrowseResult>> finalFuture = completedFuture(root);
+ final ReferenceDescription[] references = filtered.getReferences();
+
+ if (depth >= maxDepth || null == references) {
+ return finalFuture;
+ }
+
+ final List<CompletableFuture<Map<ExpandedNodeId, BrowseResult>>> futures = new ArrayList<>();
+
+ // Save current node
+ futures.add(finalFuture);
+
+ final List<ExpandedNodeId> nodeIds = Stream.of(references)
+ .map(ReferenceDescription::getNodeId).collect(Collectors.toList());
+
+ final List<List<ExpandedNodeId>> lists = Lists.partition(nodeIds, maxNodesPerRequest);
+ for (final List<ExpandedNodeId> list : lists) {
+ futures.add(browse(list, browseDescription.getBrowseDirection(),
+ browseDescription.getNodeClassMask().intValue(), depth + 1, maxDepth, pattern,
+ browseDescription.getIncludeSubtypes(), maxNodesPerRequest));
+ }
+
+ return flatten(futures);
+ });
+ }
+
+ // Browse according to a list of browse descriptions
+ public CompletableFuture<Map<ExpandedNodeId, BrowseResult>> browse(
+ List<BrowseDescription> browseDescriptions,
+ int depth, int maxDepth, Pattern pattern, int maxNodesPerRequest) {
+
+ return this.client.browse(browseDescriptions)
+
+ .thenCompose(partials -> {
+
+ // Fail a bit more gracefully in case of missing results
+ if (partials.size() != browseDescriptions.size()) {
+
+ final CompletableFuture<Map<ExpandedNodeId, BrowseResult>> failedFuture = new CompletableFuture<>();
+ failedFuture.completeExceptionally(new IllegalArgumentException(
+ format("Invalid number of browse results: %s, expected %s",
+ partials.size(), browseDescriptions.size())));
+ return failedFuture;
+
+ /* @TODO: Replace with Java 9 functionality like follows
+ return CompletableFuture.failedFuture(new IllegalArgumentException(
+ format("Invalid number of browse results: %s, expected %s", partials.size(),
+ browseDescriptions.size()))); */
+ }
+
+ final List<CompletableFuture<Map<ExpandedNodeId, BrowseResult>>> futures = new ArrayList<>();
+
+ for (int i = 0; i < partials.size(); i++) {
+
+ futures.add(browse(browseDescriptions.get(i), partials.get(i), depth, maxDepth, pattern,
+ maxNodesPerRequest));
+ }
+
+ return flatten(futures);
+ });
+ }
+
+ // Wrapper for looking up nodes and instantiating initial browse descriptions according to the configuration provided
+ @SuppressWarnings("unchecked")
+ public CompletableFuture<Map<ExpandedNodeId, BrowseResult>> browse(
+ List<ExpandedNodeId> expandedNodeIds, BrowseDirection direction, int nodeClasses, int depth, int maxDepth,
+ Pattern pattern, boolean includeSubTypes, int maxNodesPerRequest) {
+
+ final CompletableFuture<NodeId>[] futures = expandedNodeIds.stream().map(this::lookupNamespace)
+ .toArray(CompletableFuture[]::new);
+
+ return CompletableFuture.allOf(futures)
+
+ .thenCompose(__ -> {
+
+ final List<NodeId> nodeIds = Stream.of(futures).map(CompletableFuture::join)
+ .collect(Collectors.toList());
+
+ return completedFuture(nodeIds.stream().map(nodeId -> new BrowseDescription(
+ nodeId, direction, Identifiers.References, includeSubTypes, uint(nodeClasses),
+ uint(BrowseResultMask.All.getValue()))).collect(Collectors.toList()));
+ })
+
+ .thenCompose(descriptions -> browse(descriptions, depth, maxDepth, pattern, maxNodesPerRequest));
+ }
}
private final MiloClientConfiguration configuration;
@@ -389,7 +574,7 @@ public class SubscriptionManager {
connect();
}
- private synchronized void handleConnectionFailue(final Throwable e) {
+ private synchronized void handleConnectionFailure(final Throwable e) {
if (this.connected != null) {
this.connected.dispose();
this.connected = null;
@@ -456,7 +641,7 @@ public class SubscriptionManager {
private Connected performConnect() throws Exception {
- // eval enpoint
+ // eval endpoint
String discoveryUri = getEndpointDiscoveryUri();
@@ -473,7 +658,7 @@ public class SubscriptionManager {
final EndpointDescription endpoint = DiscoveryClient.getEndpoints(discoveryUri).thenApply(endpoints -> {
if (LOG.isDebugEnabled()) {
- LOG.debug("Found enpoints:");
+ LOG.debug("Found endpoints:");
for (final EndpointDescription ep : endpoints) {
LOG.debug("\t{}", ep);
}
@@ -649,7 +834,7 @@ public class SubscriptionManager {
try {
worker.work(this.connected);
} catch (final Exception e) {
- handleConnectionFailue(e);
+ handleConnectionFailure(e);
}
}
}
@@ -697,7 +882,7 @@ public class SubscriptionManager {
// handle outside the lock, running using
// handleAsync
if (e != null) {
- handleConnectionFailue(e);
+ handleConnectionFailure(e);
}
return null;
}, this.executor);
@@ -714,7 +899,7 @@ public class SubscriptionManager {
// handle outside the lock, running using
// handleAsync
if (e != null) {
- handleConnectionFailue(e);
+ handleConnectionFailure(e);
}
return null;
}, this.executor);
@@ -731,11 +916,31 @@ public class SubscriptionManager {
// handle outside the lock, running using
// handleAsync
if (e != null) {
- handleConnectionFailue(e);
+ handleConnectionFailure(e);
}
return nodes;
}, this.executor);
}
}
+ public CompletableFuture<Map<ExpandedNodeId, BrowseResult>> browse(
+ List<ExpandedNodeId> expandedNodeIds, BrowseDirection direction, int nodeClasses, int maxDepth, String filter,
+ boolean includeSubTypes, int maxNodesPerRequest) {
+ synchronized (this) {
+ if (this.connected == null) {
+ return newNotConnectedResult();
+ }
+
+ return this.connected.browse(expandedNodeIds, direction, nodeClasses, 1, maxDepth,
+ null != filter ? Pattern.compile(filter) : null, includeSubTypes, maxNodesPerRequest)
+ .handleAsync((browseResult, e) -> {
+ // handle outside the lock, running using
+ // handleAsync
+ if (e != null) {
+ handleConnectionFailure(e);
+ }
+ return browseResult;
+ }, this.executor);
+ }
+ }
}
diff --git a/components/camel-milo/src/test/java/org/apache/camel/component/milo/AbstractMiloServerTest.java b/components/camel-milo/src/test/java/org/apache/camel/component/milo/AbstractMiloServerTest.java
index 7e54cfa..c4677ba 100644
--- a/components/camel-milo/src/test/java/org/apache/camel/component/milo/AbstractMiloServerTest.java
+++ b/components/camel-milo/src/test/java/org/apache/camel/component/milo/AbstractMiloServerTest.java
@@ -21,6 +21,8 @@ import java.security.GeneralSecurityException;
import java.util.function.Consumer;
import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.Predicate;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.component.milo.server.MiloServerComponent;
import org.apache.camel.component.mock.AssertionClause;
@@ -28,6 +30,9 @@ import org.apache.camel.test.AvailablePortFinder;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
+import org.opentest4j.AssertionFailedError;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -37,6 +42,8 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue;
public abstract class AbstractMiloServerTest extends CamelTestSupport {
+ private static final Logger LOG = LoggerFactory.getLogger(AbstractMiloServerTest.class);
+
private int serverPort;
@Override
@@ -153,4 +160,17 @@ public abstract class AbstractMiloServerTest extends CamelTestSupport {
return false;
}
+ protected Predicate assertPredicate(Consumer<Exchange> consumer) {
+
+ return exchange -> {
+ try {
+ consumer.accept(exchange);
+ return true;
+ } catch (AssertionFailedError error) {
+ LOG.error("Assertion error: " + error.getMessage(), error);
+ return false;
+ }
+ };
+ }
+
}
diff --git a/components/camel-milo/src/test/java/org/apache/camel/component/milo/browse/BrowseServerTest.java b/components/camel-milo/src/test/java/org/apache/camel/component/milo/browse/BrowseServerTest.java
new file mode 100644
index 0000000..f4ea89e
--- /dev/null
+++ b/components/camel-milo/src/test/java/org/apache/camel/component/milo/browse/BrowseServerTest.java
@@ -0,0 +1,381 @@
+/*
+ * 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.camel.component.milo.browse;
+
+import java.nio.CharBuffer;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.EndpointInject;
+import org.apache.camel.Exchange;
+import org.apache.camel.Message;
+import org.apache.camel.Produce;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.ExchangeBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.milo.AbstractMiloServerTest;
+import org.apache.camel.component.milo.NodeIds;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.eclipse.milo.opcua.stack.core.Identifiers;
+import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
+import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
+import org.eclipse.milo.opcua.stack.core.types.structured.BrowseResult;
+import org.eclipse.milo.opcua.stack.core.types.structured.ReferenceDescription;
+import org.junit.jupiter.api.Test;
+
+import static org.apache.camel.component.mock.MockEndpoint.assertIsSatisfied;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for browsing
+ */
+public class BrowseServerTest extends AbstractMiloServerTest {
+
+ private static final String DIRECT_START_1 = "direct:start1";
+ private static final String DIRECT_START_2 = "direct:start2";
+ private static final String DIRECT_START_3 = "direct:start3";
+ private static final String DIRECT_START_4 = "direct:start4";
+ private static final String DIRECT_START_5 = "direct:start5";
+ private static final String DIRECT_START_6 = "direct:start6";
+ private static final String DIRECT_START_7 = "direct:start7";
+ private static final String DIRECT_START_8 = "direct:start8";
+ private static final String DIRECT_START_9 = "direct:start9";
+
+ private static final String MOCK_TEST_1 = "mock:test1";
+ private static final String MOCK_TEST_2 = "mock:test2";
+ private static final String MOCK_TEST_3 = "mock:test3";
+ private static final String MOCK_TEST_4 = "mock:test4";
+ private static final String MOCK_TEST_5 = "mock:test5";
+ private static final String MOCK_TEST_6 = "mock:test6";
+ private static final String MOCK_TEST_7 = "mock:test7";
+ private static final String MOCK_TEST_8 = "mock:test8";
+ private static final String MOCK_TEST_9 = "mock:test9";
+
+ private static final String MILO_BROWSE_BASE
+ = "milo-browse:opc.tcp://foo:bar@127.0.0.1:@@port@@";
+
+ private static final String MILO_BROWSE_ROOT
+ = MILO_BROWSE_BASE + "?overrideHost=true&allowedSecurityPolicies=None";
+
+ private static final String MILO_BROWSE_WITHOUT_SUB_TYPES
+ = MILO_BROWSE_ROOT + "&includeSubTypes=false";
+
+ private static final String MILO_BROWSE_ROOT_RECURSIVE_2
+ = MILO_BROWSE_ROOT + "&recursive=true&depth=2";
+
+ private static final String MILO_BROWSE_ROOT_RECURSIVE_2_ONE_ID_PER_REQ
+ = MILO_BROWSE_ROOT + "&recursive=true&depth=2&maxNodeIdsPerRequest=1";
+
+ private static final String MILO_BROWSE_ROOT_RECURSIVE_FILTER
+ = MILO_BROWSE_ROOT + "&recursive=true&depth=2&filter=.*i=8[6,8].*";
+
+ private static final String MILO_BROWSE_INVERSE
+ = MILO_BROWSE_ROOT + "&direction=Inverse";
+
+ private static final String MILO_BROWSE_TYPES_ONLY
+ = MILO_BROWSE_ROOT + "&nodeClasses=Object,Variable,DataType&recursive=true&depth=5";
+
+ private static final String MILO_BROWSE_NO_TYPES
+ = MILO_BROWSE_ROOT + "&nodeClasses=Variable&recursive=true&depth=5";
+
+ private static final String MILO_BROWSE_NODE_VIA_ENDPOINT
+ = MILO_BROWSE_ROOT + "&node=RAW(ns=0;i=86)";
+
+ @EndpointInject(MOCK_TEST_1)
+ protected MockEndpoint mock1;
+
+ @EndpointInject(MOCK_TEST_2)
+ protected MockEndpoint mock2;
+
+ @EndpointInject(MOCK_TEST_3)
+ protected MockEndpoint mock3;
+
+ @EndpointInject(MOCK_TEST_4)
+ protected MockEndpoint mock4;
+
+ @EndpointInject(MOCK_TEST_5)
+ protected MockEndpoint mock5;
+
+ @EndpointInject(MOCK_TEST_6)
+ protected MockEndpoint mock6;
+
+ @EndpointInject(MOCK_TEST_7)
+ protected MockEndpoint mock7;
+
+ @EndpointInject(MOCK_TEST_8)
+ protected MockEndpoint mock8;
+
+ @EndpointInject(MOCK_TEST_9)
+ protected MockEndpoint mock9;
+
+ @Produce(DIRECT_START_1)
+ protected ProducerTemplate producer1;
+
+ @Produce(DIRECT_START_2)
+ protected ProducerTemplate producer2;
+
+ @Produce(DIRECT_START_3)
+ protected ProducerTemplate producer3;
+
+ @Produce(DIRECT_START_4)
+ protected ProducerTemplate producer4;
+
+ @Produce(DIRECT_START_5)
+ protected ProducerTemplate producer5;
+
+ @Produce(DIRECT_START_6)
+ protected ProducerTemplate producer6;
+
+ @Produce(DIRECT_START_7)
+ protected ProducerTemplate producer7;
+
+ @Produce(DIRECT_START_8)
+ protected ProducerTemplate producer8;
+
+ @Produce(DIRECT_START_9)
+ protected ProducerTemplate producer9;
+
+ @Override
+ protected RoutesBuilder createRouteBuilder() throws Exception {
+ return new RouteBuilder() {
+ @Override
+ public void configure() throws Exception {
+ from(DIRECT_START_1).enrich(resolve(MILO_BROWSE_ROOT)).to(MOCK_TEST_1);
+ from(DIRECT_START_2).enrich(resolve(MILO_BROWSE_WITHOUT_SUB_TYPES)).to(MOCK_TEST_2);
+ from(DIRECT_START_3).enrich(resolve(MILO_BROWSE_ROOT_RECURSIVE_2)).to(MOCK_TEST_3);
+ from(DIRECT_START_4).enrich(resolve(MILO_BROWSE_ROOT_RECURSIVE_FILTER)).to(MOCK_TEST_4);
+ from(DIRECT_START_5).enrich(resolve(MILO_BROWSE_INVERSE)).to(MOCK_TEST_5);
+ from(DIRECT_START_6).enrich(resolve(MILO_BROWSE_TYPES_ONLY)).to(MOCK_TEST_6);
+ from(DIRECT_START_7).enrich(resolve(MILO_BROWSE_NO_TYPES)).to(MOCK_TEST_7);
+ from(DIRECT_START_8).enrich(resolve(MILO_BROWSE_NODE_VIA_ENDPOINT)).to(MOCK_TEST_8);
+ from(DIRECT_START_9).enrich(resolve(MILO_BROWSE_ROOT_RECURSIVE_2_ONE_ID_PER_REQ)).to(MOCK_TEST_9);
+ }
+ };
+ }
+
+ private void assertBrowseResult(final BrowseResult browseResult, final String... expectedDisplayNames) {
+
+ assertNotNull(browseResult);
+
+ assertTrue(browseResult.getStatusCode().isGood());
+ assertFalse(browseResult.getStatusCode().isBad());
+
+ final ReferenceDescription[] references = browseResult.getReferences();
+
+ if (null == expectedDisplayNames || expectedDisplayNames.length == 0) {
+
+ assertTrue(references == null || references.length == 0);
+ } else {
+
+ assertNotNull(references);
+
+ final String[] displayNames = Arrays.stream(references).map(ReferenceDescription::getDisplayName)
+ .map(LocalizedText::getText).toArray(String[]::new);
+
+ assertArrayEquals(expectedDisplayNames, displayNames);
+ }
+ }
+
+ private void assertBrowseResult(
+ final Map<ExpandedNodeId, BrowseResult> browseResults, final String... expectedDisplayNames) {
+
+ assertEquals(1, browseResults.values().size());
+ assertBrowseResult(browseResults.values().toArray(new BrowseResult[0])[0], expectedDisplayNames);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void assertBrowseResult(final Message message, final String... expectedDisplayNames) {
+
+ final Map<ExpandedNodeId, BrowseResult> browseResults = (Map<ExpandedNodeId, BrowseResult>) message.getBody(Map.class);
+ assertNotNull(browseResults);
+
+ assertBrowseResult(browseResults, expectedDisplayNames);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void assertBrowseResults(final Message message, final int expectedNumberOfResults, final int expectedNumberOfIds) {
+
+ final Map<ExpandedNodeId, BrowseResult> browseResults = (Map<ExpandedNodeId, BrowseResult>) message.getBody(Map.class);
+ assertNotNull(browseResults);
+
+ final List<?> nodes = message.getHeader(NodeIds.HEADER_NODE_IDS, List.class);
+ assertNotNull(nodes);
+
+ assertEquals(expectedNumberOfResults, browseResults.keySet().size());
+ assertEquals(expectedNumberOfIds, expectedNumberOfIds);
+ }
+
+ @SuppressWarnings("unused") // For debugging tests
+ private void visualizeTree(
+ final Map<ExpandedNodeId, BrowseResult> browseResults, final ExpandedNodeId expandedNodeId,
+ final String displayName, final StringBuilder builder, int depth) {
+
+ BrowseResult browseResult = browseResults.get(expandedNodeId);
+ if (null == browseResult) {
+ return;
+ }
+ String indent = CharBuffer.allocate(depth * 3).toString().replace('\0', ' ');
+ builder.append(indent).append(expandedNodeId.toParseableString()).append(" (").append(displayName).append(")")
+ .append(System.lineSeparator());
+ if (null != browseResult.getReferences()) {
+ for (final ReferenceDescription referenceDescription : browseResult.getReferences()) {
+ visualizeTree(browseResults, referenceDescription.getNodeId(), referenceDescription.getDisplayName().getText(),
+ builder, depth + 1);
+ }
+ }
+
+ }
+
+ // Test default behaviour (browsing root node)
+ @Test
+ public void testBrowseRoot() throws Exception {
+
+ mock1.reset();
+ mock1.setExpectedCount(1);
+ mock1.expectedMessagesMatches(assertPredicate(e -> assertBrowseResult(e.getMessage(),
+ "Objects", "Types", "Views")));
+ producer1.send(ExchangeBuilder.anExchange(context).build());
+ assertIsSatisfied(5, TimeUnit.SECONDS, mock1);
+ }
+
+ // Test explicit node specification (types folder) via header field
+ @Test
+ public void testBrowseTypesHeader() throws Exception {
+
+ mock1.reset();
+ mock1.setExpectedCount(1);
+ mock1.expectedMessagesMatches(assertPredicate(e -> assertBrowseResult(e.getMessage(),
+ "ObjectTypes", "VariableTypes", "DataTypes", "ReferenceTypes", "EventTypes")));
+ producer1.send(ExchangeBuilder.anExchange(context)
+ .withHeader(NodeIds.HEADER_NODE_IDS, Collections.singletonList(Identifiers.TypesFolder.toParseableString()))
+ .build());
+ assertIsSatisfied(5, TimeUnit.SECONDS, mock1);
+ }
+
+ // Test explicit node specification (types folder) via endpoint parameter
+ @Test
+ public void testBrowseTypesEndpoint() throws Exception {
+
+ mock8.reset();
+ mock8.setExpectedCount(1);
+ mock8.expectedMessagesMatches(assertPredicate(e -> assertBrowseResult(e.getMessage(),
+ "ObjectTypes", "VariableTypes", "DataTypes", "ReferenceTypes", "EventTypes")));
+ producer8.send(ExchangeBuilder.anExchange(context).build());
+ assertIsSatisfied(5, TimeUnit.SECONDS, mock8);
+ }
+
+ // Test that reference array is empty, if indicated that sub types are not to be included (non-recursive browse only)
+ @Test
+ public void testBrowseWithoutSubTypes() throws Exception {
+
+ mock2.reset();
+ mock2.setExpectedCount(1);
+ mock2.expectedMessagesMatches(assertPredicate(e -> assertBrowseResult(e.getMessage())));
+ producer2.send(ExchangeBuilder.anExchange(context).build());
+ assertIsSatisfied(5, TimeUnit.SECONDS, mock2);
+ }
+
+ // Test recursive browse option with maximum depth of two
+ @Test
+ public void testBrowseRecursive() throws Exception {
+
+ mock3.reset();
+ mock3.setExpectedCount(1);
+ mock3.expectedMessagesMatches(assertPredicate(e -> assertBrowseResults(e.getMessage(), 4, 10)));
+ producer3.send(ExchangeBuilder.anExchange(context).build());
+ assertIsSatisfied(5, TimeUnit.SECONDS, mock3);
+ }
+
+ // Test recursive browse option with maximum depth of two; just one node per request should result in the same results
+ @Test
+ public void testBrowseRecursiveOneNodePerRequest() throws Exception {
+
+ mock9.reset();
+ mock9.setExpectedCount(1);
+ mock9.expectedMessagesMatches(assertPredicate(e -> assertBrowseResults(e.getMessage(), 4, 10)));
+ producer9.send(ExchangeBuilder.anExchange(context).build());
+ assertIsSatisfied(5, TimeUnit.SECONDS, mock9);
+ }
+
+ // Test filter option while browsing recursively (it's expected to work on both levels, base node as well as sub nodes)
+ @Test
+ public void testBrowseRecursiveFilter() throws Exception {
+
+ mock4.reset();
+ mock4.setExpectedCount(1);
+ mock4.expectedMessagesMatches(assertPredicate(e -> assertBrowseResults(e.getMessage(), 2, 2)));
+ producer4.send(ExchangeBuilder.anExchange(context).build());
+ assertIsSatisfied(5, TimeUnit.SECONDS, mock4);
+ }
+
+ // Test direction option while browsing (back to root, from types folder)
+ @Test
+ public void testBrowseInverse() throws Exception {
+
+ mock5.reset();
+ mock5.setExpectedCount(1);
+ mock5.expectedMessagesMatches(assertPredicate(e -> assertBrowseResult(e.getMessage(), "Root")));
+ producer5.send(ExchangeBuilder.anExchange(context)
+ .withHeader(NodeIds.HEADER_NODE_IDS, Collections.singletonList(Identifiers.TypesFolder.toParseableString()))
+ .build());
+ assertIsSatisfied(5, TimeUnit.SECONDS, mock5);
+ }
+
+ // Test empty answer when browsing invalid node
+ @Test
+ public void testBrowseInvalid() throws Exception {
+ mock1.reset();
+ mock1.setExpectedCount(0);
+ final Exchange exchange = producer1.send(ExchangeBuilder.anExchange(context)
+ .withHeader(NodeIds.HEADER_NODE_IDS, Collections.singletonList("invalidNodeId"))
+ .build());
+ assertIsSatisfied(5, TimeUnit.SECONDS, mock1);
+ assertNotNull(exchange.getException());
+ }
+
+ // Test node classes option, searching for types
+ @Test
+ public void testBrowseTypesClass() throws Exception {
+ mock6.reset();
+ mock6.setExpectedCount(1);
+ mock6.expectedMessagesMatches(assertPredicate(e -> assertBrowseResults(e.getMessage(), 9, 9)));
+ producer6.send(ExchangeBuilder.anExchange(context)
+ .withHeader(NodeIds.HEADER_NODE_IDS, Collections.singletonList(Identifiers.String.toParseableString()))
+ .build());
+ assertIsSatisfied(5, TimeUnit.SECONDS, mock6);
+ }
+
+ // Test node classes option, not searching for types
+ @Test
+ public void testBrowseNoTypesClass() throws Exception {
+ mock7.reset();
+ mock7.setExpectedCount(1);
+ mock7.expectedMessagesMatches(assertPredicate(e -> assertBrowseResults(e.getMessage(), 1, 0)));
+ producer7.send(ExchangeBuilder.anExchange(context)
+ .withHeader(NodeIds.HEADER_NODE_IDS, Collections.singletonList(Identifiers.String.toParseableString()))
+ .build());
+ assertIsSatisfied(5, TimeUnit.SECONDS, mock7);
+ }
+}
diff --git a/components/camel-milo/src/test/java/org/apache/camel/component/milo/call/MockCamelNamespace.java b/components/camel-milo/src/test/java/org/apache/camel/component/milo/call/MockCamelNamespace.java
index ebb58e6..70bbe26 100644
--- a/components/camel-milo/src/test/java/org/apache/camel/component/milo/call/MockCamelNamespace.java
+++ b/components/camel-milo/src/test/java/org/apache/camel/component/milo/call/MockCamelNamespace.java
@@ -112,7 +112,6 @@ public class MockCamelNamespace extends ManagedNamespace {
methodNode.getNodeId(),
Identifiers.HasComponent,
folderNode.getNodeId().expanded(),
- folderNode.getNodeClass(),
false));
}
diff --git a/components/camel-milo/src/test/resources/log4j2.properties b/components/camel-milo/src/test/resources/log4j2.properties
index 68f30b8..dcb705d 100644
--- a/components/camel-milo/src/test/resources/log4j2.properties
+++ b/components/camel-milo/src/test/resources/log4j2.properties
@@ -26,3 +26,9 @@ appender.out.layout.type = PatternLayout
appender.out.layout.pattern = %d [%-15.15t] %-5p %-30.30c{1} - %m%n
rootLogger.level = INFO
rootLogger.appenderRef.file.ref = file
+
+logger.internal.name = org.apache.camel.component.milo.client.internal
+logger.internal.level = INFO
+
+logger.browse.name = org.apache.camel.component.milo.browse
+logger.browse.level = INFO