From 85655538dcb7a8f7f41c0f87b53a9c3ba84cfab5 Mon Sep 17 00:00:00 2001 From: Jochen Mader Date: Fri, 13 Dec 2024 11:40:17 +0100 Subject: [PATCH] Added support for arrays --- .../opcua/mqtt2opcua/BuiltinJsonSchema.java | 33 +++++++++++++++++++ .../opcua/mqtt2opcua/JsonSchemaGenerator.java | 29 ++++++++++++---- .../mqtt2opcua/JsonToOpcUAConverterTest.java | 5 ++- 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/modules/hivemq-edge-module-opcua/src/main/java/com/hivemq/edge/adapters/opcua/mqtt2opcua/BuiltinJsonSchema.java b/modules/hivemq-edge-module-opcua/src/main/java/com/hivemq/edge/adapters/opcua/mqtt2opcua/BuiltinJsonSchema.java index 3012bc0f64..48b9f27f8d 100644 --- a/modules/hivemq-edge-module-opcua/src/main/java/com/hivemq/edge/adapters/opcua/mqtt2opcua/BuiltinJsonSchema.java +++ b/modules/hivemq-edge-module-opcua/src/main/java/com/hivemq/edge/adapters/opcua/mqtt2opcua/BuiltinJsonSchema.java @@ -35,7 +35,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; public class BuiltinJsonSchema { @@ -44,6 +46,9 @@ public class BuiltinJsonSchema { private static final @NotNull String MINIMUM_KEY_WORD = "minimum"; private static final @NotNull String MAXIMUM_KEY_WORD = "maximum"; public static final @NotNull String INTEGER_DATA_TYPE = "integer"; + public static final @NotNull String ARRAY_DATA_TYPE = "array"; + public static final @NotNull String ARRAY_ITEMS = "items"; + public static final @NotNull String ARRAY_MAX_TIMES = "maxItems"; private final @NotNull HashMap classToJsonSchema = new HashMap<>(); @@ -111,6 +116,10 @@ public BuiltinJsonSchema() { return classToJsonSchema.get(builtinDataType); } + public @NotNull JsonNode getJsonSchemaForArray(final @NotNull BuiltinDataType builtinDataType) { + return classToJsonSchema.get(builtinDataType); + } + private @NotNull JsonNode createJsonSchemaForBuiltinType( final @NotNull String title, final @NotNull BuiltinDataType builtinDataType) { final ObjectNode rootNode = OBJECT_MAPPER.createObjectNode(); @@ -130,6 +139,30 @@ public BuiltinJsonSchema() { return rootNode; } + public static void populatePropertiesForArray(final @NotNull ObjectNode propertiesNode, + final @NotNull BuiltinDataType builtinDataType, + final @NotNull ObjectMapper objectMapper, + final @NotNull UInteger[] dimensions) { + final long maxSize = dimensions[0].longValue(); + + propertiesNode.set("type", new TextNode(ARRAY_DATA_TYPE)); + + //0 for a dimension means unlimited + if(maxSize > 0) { + propertiesNode.set("maxItems", new LongNode(maxSize)); + } + final ObjectNode itemsNode = objectMapper.createObjectNode(); + propertiesNode.set("items", itemsNode); + + if (dimensions.length == 1) { //we are the last element + //last element, we can now set the array type + populatePropertiesForBuiltinType(itemsNode, builtinDataType, objectMapper); + } else { + //nesting deeper + populatePropertiesForArray(itemsNode, builtinDataType, objectMapper, Arrays.copyOfRange(dimensions, 1, dimensions.length)); + } + } + public static void populatePropertiesForBuiltinType( final @NotNull ObjectNode nestedPropertiesNode, final @NotNull BuiltinDataType builtinDataType, diff --git a/modules/hivemq-edge-module-opcua/src/main/java/com/hivemq/edge/adapters/opcua/mqtt2opcua/JsonSchemaGenerator.java b/modules/hivemq-edge-module-opcua/src/main/java/com/hivemq/edge/adapters/opcua/mqtt2opcua/JsonSchemaGenerator.java index 90d5a35230..f17ce8d452 100644 --- a/modules/hivemq-edge-module-opcua/src/main/java/com/hivemq/edge/adapters/opcua/mqtt2opcua/JsonSchemaGenerator.java +++ b/modules/hivemq-edge-module-opcua/src/main/java/com/hivemq/edge/adapters/opcua/mqtt2opcua/JsonSchemaGenerator.java @@ -31,6 +31,7 @@ import org.eclipse.milo.opcua.stack.core.serialization.codecs.DataTypeCodec; 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.builtin.unsigned.UInteger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.opcfoundation.opcua.binaryschema.FieldType; @@ -67,27 +68,41 @@ public void createJsonSchema( output.tagNotFound("No node was found for the given node id '" + destinationNodeId + "'"); return; } + final NodeId dataTypeNodeId = uaVariableNode.getDataType(); final DataTypeTree.DataType dataType = tree.getDataType(dataTypeNodeId); + final UInteger[] dimensions = uaVariableNode.getArrayDimensions(); if (dataType == null) { output.fail("Unable to find the data type for the given node id '" + destinationNodeId + "'."); return; } final BuiltinDataType builtinType = tree.getBuiltinType(dataType.getNodeId()); if (builtinType != BuiltinDataType.ExtensionObject) { - output.finish(builtinJsonSchema.getJsonSchema(builtinType)); + if(dimensions != null && dimensions.length > 0) { + final ObjectNode node = objectMapper.createObjectNode(); + BuiltinJsonSchema.populatePropertiesForArray( + node, + builtinType, + objectMapper, + dimensions); + + System.out.println("HMM " + node); + output.finish(node); + } else { + output.finish(builtinJsonSchema.getJsonSchema(builtinType)); + } } else { final NodeId binaryEncodingId = dataType.getBinaryEncodingId(); if (binaryEncodingId == null) { output.fail("No encoding was present for the complex data type: '" + dataType + "'."); } - output.finish(jsonSchemaFromNodeId(binaryEncodingId)); + output.finish(jsonSchemaFromNodeId(binaryEncodingId, dimensions)); } }); } public void addNestedStructureInformation( - final @NotNull ObjectNode propertiesNode, final @NotNull FieldType fieldType) { + final @NotNull ObjectNode propertiesNode, final @NotNull FieldType fieldType, final @NotNull UInteger[] dimensions) { final BuiltinDataType builtinDataType = convertFieldTypeToBuiltInDataType(fieldType, client); final ObjectNode nestedPropertiesNode = objectMapper.createObjectNode(); @@ -95,6 +110,8 @@ public void addNestedStructureInformation( if (builtinDataType != BuiltinDataType.ExtensionObject) { BuiltinJsonSchema.populatePropertiesForBuiltinType(nestedPropertiesNode, builtinDataType, objectMapper); + } else if(dimensions != null && dimensions.length > 0) { + BuiltinJsonSchema.populatePropertiesForArray(nestedPropertiesNode, builtinDataType, objectMapper, dimensions); } else { nestedPropertiesNode.set("type", new TextNode("object")); final ObjectNode innerProperties = objectMapper.createObjectNode(); @@ -128,13 +145,13 @@ public void addNestedStructureInformation( final ArrayNode requiredAttributesArray = objectMapper.createArrayNode(); for (final Map.Entry entry : embeddedFields.entrySet()) { requiredAttributesArray.add(entry.getValue().getName()); - addNestedStructureInformation(innerProperties, entry.getValue()); + addNestedStructureInformation(innerProperties, entry.getValue(), dimensions); } nestedPropertiesNode.set("required", requiredAttributesArray); } } - private @NotNull JsonNode jsonSchemaFromNodeId(final @Nullable NodeId binaryEncodingId) { + private @NotNull JsonNode jsonSchemaFromNodeId(final @Nullable NodeId binaryEncodingId, final @NotNull UInteger[] dimensions) { if (binaryEncodingId == null) { throw new RuntimeException("Binary encoding id was null for nested struct."); } @@ -157,7 +174,7 @@ public void addNestedStructureInformation( for (final Map.Entry entry : fields.entrySet()) { requiredAttributesArray.add(entry.getValue().getName()); final FieldType fieldType = entry.getValue(); - addNestedStructureInformation(propertiesNode, fieldType); + addNestedStructureInformation(propertiesNode, fieldType, dimensions); } valueNode.set("required", requiredAttributesArray); diff --git a/modules/hivemq-edge-module-opcua/src/test/java/com/hivemq/edge/adapters/opcua/mqtt2opcua/JsonToOpcUAConverterTest.java b/modules/hivemq-edge-module-opcua/src/test/java/com/hivemq/edge/adapters/opcua/mqtt2opcua/JsonToOpcUAConverterTest.java index 3d48c5ec3b..122535d785 100644 --- a/modules/hivemq-edge-module-opcua/src/test/java/com/hivemq/edge/adapters/opcua/mqtt2opcua/JsonToOpcUAConverterTest.java +++ b/modules/hivemq-edge-module-opcua/src/test/java/com/hivemq/edge/adapters/opcua/mqtt2opcua/JsonToOpcUAConverterTest.java @@ -1,14 +1,18 @@ package com.hivemq.edge.adapters.opcua.mqtt2opcua; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.DoubleNode; import com.fasterxml.jackson.databind.node.IntNode; import com.fasterxml.jackson.databind.node.LongNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.eclipse.milo.opcua.stack.core.BuiltinDataType; import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UByte; import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -94,5 +98,4 @@ void extractFloat() { assertEquals(12.2, value, 0.0001); } - }