Skip to content

Commit 03f37cd

Browse files
authored
Merge pull request #702 from hivemq/fix/opcua-arrays
Added support for arrays
2 parents c2e747b + e548dc4 commit 03f37cd

File tree

5 files changed

+161
-164
lines changed

5 files changed

+161
-164
lines changed

modules/hivemq-edge-module-opcua/src/main/java/com/hivemq/edge/adapters/opcua/mqtt2opcua/BuiltinJsonSchema.java

+53
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@
3535
import org.slf4j.Logger;
3636
import org.slf4j.LoggerFactory;
3737

38+
import java.util.Arrays;
3839
import java.util.HashMap;
40+
import java.util.List;
41+
import java.util.Objects;
3942

4043
public class BuiltinJsonSchema {
4144

@@ -44,6 +47,9 @@ public class BuiltinJsonSchema {
4447
private static final @NotNull String MINIMUM_KEY_WORD = "minimum";
4548
private static final @NotNull String MAXIMUM_KEY_WORD = "maximum";
4649
public static final @NotNull String INTEGER_DATA_TYPE = "integer";
50+
public static final @NotNull String ARRAY_DATA_TYPE = "array";
51+
public static final @NotNull String ARRAY_ITEMS = "items";
52+
public static final @NotNull String ARRAY_MAX_TIMES = "maxItems";
4753

4854
private final @NotNull HashMap<BuiltinDataType, JsonNode> classToJsonSchema = new HashMap<>();
4955

@@ -130,6 +136,53 @@ public BuiltinJsonSchema() {
130136
return rootNode;
131137
}
132138

139+
public @NotNull JsonNode getJsonSchema(final @NotNull BuiltinDataType builtinDataType,
140+
final @NotNull UInteger[] dimensions) {
141+
142+
final ObjectNode rootNode = OBJECT_MAPPER.createObjectNode();
143+
final ObjectNode propertiesNode = OBJECT_MAPPER.createObjectNode();
144+
final ObjectNode valueNode = OBJECT_MAPPER.createObjectNode();
145+
rootNode.set("$schema", new TextNode("https://json-schema.org/draft/2019-09/schema"));
146+
rootNode.set("title", new TextNode("Array of " + builtinDataType.name() + " JsonSchema"));
147+
rootNode.set("type", new TextNode("object"));
148+
rootNode.set("properties", propertiesNode);
149+
propertiesNode.set("value", valueNode);
150+
populatePropertiesForArray(valueNode, builtinDataType, OBJECT_MAPPER, dimensions);
151+
152+
final ArrayNode requiredAttributes = OBJECT_MAPPER.createArrayNode();
153+
requiredAttributes.add("value");
154+
rootNode.set("required", requiredAttributes);
155+
return rootNode;
156+
}
157+
158+
public static void populatePropertiesForArray(final @NotNull ObjectNode propertiesNode,
159+
final @NotNull BuiltinDataType builtinDataType,
160+
final @NotNull ObjectMapper objectMapper,
161+
final @NotNull UInteger[] dimensions) {
162+
if(dimensions.length == 0) {
163+
throw new IllegalArgumentException("Array of " + builtinDataType.name() + " dimensions must not be empty");
164+
}
165+
final long maxSize = dimensions[0].longValue();
166+
167+
propertiesNode.set("type", new TextNode(ARRAY_DATA_TYPE));
168+
169+
//0 for a dimension means unlimited
170+
if(maxSize > 0) {
171+
propertiesNode.set("maxItems", new LongNode(maxSize));
172+
propertiesNode.set("minItems", new LongNode(maxSize));
173+
}
174+
final ObjectNode itemsNode = objectMapper.createObjectNode();
175+
propertiesNode.set("items", itemsNode);
176+
177+
if (dimensions.length == 1) {
178+
//last element, we can now set the array type
179+
populatePropertiesForBuiltinType(itemsNode, builtinDataType, objectMapper);
180+
} else {
181+
//nesting deeper
182+
populatePropertiesForArray(itemsNode, builtinDataType, objectMapper, Arrays.copyOfRange(dimensions, 1, dimensions.length));
183+
}
184+
}
185+
133186
public static void populatePropertiesForBuiltinType(
134187
final @NotNull ObjectNode nestedPropertiesNode,
135188
final @NotNull BuiltinDataType builtinDataType,

modules/hivemq-edge-module-opcua/src/main/java/com/hivemq/edge/adapters/opcua/mqtt2opcua/JsonSchemaGenerator.java

+15-6
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.eclipse.milo.opcua.stack.core.serialization.codecs.DataTypeCodec;
3232
import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
3333
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
34+
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
3435
import org.jetbrains.annotations.NotNull;
3536
import org.jetbrains.annotations.Nullable;
3637
import org.opcfoundation.opcua.binaryschema.FieldType;
@@ -67,34 +68,42 @@ public void createJsonSchema(
6768
output.tagNotFound("No node was found for the given node id '" + destinationNodeId + "'");
6869
return;
6970
}
71+
7072
final NodeId dataTypeNodeId = uaVariableNode.getDataType();
7173
final DataTypeTree.DataType dataType = tree.getDataType(dataTypeNodeId);
74+
final UInteger[] dimensions = uaVariableNode.getArrayDimensions();
7275
if (dataType == null) {
7376
output.fail("Unable to find the data type for the given node id '" + destinationNodeId + "'.");
7477
return;
7578
}
7679
final BuiltinDataType builtinType = tree.getBuiltinType(dataType.getNodeId());
7780
if (builtinType != BuiltinDataType.ExtensionObject) {
78-
output.finish(builtinJsonSchema.getJsonSchema(builtinType));
81+
if(dimensions != null && dimensions.length > 0) {
82+
output.finish(builtinJsonSchema.getJsonSchema(builtinType, dimensions));
83+
} else {
84+
output.finish(builtinJsonSchema.getJsonSchema(builtinType));
85+
}
7986
} else {
8087
final NodeId binaryEncodingId = dataType.getBinaryEncodingId();
8188
if (binaryEncodingId == null) {
8289
output.fail("No encoding was present for the complex data type: '" + dataType + "'.");
8390
}
84-
output.finish(jsonSchemaFromNodeId(binaryEncodingId));
91+
output.finish(jsonSchemaFromNodeId(binaryEncodingId, dimensions));
8592
}
8693
});
8794
}
8895

8996
public void addNestedStructureInformation(
90-
final @NotNull ObjectNode propertiesNode, final @NotNull FieldType fieldType) {
97+
final @NotNull ObjectNode propertiesNode, final @NotNull FieldType fieldType, final @NotNull UInteger[] dimensions) {
9198
final BuiltinDataType builtinDataType = convertFieldTypeToBuiltInDataType(fieldType, client);
9299

93100
final ObjectNode nestedPropertiesNode = objectMapper.createObjectNode();
94101
propertiesNode.set(fieldType.getName(), nestedPropertiesNode);
95102

96103
if (builtinDataType != BuiltinDataType.ExtensionObject) {
97104
BuiltinJsonSchema.populatePropertiesForBuiltinType(nestedPropertiesNode, builtinDataType, objectMapper);
105+
} else if(dimensions != null && dimensions.length > 0) {
106+
BuiltinJsonSchema.populatePropertiesForArray(nestedPropertiesNode, builtinDataType, objectMapper, dimensions);
98107
} else {
99108
nestedPropertiesNode.set("type", new TextNode("object"));
100109
final ObjectNode innerProperties = objectMapper.createObjectNode();
@@ -128,13 +137,13 @@ public void addNestedStructureInformation(
128137
final ArrayNode requiredAttributesArray = objectMapper.createArrayNode();
129138
for (final Map.Entry<String, FieldType> entry : embeddedFields.entrySet()) {
130139
requiredAttributesArray.add(entry.getValue().getName());
131-
addNestedStructureInformation(innerProperties, entry.getValue());
140+
addNestedStructureInformation(innerProperties, entry.getValue(), dimensions);
132141
}
133142
nestedPropertiesNode.set("required", requiredAttributesArray);
134143
}
135144
}
136145

137-
private @NotNull JsonNode jsonSchemaFromNodeId(final @Nullable NodeId binaryEncodingId) {
146+
private @NotNull JsonNode jsonSchemaFromNodeId(final @Nullable NodeId binaryEncodingId, final @NotNull UInteger[] dimensions) {
138147
if (binaryEncodingId == null) {
139148
throw new RuntimeException("Binary encoding id was null for nested struct.");
140149
}
@@ -157,7 +166,7 @@ public void addNestedStructureInformation(
157166
for (final Map.Entry<String, FieldType> entry : fields.entrySet()) {
158167
requiredAttributesArray.add(entry.getValue().getName());
159168
final FieldType fieldType = entry.getValue();
160-
addNestedStructureInformation(propertiesNode, fieldType);
169+
addNestedStructureInformation(propertiesNode, fieldType, dimensions);
161170
}
162171
valueNode.set("required", requiredAttributesArray);
163172

modules/hivemq-edge-module-opcua/src/main/java/com/hivemq/edge/adapters/opcua/mqtt2opcua/JsonToOpcUAConverter.java

+24-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.hivemq.edge.adapters.opcua.mqtt2opcua;
1717

1818
import com.fasterxml.jackson.databind.JsonNode;
19+
import com.fasterxml.jackson.databind.node.ArrayNode;
1920
import com.google.common.io.BaseEncoding;
2021
import org.apache.commons.lang3.NotImplementedException;
2122
import org.eclipse.milo.opcua.binaryschema.AbstractCodec;
@@ -46,9 +47,13 @@
4647
import org.slf4j.Logger;
4748
import org.slf4j.LoggerFactory;
4849

50+
import java.lang.reflect.Array;
4951
import java.lang.reflect.Field;
5052
import java.time.Instant;
53+
import java.util.ArrayList;
54+
import java.util.Arrays;
5155
import java.util.Date;
56+
import java.util.List;
5257
import java.util.Map;
5358
import java.util.Optional;
5459
import java.util.UUID;
@@ -110,7 +115,11 @@ public JsonToOpcUAConverter(final @NotNull OpcUaClient client) throws UaExceptio
110115
rootNode);
111116

112117
if (builtinType != BuiltinDataType.ExtensionObject) {
113-
return parsetoOpcUAObject(builtinType, rootNode);
118+
if(rootNode.isArray()) {
119+
return generateArrayFromArrayNode((ArrayNode) rootNode, builtinType);
120+
} else {
121+
return parsetoOpcUAObject(builtinType, rootNode);
122+
}
114123
}
115124

116125
final NodeId binaryEncodingId = dataType.getBinaryEncodingId();
@@ -575,4 +584,18 @@ static boolean extractBoolean(final JsonNode jsonNode) {
575584
intendedClass +
576585
"due to underflow.");
577586
}
587+
588+
private Object[] generateArrayFromArrayNode(final @NotNull ArrayNode arrayNode, final @NotNull BuiltinDataType type) {
589+
Object[] ret = (Object[])Array.newInstance(type.getBackingClass(), arrayNode.size());
590+
591+
for (int i = 0; i < arrayNode.size(); i++) {
592+
JsonNode arrayEntry = arrayNode.get(i);
593+
if (arrayEntry.isArray()) {
594+
ret[i] = generateArrayFromArrayNode((ArrayNode) arrayEntry, type);
595+
} else {
596+
ret[i] = parsetoOpcUAObject(type, arrayEntry);
597+
}
598+
}
599+
return ret;
600+
}
578601
}

0 commit comments

Comments
 (0)