This is an automated email from the ASF dual-hosted git repository. zehnder pushed a commit to branch SP-1713 in repository https://gitbox.apache.org/repos/asf/streampipes.git
commit c92ef0118c592a3d7f025449611bb54cc032f58f Author: Philipp Zehnder <[email protected]> AuthorDate: Mon Jun 26 10:29:22 2023 +0200 [#1713] Fix NullPointer in OPC UA browsing. Fix OPC UA demo docker container. --- .../cli/deploy/standalone/opcua/docker-compose.yml | 1 + .../streampipes-connectors-opcua/pom.xml | 12 + .../connectors/opcua/adapter/OpcUaNodeBrowser.java | 36 +-- .../opcua/adapter/OpcUaNodeMetadataExtractor.java | 171 ++++++++++++++ .../adapter/OpcUaNodeMetadataExtractorTest.java | 258 +++++++++++++++++++++ 5 files changed, 444 insertions(+), 34 deletions(-) diff --git a/installer/cli/deploy/standalone/opcua/docker-compose.yml b/installer/cli/deploy/standalone/opcua/docker-compose.yml index 65ed1de07..994d8b90b 100644 --- a/installer/cli/deploy/standalone/opcua/docker-compose.yml +++ b/installer/cli/deploy/standalone/opcua/docker-compose.yml @@ -21,6 +21,7 @@ services: - ./config-generated/:/appdata ports: - 50000:50000 + command: --ut=true logging: driver: "json-file" options: diff --git a/streampipes-extensions/streampipes-connectors-opcua/pom.xml b/streampipes-extensions/streampipes-connectors-opcua/pom.xml index b5b3350e9..b7c34ab0d 100644 --- a/streampipes-extensions/streampipes-connectors-opcua/pom.xml +++ b/streampipes-extensions/streampipes-connectors-opcua/pom.xml @@ -60,6 +60,18 @@ <groupId>org.eclipse.milo</groupId> <artifactId>sdk-client</artifactId> </dependency> + + <!-- Test dependencies --> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> </dependencies> </project> diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaNodeBrowser.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaNodeBrowser.java index b9ef833c5..fa33a2896 100644 --- a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaNodeBrowser.java +++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaNodeBrowser.java @@ -28,20 +28,16 @@ import org.eclipse.milo.opcua.sdk.client.OpcUaClient; import org.eclipse.milo.opcua.sdk.client.nodes.UaNode; import org.eclipse.milo.opcua.sdk.client.nodes.UaVariableNode; 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.NodeId; import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode; import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; import org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass; -import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; @@ -115,42 +111,13 @@ public class OpcUaNodeBrowser { childNode.setNodeName(node.getDisplayName().getText()); childNode.setInternalNodeName(node.getNodeId().toParseableString()); childNode.setDataNode(isDataNode(node)); - childNode.setNodeMetadata(buildMetadata(client, node)); + childNode.setNodeMetadata(new OpcUaNodeMetadataExtractor(client, node).extract()); return childNode; }) .collect(Collectors.toList()); } - private Map<String, Object> buildMetadata(OpcUaClient client, - UaNode node) { - var metadata = new HashMap<String, Object>(); - metadata.put("NamespaceIndex", node.getNodeId().getNamespaceIndex().toString()); - metadata.put("NodeClass", node.getNodeClass().toString()); - metadata.put("Description", node.getDescription().getText()); - metadata.put("BrowseName", node.getBrowseName().getName()); - metadata.put("DisplayName", node.getDisplayName().getText()); - if (node instanceof UaVariableNode) { - var dataTypeNodeId = ((UaVariableNode) node).getDataType(); - metadata.put("Value", String.valueOf(((UaVariableNode) node).getValue().getValue().getValue())); - try { - var dataTypeNode = client.getAddressSpace().getNode(dataTypeNodeId); - var value = client.readValue(0, TimestampsToReturn.Both, node.getNodeId()).get(); - metadata.put("SourceTime", value.getSourceTime().getJavaDate().toString()); - metadata.put("ServerTime", value.getServerTime().getJavaDate().toString()); - var statusCode = value.getStatusCode().getValue(); - var statusCodeName = StatusCodes.lookup(statusCode).get(); - if (statusCodeName.length > 0) { - metadata.put("Status", statusCodeName[0]); - } - metadata.put("DataType", dataTypeNode.getDisplayName().getText()); - } catch (UaException | ExecutionException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - return metadata; - } private AddressSpace getAddressSpace() { return client.getAddressSpace(); @@ -160,4 +127,5 @@ public class OpcUaNodeBrowser { return (node.getNodeClass().equals(NodeClass.Variable) || (node.getNodeClass().equals(NodeClass.VariableType))) && node instanceof UaVariableNode; } + } diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaNodeMetadataExtractor.java b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaNodeMetadataExtractor.java new file mode 100644 index 000000000..04dda6eb6 --- /dev/null +++ b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaNodeMetadataExtractor.java @@ -0,0 +1,171 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.streampipes.extensions.connectors.opcua.adapter; + +import org.eclipse.milo.opcua.sdk.client.OpcUaClient; +import org.eclipse.milo.opcua.sdk.client.nodes.UaNode; +import org.eclipse.milo.opcua.sdk.client.nodes.UaVariableNode; +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.DataValue; +import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime; +import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +public class OpcUaNodeMetadataExtractor { + private final OpcUaClient client; + private final UaNode node; + + private final Map<String, Object> metadata; + + public OpcUaNodeMetadataExtractor(OpcUaClient client, UaNode node) { + this.client = client; + this.node = node; + this.metadata = new HashMap<>(); + } + + public Map<String, Object> extract() { + extractNamespaceIndex(); + extractNodeClass(); + extractDescription(); + extractBrowseName(); + extractDisplayName(); + + if (node instanceof UaVariableNode) { + extractValue(); + try { + var dataTypeNodeId = ((UaVariableNode) node).getDataType(); + var dataTypeNode = client.getAddressSpace().getNode(dataTypeNodeId); + var value = client.readValue(0, TimestampsToReturn.Both, node.getNodeId()).get(); + + extractSourceTime(value); + extractServerTime(value); + extractStatusCode(value); + extractDataType(dataTypeNode); + } catch (UaException | ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + return metadata; + } + + + public void extractDescription() { + if (node.getDescription() != null) { + add("Description", node.getDescription().getText()); + } else { + add("Description", ""); + } + } + + public void extractNamespaceIndex() { + if (node.getNodeId() != null) { + add("NamespaceIndex", node.getNodeId().getNamespaceIndex().toString()); + } else { + add("NamespaceIndex", ""); + } + } + + public void extractNodeClass() { + if (node.getNodeClass() != null) { + add("NodeClass", node.getNodeClass().toString()); + } else { + add("NodeClass", ""); + } + } + + public void extractBrowseName() { + if (node.getBrowseName() != null) { + add("BrowseName", node.getBrowseName().getName()); + } else { + add("BrowseName", ""); + } + } + + public void extractDisplayName() { + if (node.getDisplayName() != null) { + add("DisplayName", node.getDisplayName().getText()); + } else { + add("DisplayName", ""); + } + } + + public void extractValue() { + if (node instanceof UaVariableNode) { + var v1 = ((UaVariableNode) node).getValue(); + if (v1 != null) { + var v2 = v1.getValue(); + if (v2 != null) { + add("Value", String.valueOf(v2.getValue())); + } + } + } + } + + public void extractSourceTime(DataValue value) { + add("SourceTime", dateTimeToString(value.getSourceTime())); + } + + public void extractServerTime(DataValue value) { + add("ServerTime", dateTimeToString(value.getServerTime())); + } + + public void extractStatusCode(DataValue value) { + var statusCode = value.getStatusCode(); + if (statusCode != null) { + var statusCodeValue = statusCode.getValue(); + var lookup = StatusCodes.lookup(statusCodeValue); + if (lookup.isPresent()) { + var statusCodeName = lookup.get(); + if (statusCodeName.length > 0) { + metadata.put("Status", statusCodeName[0]); + } + } + } + } + + public void extractDataType(UaNode dataTypeNode) { + if (dataTypeNode.getDisplayName() != null) { + metadata.put("DataType", dataTypeNode.getDisplayName().getText()); + } else { + metadata.put("DataType", ""); + } + } + + private String dateTimeToString(DateTime time) { + if (time != null) { + return time.getJavaDate().toString(); + } else { + return ""; + } + } + + public Map<String, Object> getMetadata() { + return metadata; + } + + private void add(String key, String value) { + metadata.put(key, value); + } + +} diff --git a/streampipes-extensions/streampipes-connectors-opcua/src/test/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaNodeMetadataExtractorTest.java b/streampipes-extensions/streampipes-connectors-opcua/src/test/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaNodeMetadataExtractorTest.java new file mode 100644 index 000000000..e9b5e7691 --- /dev/null +++ b/streampipes-extensions/streampipes-connectors-opcua/src/test/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaNodeMetadataExtractorTest.java @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.streampipes.extensions.connectors.opcua.adapter; + +import org.eclipse.milo.opcua.sdk.client.nodes.UaNode; +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.LocalizedText; +import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; +import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName; +import org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class OpcUaNodeMetadataExtractorTest { + + private final String date = "Tue Jan 02 23:52:56 CET 1601"; + + @Test + public void testExtractDescription() { + var description = "test"; + var node = mock(UaNode.class); + when(node.getDescription()).thenReturn(new LocalizedText(description)); + var extractor = getExtractor(node); + + extractor.extractDescription(); + + var metadata = extractor.getMetadata(); + assertTrue(metadata.containsKey("Description")); + assertEquals(description, metadata.get("Description")); + } + + @Test + public void testExtractDescriptionNull() { + var node = mock(UaNode.class); + when(node.getDescription()).thenReturn(null); + var extractor = getExtractor(node); + + extractor.extractDescription(); + + var metadata = extractor.getMetadata(); + assertTrue(metadata.containsKey("Description")); + assertEquals("", metadata.get("Description")); + } + + @Test + public void testExtractNamespaceIndex() { + var node = mock(UaNode.class); + when(node.getNodeId()).thenReturn(new NodeId(1, 1)); + var extractor = getExtractor(node); + + extractor.extractNamespaceIndex(); + + var metadata = extractor.getMetadata(); + assertTrue(metadata.containsKey("NamespaceIndex")); + assertEquals("1", metadata.get("NamespaceIndex")); + } + + @Test + public void testExtractNamespaceIndexNull() { + var node = mock(UaNode.class); + when(node.getNodeId()).thenReturn(null); + var extractor = getExtractor(node); + + extractor.extractNamespaceIndex(); + + var metadata = extractor.getMetadata(); + assertTrue(metadata.containsKey("NamespaceIndex")); + assertEquals("", metadata.get("NamespaceIndex")); + } + + @Test + public void testExtractNodeClass() { + var node = mock(UaNode.class); + when(node.getNodeClass()).thenReturn(NodeClass.Variable); + var extractor = getExtractor(node); + + extractor.extractNodeClass(); + + var metadata = extractor.getMetadata(); + assertTrue(metadata.containsKey("NodeClass")); + assertEquals("Variable", metadata.get("NodeClass")); + } + + @Test + public void testExtractNodeClassNull() { + var node = mock(UaNode.class); + when(node.getNodeClass()).thenReturn(null); + var extractor = getExtractor(node); + + extractor.extractNodeClass(); + + var metadata = extractor.getMetadata(); + assertTrue(metadata.containsKey("NodeClass")); + assertEquals("", metadata.get("NodeClass")); + } + + @Test + public void testExtractBrowseName() { + var expectedNodeName = "nodeName"; + var node = mock(UaNode.class); + when(node.getBrowseName()).thenReturn(new QualifiedName(1, expectedNodeName)); + var extractor = getExtractor(node); + + extractor.extractBrowseName(); + + var metadata = extractor.getMetadata(); + assertTrue(metadata.containsKey("BrowseName")); + assertEquals(expectedNodeName, metadata.get("BrowseName")); + } + + @Test + public void testExtractBrowseNameNull() { + var node = mock(UaNode.class); + when(node.getBrowseName()).thenReturn(null); + var extractor = getExtractor(node); + + extractor.extractBrowseName(); + + var metadata = extractor.getMetadata(); + assertTrue(metadata.containsKey("BrowseName")); + assertEquals("", metadata.get("BrowseName")); + } + + @Test + public void testExtractDisplayName() { + var expectedName = "expectedName"; + var node = mock(UaNode.class); + when(node.getDisplayName()).thenReturn(new LocalizedText(expectedName)); + var extractor = getExtractor(node); + + extractor.extractDisplayName(); + + var metadata = extractor.getMetadata(); + assertTrue(metadata.containsKey("DisplayName")); + assertEquals(expectedName, metadata.get("DisplayName")); + } + + @Test + public void testExtractDisplayNameNull() { + var node = mock(UaNode.class); + when(node.getDisplayName()).thenReturn(null); + var extractor = getExtractor(node); + + extractor.extractDisplayName(); + + var metadata = extractor.getMetadata(); + assertTrue(metadata.containsKey("DisplayName")); + assertEquals("", metadata.get("DisplayName")); + } + + @Test + public void testExtractSourceTime() { + var value = mock(DataValue.class); + when(value.getSourceTime()).thenReturn(new DateTime(1687762000000L)); + var extractor = getExtractor(); + + extractor.extractSourceTime(value); + + var metadata = extractor.getMetadata(); + assertTrue(metadata.containsKey("SourceTime")); + assertEquals(date, metadata.get("SourceTime")); + } + + @Test + public void testExtractSourceTimeNull() { + var value = mock(DataValue.class); + when(value.getSourceTime()).thenReturn(null); + var extractor = getExtractor(); + + extractor.extractSourceTime(value); + + var metadata = extractor.getMetadata(); + assertTrue(metadata.containsKey("SourceTime")); + assertEquals("", metadata.get("SourceTime")); + } + + @Test + public void testExtractServerTime() { + var value = mock(DataValue.class); + when(value.getServerTime()).thenReturn(new DateTime(1687762000000L)); + var extractor = getExtractor(); + + extractor.extractServerTime(value); + + var metadata = extractor.getMetadata(); + assertTrue(metadata.containsKey("ServerTime")); + assertEquals(date, metadata.get("ServerTime")); + } + + @Test + public void testExtractServerTimeNull() { + var value = mock(DataValue.class); + when(value.getServerTime()).thenReturn(null); + var extractor = getExtractor(); + + extractor.extractServerTime(value); + + var metadata = extractor.getMetadata(); + assertTrue(metadata.containsKey("ServerTime")); + assertEquals("", metadata.get("ServerTime")); + } + + @Test + public void testExtractDataType() { + var expectedName = "expectedName"; + var dataTypeNode = mock(UaNode.class); + when(dataTypeNode.getDisplayName()).thenReturn(new LocalizedText(expectedName)); + var extractor = getExtractor(); + + extractor.extractDataType(dataTypeNode); + + var metadata = extractor.getMetadata(); + assertTrue(metadata.containsKey("DataType")); + assertEquals(expectedName, metadata.get("DataType")); + } + + @Test + public void testExtractDataTypeNull() { + var dataTypeNode = mock(UaNode.class); + when(dataTypeNode.getDisplayName()).thenReturn(null); + var extractor = getExtractor(); + + extractor.extractDataType(dataTypeNode); + + var metadata = extractor.getMetadata(); + assertTrue(metadata.containsKey("DataType")); + assertEquals("", metadata.get("DataType")); + } + + private OpcUaNodeMetadataExtractor getExtractor(UaNode node) { + return new OpcUaNodeMetadataExtractor(null, node); + } + + private OpcUaNodeMetadataExtractor getExtractor() { + return new OpcUaNodeMetadataExtractor(null, null); + } +} \ No newline at end of file
