diff --git a/opc-ua-sdk/integration-tests/src/test/java/org/eclipse/milo/opcua/sdk/server/api/util/AttributeWriterTest.java b/opc-ua-sdk/integration-tests/src/test/java/org/eclipse/milo/opcua/sdk/server/api/util/AttributeWriterTest.java new file mode 100644 index 0000000000..862a6bc2df --- /dev/null +++ b/opc-ua-sdk/integration-tests/src/test/java/org/eclipse/milo/opcua/sdk/server/api/util/AttributeWriterTest.java @@ -0,0 +1,126 @@ +package org.eclipse.milo.opcua.sdk.server.api.util; + +import org.eclipse.milo.opcua.sdk.core.AccessLevel; +import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode; +import org.eclipse.milo.opcua.sdk.test.AbstractClientServerTest; +import org.eclipse.milo.opcua.stack.core.Identifiers; +import org.eclipse.milo.opcua.stack.core.StatusCodes; +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.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.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.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class AttributeWriterTest extends AbstractClientServerTest { + + @Test + void writeNullAllowed() throws Exception { + StatusCode statusCode = client.writeValue( + new NodeId(2, "AllowNulls"), + DataValue.valueOnly(Variant.NULL_VALUE) + ).get(); + + assertEquals(StatusCode.GOOD, statusCode); + } + + @Test + void writeNullDisallowed() throws Exception { + StatusCode statusCode = client.writeValue( + new NodeId(2, "DisallowNulls"), + DataValue.valueOnly(Variant.NULL_VALUE) + ).get(); + + assertEquals(new StatusCode(StatusCodes.Bad_TypeMismatch), statusCode); + } + + @Test + void writeNullNotConfigured() throws Exception { + // Default behavior when AllowNulls property is not configured is to reject null values. + StatusCode statusCode = client.writeValue( + new NodeId(2, "AllowNullsNotConfigured"), + DataValue.valueOnly(Variant.NULL_VALUE) + ).get(); + + assertEquals(new StatusCode(StatusCodes.Bad_TypeMismatch), statusCode); + } + + @Test + void writeByteStringToUByteArray() throws Exception { + StatusCode statusCode = client.writeValue( + new NodeId(2, "UByteArray"), + DataValue.valueOnly(new Variant(ByteString.of(new byte[]{1, 2, 3}))) + ).get(); + + assertEquals(StatusCode.GOOD, statusCode); + } + + @BeforeAll + void configure() { + testNamespace.configureNode((context, nodeManager) -> { + UaVariableNode allowNulls = UaVariableNode.build( + context, + b -> { + b.setNodeId(new NodeId(2, "AllowNulls")); + b.setBrowseName(new QualifiedName(2, "AllowNulls")); + b.setDisplayName(LocalizedText.english("AllowNulls")); + b.setDataType(Identifiers.String); + b.setAccessLevel(AccessLevel.READ_WRITE); + b.setUserAccessLevel(AccessLevel.READ_WRITE); + return b.buildAndAdd(); + } + ); + + allowNulls.setAllowNulls(true); + + UaVariableNode disallowNulls = UaVariableNode.build( + context, + b -> { + b.setNodeId(new NodeId(2, "DisallowNulls")); + b.setBrowseName(new QualifiedName(2, "DisallowNulls")); + b.setDisplayName(LocalizedText.english("DisallowNulls")); + b.setDataType(Identifiers.String); + b.setAccessLevel(AccessLevel.READ_WRITE); + b.setUserAccessLevel(AccessLevel.READ_WRITE); + return b.buildAndAdd(); + } + ); + + disallowNulls.setAllowNulls(false); + + UaVariableNode.build( + context, + b -> { + b.setNodeId(new NodeId(2, "AllowNullsNotConfigured")); + b.setBrowseName(new QualifiedName(2, "AllowNullsNotConfigured")); + b.setDisplayName(LocalizedText.english("AllowNullsNotConfigured")); + b.setDataType(Identifiers.String); + b.setAccessLevel(AccessLevel.READ_WRITE); + b.setUserAccessLevel(AccessLevel.READ_WRITE); + return b.buildAndAdd(); + } + ); + + UaVariableNode.build( + context, + b -> { + b.setNodeId(new NodeId(2, "UByteArray")); + b.setBrowseName(new QualifiedName(2, "UByteArray")); + b.setDisplayName(LocalizedText.english("UByteArray")); + b.setDataType(Identifiers.Byte); + b.setArrayDimensions(new UInteger[]{UInteger.valueOf(0)}); + b.setAccessLevel(AccessLevel.READ_WRITE); + b.setUserAccessLevel(AccessLevel.READ_WRITE); + return b.buildAndAdd(); + } + ); + }); + } + +} diff --git a/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/util/AttributeWriter.java b/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/util/AttributeWriter.java index dbf94f49e2..aacedca1d2 100644 --- a/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/util/AttributeWriter.java +++ b/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/util/AttributeWriter.java @@ -23,6 +23,7 @@ import org.eclipse.milo.opcua.sdk.server.nodes.AttributeContext; import org.eclipse.milo.opcua.sdk.server.nodes.UaNode; import org.eclipse.milo.opcua.sdk.server.nodes.UaServerNode; +import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode; import org.eclipse.milo.opcua.stack.core.AttributeId; import org.eclipse.milo.opcua.stack.core.Identifiers; import org.eclipse.milo.opcua.stack.core.StatusCodes; @@ -119,6 +120,12 @@ public static void writeAttribute(AttributeContext context, ); if (attributeId == AttributeId.Value) { + boolean allowNulls = false; + if (node instanceof UaVariableNode) { + Boolean b = ((UaVariableNode) node).getAllowNulls(); + allowNulls = b != null ? b : false; + } + NodeId dataType = extract( node.getAttribute( internalContext, @@ -126,7 +133,7 @@ public static void writeAttribute(AttributeContext context, ); if (dataType != null) { - value = validateDataType(context.getServer(), dataType, value); + value = validateDataType(context.getServer(), dataType, value, allowNulls); } Integer valueRank = extract( @@ -206,13 +213,20 @@ private static WriteMask writeMaskForAttribute(AttributeId attributeId) { private static DataValue validateDataType( OpcUaServer server, NodeId dataType, - DataValue value) throws UaException { + DataValue value, + boolean allowNulls + ) throws UaException { Variant variant = value.getValue(); - if (variant == null) return value; Object o = variant.getValue(); - if (o == null) throw new UaException(StatusCodes.Bad_TypeMismatch); + if (o == null) { + if (allowNulls) { + return value; + } else { + throw new UaException(StatusCodes.Bad_TypeMismatch); + } + } Class valueClass = o.getClass().isArray() ? ArrayUtil.getType(o) : o.getClass(); diff --git a/opc-ua-sdk/sdk-server/src/test/java/org/eclipse/milo/opcua/sdk/server/util/AttributeWriterTest.java b/opc-ua-sdk/sdk-server/src/test/java/org/eclipse/milo/opcua/sdk/server/util/AttributeWriterTest.java deleted file mode 100644 index 0f94a113de..0000000000 --- a/opc-ua-sdk/sdk-server/src/test/java/org/eclipse/milo/opcua/sdk/server/util/AttributeWriterTest.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (c) 2019 the Eclipse Milo Authors - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ - -package org.eclipse.milo.opcua.sdk.server.util; - -import java.util.function.Consumer; - -import org.eclipse.milo.opcua.sdk.core.AccessLevel; -import org.eclipse.milo.opcua.sdk.core.ValueRanks; -import org.eclipse.milo.opcua.sdk.server.OpcUaServer; -import org.eclipse.milo.opcua.sdk.server.nodes.AttributeContext; -import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode; -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.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.builtin.Variant; -import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UByte; -import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; -import org.mockito.Mockito; -import org.testng.Assert; -import org.testng.annotations.Test; - -import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint; - -public class AttributeWriterTest { - - @Test - public void testVariantToVariant() throws UaException { - testWriteConversion(new Variant("String"), null, null); - } - - @Test - public void testStringToString() throws UaException { - testWriteConversion(new Variant("String"), Identifiers.String, null); - } - - @Test - public void testStringToDouble() throws UaException { - expectFailure(StatusCodes.Bad_TypeMismatch, () -> testWriteConversion(new Variant("String"), Identifiers.Double, null)); - } - - @Test - public void testByteStringToUByteArray() throws UaException { - testWriteConversion(new Variant(ByteString.of("foo".getBytes())), Identifiers.Byte, node -> { - node.setValueRank(ValueRanks.OneDimension); - node.setArrayDimensions(new UInteger[]{uint(0)}); - }); - } - - public interface UaOperation { - void run() throws UaException; - } - - private static void expectFailure(long code, UaOperation operation) { - try { - operation.run(); - Assert.fail("Operation is expected to fail with code: " + code); - } catch (UaException e) { - Assert.assertEquals(e.getStatusCode().getValue(), code, "Status code does not match"); - } - } - - private void testWriteConversion( - Variant value, - NodeId dataType, - Consumer nodeCustomizer) throws UaException { - - testWriteConversion(new DataValue(value), dataType, nodeCustomizer); - - } - - private void testWriteConversion( - DataValue value, - NodeId dataType, - Consumer nodeCustomizer) throws UaException { - - final UaVariableNode varNode = createMockNode("test", node -> { - UByte accessLevel = AccessLevel.toValue(AccessLevel.READ_WRITE); - node.setAccessLevel(accessLevel); - node.setUserAccessLevel(accessLevel); - if (nodeCustomizer != null) { - nodeCustomizer.accept(node); - } - }); - - if (dataType != null) { - varNode.setDataType(dataType); - } - - OpcUaServer server = Mockito.mock(OpcUaServer.class); - - AttributeWriter.writeAttribute( - new AttributeContext(server, null), - varNode, - AttributeId.Value, - value, - null - ); - } - - private UaVariableNode createMockNode( - String id, - Consumer nodeCustomizer) { - - final NodeId nodeId = new NodeId(0, id); - - final QualifiedName browseName = new QualifiedName(0, id); - final LocalizedText displayName = LocalizedText.english(id); - - final UaVariableNode node = new UaVariableNode( - null, nodeId, browseName, displayName); - - if (nodeCustomizer != null) { - nodeCustomizer.accept(node); - } - - return node; - } - -}