Skip to content

Commit 92b3334

Browse files
authored
Disallow null for StatusCode field in DataValue (#1394)
A `null` value is semantically the same, and encodes the same, as `StatusCodes.GOOD`. Any distinction is lost during serialization.
1 parent 9d9b398 commit 92b3334

5 files changed

Lines changed: 86 additions & 18 deletions

File tree

milo-examples/client-examples/src/main/java/org/eclipse/milo/examples/client/WriteExample.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024 the Eclipse Milo Authors
2+
* Copyright (c) 2025 the Eclipse Milo Authors
33
*
44
* This program and the accompanying materials are made
55
* available under the terms of the Eclipse Public License 2.0
@@ -39,8 +39,8 @@ public void run(OpcUaClient client, CompletableFuture<OpcUaClient> future) throw
3939
for (int i = 0; i < 10; i++) {
4040
Variant v = Variant.ofInt32(i);
4141

42-
// don't write status or timestamps
43-
DataValue dv = new DataValue(v, null, null);
42+
// don't write timestamps
43+
DataValue dv = DataValue.valueOnly(v);
4444

4545
List<StatusCode> statusCodes = client.writeValues(nodeIds, List.of(dv));
4646

opc-ua-stack/encoding-json/src/test/java/org/eclipse/milo/opcua/stack/core/encoding/json/OpcUaJsonEncoderTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -890,13 +890,13 @@ public void writeDataValue() throws IOException {
890890

891891
// omit all fields
892892
encoder.reset(writer = new StringWriter());
893-
encoder.encodeDataValue(null, new DataValue(Variant.NULL_VALUE, null, null));
893+
encoder.encodeDataValue(null, new DataValue(Variant.NULL_VALUE, StatusCode.GOOD, null));
894894
assertEquals("", writer.toString());
895895

896896
// omit all fields while embedded in object
897897
encoder.reset(writer = new StringWriter());
898898
encoder.jsonWriter.beginObject();
899-
encoder.encodeDataValue("foo", new DataValue(Variant.NULL_VALUE, null, null));
899+
encoder.encodeDataValue("foo", new DataValue(Variant.NULL_VALUE, StatusCode.GOOD, null));
900900
encoder.jsonWriter.endObject();
901901
assertEquals("{}", writer.toString());
902902
}

opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/encoding/binary/OpcUaBinaryEncoder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,11 +292,11 @@ public void encodeDataValue(DataValue value) throws UaSerializationException {
292292
} else {
293293
int mask = 0x00;
294294

295-
if (value.value() != null && value.value().isNotNull()) {
295+
if (value.value().isNotNull()) {
296296
mask |= 0x01;
297297
}
298298

299-
if (!StatusCode.GOOD.equals(value.statusCode())) {
299+
if (value.getStatusCode().getValue() != 0L) {
300300
mask |= 0x02;
301301
}
302302

opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/types/builtin/DataValue.java

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
@NullMarked
2121
public record DataValue(
2222
Variant value,
23-
@Nullable StatusCode statusCode,
23+
StatusCode statusCode,
2424
@Nullable DateTime sourceTime,
2525
@Nullable UShort sourcePicoseconds,
2626
@Nullable DateTime serverTime,
@@ -42,13 +42,13 @@ public DataValue(Variant value, StatusCode status) {
4242
this(value, status, DateTime.now());
4343
}
4444

45-
public DataValue(Variant value, @Nullable StatusCode status, @Nullable DateTime time) {
45+
public DataValue(Variant value, StatusCode status, @Nullable DateTime time) {
4646
this(value, status, time, time);
4747
}
4848

4949
public DataValue(
5050
Variant value,
51-
@Nullable StatusCode status,
51+
StatusCode status,
5252
@Nullable DateTime sourceTime,
5353
@Nullable DateTime serverTime) {
5454

@@ -59,7 +59,7 @@ public Variant getValue() {
5959
return value;
6060
}
6161

62-
public @Nullable StatusCode getStatusCode() {
62+
public StatusCode getStatusCode() {
6363
return statusCode;
6464
}
6565

@@ -149,19 +149,21 @@ public static DataValue derivedNonValue(DataValue from, TimestampsToReturn times
149149
}
150150

151151
/**
152-
* Create a {@link DataValue} containing *only* the Variant. All other fields will be null.
152+
* Create a {@link DataValue} containing only a value.
153+
*
154+
* <p>{@link StatusCode#GOOD} is implied, and other fields will be null.
153155
*
154156
* @param v the value {@link Variant}.
155157
* @return a {@link DataValue} containing only the value.
156158
*/
157159
public static DataValue valueOnly(Variant v) {
158-
return new DataValue(v, null, null, null, null, null);
160+
return new DataValue(v, StatusCode.GOOD, null, null, null, null);
159161
}
160162

161163
public static class Builder {
162164

163165
public Variant value = Variant.NULL_VALUE;
164-
public @Nullable StatusCode status;
166+
public StatusCode status = StatusCode.GOOD;
165167
public @Nullable DateTime sourceTime;
166168
public @Nullable UShort sourcePicoseconds;
167169
public @Nullable DateTime serverTime;
@@ -188,7 +190,7 @@ public Builder setStatus(long statusCode) {
188190
return this;
189191
}
190192

191-
public Builder setStatus(@Nullable StatusCode status) {
193+
public Builder setStatus(StatusCode status) {
192194
this.status = status;
193195
return this;
194196
}

opc-ua-stack/stack-core/src/test/java/org/eclipse/milo/opcua/stack/core/encoding/binary/OpcUaBinaryEncoderTest.java

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@
1212

1313
import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ubyte;
1414
import static org.junit.jupiter.api.Assertions.assertEquals;
15+
import static org.junit.jupiter.api.Assertions.assertTrue;
1516

1617
import io.netty.buffer.ByteBuf;
1718
import io.netty.buffer.Unpooled;
1819
import org.eclipse.milo.opcua.stack.core.OpcUaDataType;
20+
import org.eclipse.milo.opcua.stack.core.StatusCodes;
1921
import org.eclipse.milo.opcua.stack.core.encoding.DefaultEncodingContext;
20-
import org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject;
21-
import org.eclipse.milo.opcua.stack.core.types.builtin.Matrix;
22-
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
22+
import org.eclipse.milo.opcua.stack.core.types.builtin.*;
2323
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UByte;
2424
import org.eclipse.milo.opcua.stack.core.types.enumerated.ApplicationType;
2525
import org.eclipse.milo.opcua.stack.core.types.structured.AccessLevelType;
@@ -205,4 +205,70 @@ void encodeVariantOfStructMatrix() {
205205
decodedMatrix.transform(
206206
v -> ((ExtensionObject) v).decode(DefaultEncodingContext.INSTANCE)));
207207
}
208+
209+
@Test
210+
void dataValueEncodingMaskValueBit() {
211+
DataValue dataValue = new DataValue(Variant.ofBoolean(true), StatusCode.GOOD, null);
212+
encoder.encodeDataValue(dataValue);
213+
214+
// get the EncodingMask byte out of the buffer and ensure only the Value bit is set
215+
byte encodingMask = buffer.getByte(buffer.readerIndex());
216+
assertEquals(0b00000001, encodingMask);
217+
218+
// decode the DataValue and ensure we decoded the value as true
219+
var decoder = new OpcUaBinaryDecoder(DefaultEncodingContext.INSTANCE).setBuffer(buffer);
220+
DataValue decodedDataValue = decoder.decodeDataValue();
221+
222+
assertEquals(Variant.ofBoolean(true), decodedDataValue.value());
223+
}
224+
225+
@Test
226+
void dataValueEncodingMaskNullValueBit() {
227+
DataValue dataValue = new DataValue(Variant.ofNull(), StatusCode.GOOD, null);
228+
encoder.encodeDataValue(dataValue);
229+
230+
// get the EncodingMask byte out of the buffer and ensure no bits are set
231+
byte encodingMask = buffer.getByte(buffer.readerIndex());
232+
assertEquals(0b00000000, encodingMask);
233+
234+
// decode the DataValue and ensure we decoded the value as null
235+
var decoder = new OpcUaBinaryDecoder(DefaultEncodingContext.INSTANCE).setBuffer(buffer);
236+
DataValue decodedDataValue = decoder.decodeDataValue();
237+
238+
assertTrue(decodedDataValue.value().isNull());
239+
}
240+
241+
@Test
242+
void dataValueEncodingMaskStatusCodeBit() {
243+
DataValue dataValue = new DataValue(Variant.ofBoolean(true), StatusCode.GOOD, null);
244+
encoder.encodeDataValue(dataValue);
245+
246+
// get the EncodingMask byte out of the buffer and ensure only the Value bit is set
247+
byte encodingMask = buffer.getByte(buffer.readerIndex());
248+
assertEquals(0b00000001, encodingMask);
249+
250+
// decode the DataValue and ensure we decoded the StatusCode as GOOD
251+
var decoder = new OpcUaBinaryDecoder(DefaultEncodingContext.INSTANCE).setBuffer(buffer);
252+
DataValue decodedDataValue = decoder.decodeDataValue();
253+
254+
assertEquals(StatusCode.GOOD, decodedDataValue.statusCode());
255+
}
256+
257+
@Test
258+
void dataValueEncodingMaskStatusCodeGoodOverloadBit() {
259+
DataValue dataValue =
260+
new DataValue(Variant.ofBoolean(true), new StatusCode(StatusCodes.Good_Overload), null);
261+
encoder.encodeDataValue(dataValue);
262+
263+
// get the EncodingMask byte out of the buffer and ensure both the Value and StatusCode bits are
264+
// set
265+
byte encodingMask = buffer.getByte(buffer.readerIndex());
266+
assertEquals(0b00000011, encodingMask);
267+
268+
// decode the DataValue and ensure we decoded the same StatusCode
269+
var decoder = new OpcUaBinaryDecoder(DefaultEncodingContext.INSTANCE).setBuffer(buffer);
270+
DataValue decodedDataValue = decoder.decodeDataValue();
271+
272+
assertEquals(new StatusCode(StatusCodes.Good_Overload), decodedDataValue.statusCode());
273+
}
208274
}

0 commit comments

Comments
 (0)