Skip to content

Commit

Permalink
WIP DataValue and Variant
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinherron committed Jul 14, 2022
1 parent 99845bd commit cd2b15b
Show file tree
Hide file tree
Showing 3 changed files with 331 additions and 5 deletions.
Expand Up @@ -12,8 +12,12 @@

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.lang.reflect.Array;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.UUID;
import java.util.function.Function;
Expand Down Expand Up @@ -42,6 +46,7 @@
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.ULong;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort;
import org.eclipse.milo.opcua.stack.core.types.enumerated.IdType;
import org.eclipse.milo.opcua.stack.core.util.TypeUtil;

import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ubyte;
import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;
Expand Down Expand Up @@ -972,12 +977,240 @@ public ExtensionObject readExtensionObject(String field) throws UaSerializationE

@Override
public DataValue readDataValue(String field) throws UaSerializationException {
return null;
try {
if (field != null) {
String nextName = jsonReader.nextName();
if (!field.equals(nextName)) {
throw new UaSerializationException(
StatusCodes.Bad_DecodingError,
String.format("readDataValue: %s != %s", field, nextName)
);
}
}

jsonReader.beginObject();

DataValue.Builder b = DataValue.newValue();
b.setStatus(StatusCode.GOOD);

while (jsonReader.peek() == JsonToken.NAME) {
String nextName = jsonReader.nextName();

switch (nextName) {
case "Value":
b.setValue(readVariant(null));
break;

case "Status":
b.setStatus(readStatusCode(null));
break;

case "SourceTimestamp":
b.setSourceTime(readDateTime(null));
break;

case "SourcePicoseconds":
b.setSourcePicoseconds(readUInt16(null));
break;

case "ServerTimestamp":
b.setServerTime(readDateTime(null));
break;

case "ServerPicoseconds":
b.setServerPicoseconds(readUInt16(null));
break;
}
}

jsonReader.endObject();

return b.build();
} catch (IOException e) {
throw new UaSerializationException(StatusCodes.Bad_DecodingError, e);
}
}

@Override
public Variant readVariant(String field) throws UaSerializationException {
return null;
try {
if (field != null) {
String nextName = jsonReader.nextName();
if (!field.equals(nextName)) {
throw new UaSerializationException(
StatusCodes.Bad_DecodingError,
String.format("readDataValue: %s != %s", field, nextName)
);
}
}

jsonReader.beginObject();

int typeId = 0;
JsonElement bodyElement = null;
int[] dimensions = null;

while (jsonReader.peek() == JsonToken.NAME) {
String nextName = jsonReader.nextName();

switch (nextName) {
case "Type":
typeId = jsonReader.nextInt();
break;
case "Body":
bodyElement = JsonParser.parseReader(jsonReader);
break;
case "Dimensions":
var dims = new ArrayList<Integer>();
jsonReader.beginArray();
while (jsonReader.peek() == JsonToken.NUMBER) {
dims.add(jsonReader.nextInt());
}
jsonReader.endArray();
dimensions = new int[dims.size()];
for (int i = 0; i < dims.size(); i++) {
dimensions[i] = dims.get(i);
}
break;
}
}

jsonReader.endObject();

if (bodyElement == null) {
return Variant.NULL_VALUE;
} else if (dimensions == null) {
// scalar or one-dimensional array value
JsonReader reader = jsonReader;
try {
jsonReader = new JsonReader(new StringReader(bodyElement.toString()));

if (bodyElement.isJsonArray()) {
var elements = new ArrayList<>();
jsonReader.beginArray();
while (jsonReader.peek() != JsonToken.END_ARRAY) {
elements.add(readBuiltinTypeValue(null, typeId));
}
jsonReader.endArray();
Object value = Array.newInstance(TypeUtil.getPrimitiveBackingClass(typeId), elements.size());
for (int i = 0; i < elements.size(); i++) {
Array.set(value, i, elements.get(i));
}
return new Variant(value);
} else {
Object value = readBuiltinTypeValue(null, typeId);
return new Variant(value);
}
} finally {
jsonReader = reader;
}
} else {
// multi-dimensional array value
JsonReader reader = jsonReader;
try {
jsonReader = new JsonReader(new StringReader(bodyElement.toString()));

Object value = readFlattenedMultiDimensionalVariantValue(typeId, dimensions);

return new Variant(value);
} finally {
jsonReader = reader;
}
}
} catch (IOException e) {
throw new UaSerializationException(StatusCodes.Bad_DecodingError, e);
}
}

private Object readFlattenedMultiDimensionalVariantValue(int typeId, int[] dimensions) throws IOException {
jsonReader.beginArray();
try {
return readFlattenedMultiDimensionalVariantValue(typeId, dimensions, 0);
} finally {
jsonReader.endArray();
}
}

private Object readFlattenedMultiDimensionalVariantValue(int typeId, int[] dimensions, int dimensionIndex) {
Object value;
Class<?> backingClass = TypeUtil.getPrimitiveBackingClass(typeId);

if (dimensionIndex == dimensions.length - 1) {
value = Array.newInstance(backingClass, dimensions[dimensionIndex]);
for (int i = 0; i < dimensions[dimensionIndex]; i++) {
Object e = readBuiltinTypeValue(null, typeId);
Array.set(value, i, e);
}
} else {
value = Array.newInstance(backingClass, Arrays.copyOfRange(dimensions, dimensionIndex, dimensions.length));
for (int i = 0; i < dimensions[dimensionIndex]; i++) {
Object e = readFlattenedMultiDimensionalVariantValue(typeId, dimensions, dimensionIndex + 1);
Array.set(value, i, e);
}
}

return value;
}

private Object readBuiltinTypeValue(String field, int typeId) throws UaSerializationException {
switch (typeId) {
case 1:
return readBoolean(field);
case 2:
return readSByte(field);
case 3:
return readByte(field);
case 4:
return readInt16(field);
case 5:
return readUInt16(field);
case 6:
return readInt32(field);
case 7:
return readUInt32(field);
case 8:
return readInt64(field);
case 9:
return readUInt64(field);
case 10:
return readFloat(field);
case 11:
return readDouble(field);
case 12:
return readString(field);
case 13:
return readDateTime(field);
case 14:
return readGuid(field);
case 15:
return readByteString(field);
case 16:
return readXmlElement(field);
case 17:
return readNodeId(field);
case 18:
return readExpandedNodeId(field);
case 19:
return readStatusCode(field);
case 20:
return readQualifiedName(field);
case 21:
return readLocalizedText(field);
case 22:
return readExtensionObject(field);
case 23:
return readDataValue(field);
case 24:
return readVariant(field);
case 25:
return readDiagnosticInfo(field);

default:
throw new UaSerializationException(
StatusCodes.Bad_EncodingError,
"not a built-in type: " + typeId
);
}
}

@Override
Expand Down
Expand Up @@ -21,13 +21,15 @@
import org.eclipse.milo.opcua.stack.core.StatusCodes;
import org.eclipse.milo.opcua.stack.core.UaSerializationException;
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;
import org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject;
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.XmlElement;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UByte;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
Expand Down Expand Up @@ -616,10 +618,101 @@ void readExtensionObject() throws IOException {
}

@Test
void readDataValue() throws IOException {}
void readDataValue() throws IOException {
var decoder = new OpcUaJsonDecoder(new StringReader(""));

DateTime now = DateTime.now();
String isoNow = now.toIso8601String();

var allFieldsValue = new DataValue(
new Variant("foo"),
new StatusCode(StatusCodes.Good_Overload),
now,
ushort(100),
now,
ushort(200)
);

decoder.reset(new StringReader(String.format("{\"Value\":{\"Type\":12,\"Body\":\"foo\"},\"Status\":3080192,\"SourceTimestamp\":\"%s\",\"SourcePicoseconds\":100,\"ServerTimestamp\":\"%s\",\"ServerPicoseconds\":200}", isoNow, isoNow)));
assertEquals(allFieldsValue, decoder.readDataValue(null));

// omit "Value"
decoder.reset(new StringReader(String.format("{\"Status\":3080192,\"SourceTimestamp\":\"%s\",\"SourcePicoseconds\":100,\"ServerTimestamp\":\"%s\",\"ServerPicoseconds\":200}", isoNow, isoNow)));
assertEquals(allFieldsValue.copy(b -> b.setValue(Variant.NULL_VALUE)), decoder.readDataValue(null));

// omit "Status"
decoder.reset(new StringReader(String.format("{\"Value\":{\"Type\":12,\"Body\":\"foo\"},\"SourceTimestamp\":\"%s\",\"SourcePicoseconds\":100,\"ServerTimestamp\":\"%s\",\"ServerPicoseconds\":200}", isoNow, isoNow)));
assertEquals(allFieldsValue.copy(b -> b.setStatus(StatusCode.GOOD)), decoder.readDataValue(null));

// omit "SourceTimestamp"
decoder.reset(new StringReader(String.format("{\"Value\":{\"Type\":12,\"Body\":\"foo\"},\"Status\":3080192,\"SourcePicoseconds\":100,\"ServerTimestamp\":\"%s\",\"ServerPicoseconds\":200}", isoNow)));
assertEquals(allFieldsValue.copy(b -> b.setSourceTime(null)), decoder.readDataValue(null));

// omit "SourcePicoseconds"
decoder.reset(new StringReader(String.format("{\"Value\":{\"Type\":12,\"Body\":\"foo\"},\"Status\":3080192,\"SourceTimestamp\":\"%s\",\"ServerTimestamp\":\"%s\",\"ServerPicoseconds\":200}", isoNow, isoNow)));
assertEquals(allFieldsValue.copy(b -> b.setSourcePicoseconds(null)), decoder.readDataValue(null));

// omit "ServerTimestamp"
decoder.reset(new StringReader(String.format("{\"Value\":{\"Type\":12,\"Body\":\"foo\"},\"Status\":3080192,\"SourceTimestamp\":\"%s\",\"SourcePicoseconds\":100,\"ServerPicoseconds\":200}", isoNow)));
assertEquals(allFieldsValue.copy(b -> b.setServerTime(null)), decoder.readDataValue(null));

// omit "ServerPicoseconds"
decoder.reset(new StringReader(String.format("{\"Value\":{\"Type\":12,\"Body\":\"foo\"},\"Status\":3080192,\"SourceTimestamp\":\"%s\",\"SourcePicoseconds\":100,\"ServerTimestamp\":\"%s\"}", isoNow, isoNow)));
assertEquals(allFieldsValue.copy(b -> b.setServerPicoseconds(null)), decoder.readDataValue(null));

// omit all fields
decoder.reset(new StringReader("{}"));
assertEquals(new DataValue(Variant.NULL_VALUE, StatusCode.GOOD, null), decoder.readDataValue(null));

decoder.reset(new StringReader(String.format("{\"foo\":{\"Value\":{\"Type\":12,\"Body\":\"foo\"},\"Status\":3080192,\"SourceTimestamp\":\"%s\",\"SourcePicoseconds\":100,\"ServerTimestamp\":\"%s\",\"ServerPicoseconds\":200}}", isoNow, isoNow)));
decoder.jsonReader.beginObject();
assertEquals(allFieldsValue, decoder.readDataValue("foo"));
decoder.jsonReader.endObject();
}

@Test
void readVariant() throws IOException {}
void readVariant() throws IOException {
var decoder = new OpcUaJsonDecoder(new StringReader(""));

decoder.reset(new StringReader("{\"Type\":1,\"Body\":true}"));
assertEquals(new Variant(true), decoder.readVariant(null));

decoder.reset(new StringReader("{\"Type\":20,\"Body\":{\"Name\":\"foo\",\"Uri\":1}}"));
assertEquals(new Variant(new QualifiedName(1, "foo")), decoder.readVariant(null));

decoder.reset(new StringReader("{\"Type\":24,\"Body\":[{\"Type\":12,\"Body\":\"foo\"},{\"Type\":12,\"Body\":\"bar\"}]}"));
assertEquals(new Variant(new Variant[]{new Variant("foo"), new Variant("bar")}), decoder.readVariant(null));

int[] value1d = {0, 1, 2, 3};
int[][] value2d = {
{0, 2, 3},
{1, 3, 4}
};
int[][][] value3d = {
{
{0, 1},
{2, 3}
},
{
{4, 5},
{6, 7},
}
};

decoder.reset(new StringReader("{\"Type\":6,\"Body\":[0,1,2,3]}"));
assertEquals(new Variant(value1d), decoder.readVariant(null));

decoder.reset(new StringReader("{\"Type\":6,\"Body\":[0,2,3,1,3,4],\"Dimensions\":[2,3]}"));
assertEquals(new Variant(value2d), decoder.readVariant(null));

decoder.reset(new StringReader("{\"Type\":6,\"Body\":[0,1,2,3,4,5,6,7],\"Dimensions\":[2,2,2]}"));
assertEquals(new Variant(value3d), decoder.readVariant(null));

decoder.reset(new StringReader("{\"foo\":{\"Type\":1,\"Body\":true}}"));
decoder.jsonReader.beginObject();
assertEquals(new Variant(true), decoder.readVariant("foo"));
decoder.jsonReader.endObject();
}

@Test
void readDiagnosticInfo() throws IOException {}
Expand Down
Expand Up @@ -817,7 +817,7 @@ public void writeDataValue() throws IOException {
encoder.writeDataValue(null, allFieldsValue.copy(b -> b.setSourceTime(null)));
assertEquals(String.format("{\"Value\":{\"Type\":12,\"Body\":\"foo\"},\"Status\":3080192,\"SourcePicoseconds\":100,\"ServerTimestamp\":\"%s\",\"ServerPicoseconds\":200}", isoNow), writer.toString());

// omit "SourcePicoseconds
// omit "SourcePicoseconds"
encoder.reset(writer = new StringWriter());
encoder.writeDataValue(null, allFieldsValue.copy(b -> b.setSourcePicoseconds(null)));
assertEquals(String.format("{\"Value\":{\"Type\":12,\"Body\":\"foo\"},\"Status\":3080192,\"SourceTimestamp\":\"%s\",\"ServerTimestamp\":\"%s\",\"ServerPicoseconds\":200}", isoNow, isoNow), writer.toString());
Expand Down

0 comments on commit cd2b15b

Please sign in to comment.