Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for arrays #702

Merged
merged 7 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;

public class BuiltinJsonSchema {

Expand All @@ -44,6 +47,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<BuiltinDataType, JsonNode> classToJsonSchema = new HashMap<>();

Expand Down Expand Up @@ -130,6 +136,53 @@ public BuiltinJsonSchema() {
return rootNode;
}

public @NotNull JsonNode getJsonSchema(final @NotNull BuiltinDataType builtinDataType,
final @NotNull UInteger[] dimensions) {

final ObjectNode rootNode = OBJECT_MAPPER.createObjectNode();
final ObjectNode propertiesNode = OBJECT_MAPPER.createObjectNode();
final ObjectNode valueNode = OBJECT_MAPPER.createObjectNode();
rootNode.set("$schema", new TextNode("https://json-schema.org/draft/2019-09/schema"));
rootNode.set("title", new TextNode("Array of " + builtinDataType.name() + " JsonSchema"));
rootNode.set("type", new TextNode("object"));
rootNode.set("properties", propertiesNode);
propertiesNode.set("value", valueNode);
populatePropertiesForArray(valueNode, builtinDataType, OBJECT_MAPPER, dimensions);

final ArrayNode requiredAttributes = OBJECT_MAPPER.createArrayNode();
requiredAttributes.add("value");
rootNode.set("required", requiredAttributes);
return rootNode;
}

public static void populatePropertiesForArray(final @NotNull ObjectNode propertiesNode,
final @NotNull BuiltinDataType builtinDataType,
final @NotNull ObjectMapper objectMapper,
final @NotNull UInteger[] dimensions) {
if(dimensions.length == 0) {
throw new IllegalArgumentException("Array of " + builtinDataType.name() + " dimensions must not be empty");
}
final long maxSize = dimensions[0].longValue();
codepitbull marked this conversation as resolved.
Show resolved Hide resolved

propertiesNode.set("type", new TextNode(ARRAY_DATA_TYPE));

//0 for a dimension means unlimited
if(maxSize > 0) {
propertiesNode.set("maxItems", new LongNode(maxSize));
propertiesNode.set("minItems", new LongNode(maxSize));
}
final ObjectNode itemsNode = objectMapper.createObjectNode();
propertiesNode.set("items", itemsNode);

if (dimensions.length == 1) {
//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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -67,34 +68,42 @@ 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) {
output.finish(builtinJsonSchema.getJsonSchema(builtinType, dimensions));
} 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();
propertiesNode.set(fieldType.getName(), nestedPropertiesNode);

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();
Expand Down Expand Up @@ -128,13 +137,13 @@ public void addNestedStructureInformation(
final ArrayNode requiredAttributesArray = objectMapper.createArrayNode();
for (final Map.Entry<String, FieldType> 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.");
}
Expand All @@ -157,7 +166,7 @@ public void addNestedStructureInformation(
for (final Map.Entry<String, FieldType> entry : fields.entrySet()) {
requiredAttributesArray.add(entry.getValue().getName());
final FieldType fieldType = entry.getValue();
addNestedStructureInformation(propertiesNode, fieldType);
addNestedStructureInformation(propertiesNode, fieldType, dimensions);
}
valueNode.set("required", requiredAttributesArray);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package com.hivemq.edge.adapters.opcua.mqtt2opcua;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.google.common.io.BaseEncoding;
import org.apache.commons.lang3.NotImplementedException;
import org.eclipse.milo.opcua.binaryschema.AbstractCodec;
Expand Down Expand Up @@ -46,9 +47,13 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
Expand Down Expand Up @@ -110,7 +115,11 @@ public JsonToOpcUAConverter(final @NotNull OpcUaClient client) throws UaExceptio
rootNode);

if (builtinType != BuiltinDataType.ExtensionObject) {
return parsetoOpcUAObject(builtinType, rootNode);
if(rootNode.isArray()) {
return generateArrayFromArrayNode((ArrayNode) rootNode, builtinType);
} else {
return parsetoOpcUAObject(builtinType, rootNode);
}
}

final NodeId binaryEncodingId = dataType.getBinaryEncodingId();
Expand Down Expand Up @@ -575,4 +584,18 @@ static boolean extractBoolean(final JsonNode jsonNode) {
intendedClass +
"due to underflow.");
}

private Object[] generateArrayFromArrayNode(final @NotNull ArrayNode arrayNode, final @NotNull BuiltinDataType type) {
Object[] ret = (Object[])Array.newInstance(type.getBackingClass(), arrayNode.size());

for (int i = 0; i < arrayNode.size(); i++) {
JsonNode arrayEntry = arrayNode.get(i);
if (arrayEntry.isArray()) {
ret[i] = generateArrayFromArrayNode((ArrayNode) arrayEntry, type);
} else {
ret[i] = parsetoOpcUAObject(type, arrayEntry);
}
}
return ret;
}
}
Loading
Loading