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

feat: implement value-only serialization #297

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
@@ -0,0 +1,79 @@
package org.eclipse.digitaltwin.aas4j.v3.dataformat.json.valueonly;

import java.util.Iterator;
import java.util.List;

import org.eclipse.digitaltwin.aas4j.v3.model.Referable;
import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;

/**
* Base class for mapping collections of submodel elements as for
* <ul>
* <li>{@link SubmodelMapper}</li>
* <li>{@link ElementsCollectionMapper}</li>
* </ul>
* @param <T>
*/
public abstract class AbstractCollectionMapper<T extends Referable> extends AbstractListMapper<T> {
/**
* @param element the submodel element that has to be mapped.
* @param values
* @param idShortPath the idShort path is a dot separated chain of idShorts, that can be used in case of
* troubleshooting.
*/
protected AbstractCollectionMapper(T element, List<SubmodelElement> values, String idShortPath) {
super(element, values, idShortPath);
}

ObjectNode valuesToJson() throws ValueOnlySerializationException {
ObjectNode elementsNode = JsonNodeFactory.instance.objectNode();
for (SubmodelElement submodelElement : values) {
String idShort = submodelElement.getIdShort();
if (elementsNode.has(idShort)) {
throw new ValueOnlySerializationException("Duplicated idShort name '" + idShort +
"' under idShort path '" + idShortPath + "'.", idShortPath);
}
ValueOnlyMapper mapper = ValueOnlyMapper.createMapper(submodelElement, idShortPath + "." + idShort);
if (mapper == null) {
// This type of submodel elements are not serialized in value-only format.
continue;
}
JsonNode mapperNode = mapper.toJson();
if (elementsNode.isObject()) {
elementsNode.setAll((ObjectNode) mapperNode);
} else {
elementsNode.set(idShort, mapperNode);
}
}
return elementsNode;
}

public void updateFromJson(JsonNode value) throws ValueOnlySerializationException {
if (!value.isObject()) {
throw new ValueOnlySerializationException(
"Cannot update the submodel elements collection at idShort path '" + idShortPath +
"', as the corresponding value-only is not a JSON object.", idShortPath);
}
ObjectNode objNode = (ObjectNode) value;
for (Iterator<String> it = objNode.fieldNames(); it.hasNext(); ) {
String idShort = it.next();
SubmodelElement submodelElement = findElementByIdShort(idShort);
ValueOnlyMapper mapper = ValueOnlyMapper.createMapper(submodelElement, idShortPath + "." + idShort);
//mapper.update(objNode.get(idShort));
mapper.update(JsonNodeFactory.instance.objectNode().set(idShort, objNode.get(idShort)));
}
}

private SubmodelElement findElementByIdShort(String idShort) throws ValueOnlySerializationException {
for (SubmodelElement submodelElement : values) {
if (idShort.equals(submodelElement.getIdShort())) {
return submodelElement;
}
}
throw new ValueOnlySerializationException("Cannot find submodel element with idShort '" + idShort +
"' at idShort path '" + idShortPath + "'.", idShortPath);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.eclipse.digitaltwin.aas4j.v3.dataformat.json.valueonly;

import java.util.List;

import org.eclipse.digitaltwin.aas4j.v3.model.Referable;
import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;

/**
* Base class for mapping elements providing list of submodel elements as for
* <ul>
* <li>{@link ElementsListMapper}</li>
* <li>{@link ElementsCollectionMapper}</li>
* </ul>
* @param <T>
*/
public abstract class AbstractListMapper<T extends Referable> extends AbstractMapper<T> {
protected final List<SubmodelElement> values;

/**
* @param element the submodel element that has to be mapped.
* @param idShortPath the idShort path is a dot separated chain of idShorts, that can be used in case of
* troubleshooting.
*/
protected AbstractListMapper(T element, List<SubmodelElement> values, String idShortPath) {
super(element, idShortPath);
this.values = values;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2023 SAP SE or an SAP affiliate company.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.digitaltwin.aas4j.v3.dataformat.json.valueonly;

import java.util.Iterator;

import org.eclipse.digitaltwin.aas4j.v3.model.Referable;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;

/**
* The abstract base class for all value-only mappers.
* @param <T> The type of the mapped elements.
*/
abstract class AbstractMapper<T extends Referable> implements ValueOnlyMapper {
protected final T element;
protected final String idShortPath;

/**
*
* @param element the submodel element that has to be mapped.
* @param idShortPath the idShort path is a dot separated chain of idShorts, that can be used in case of
* troubleshooting.
*/
protected AbstractMapper(T element, String idShortPath) {
this.element = element;
this.idShortPath = idShortPath;
}

JsonNode asValueNode(JsonNode value) {
return JsonNodeFactory.instance.objectNode().set(element.getIdShort(), value);
}

static JsonNode valueFromNode(String msg, String idShortPath, JsonNode valueOnly) {
Iterator<String> fieldNames = valueOnly.fieldNames();
if (!fieldNames.hasNext()) {
// throw exception as value-only nodes must have exactly one field!
throw new ValueOnlySerializationException(
msg + " at idShort path '" + idShortPath +
"', as the passed value does have no fields!", idShortPath);
}
String fieldName = fieldNames.next();
if (fieldNames.hasNext()) {
// throw exception as value-only nodes must have exactly one field!
throw new ValueOnlySerializationException(
msg + " at idShort path '" + idShortPath +
"', as the passed value has more than one field!", idShortPath);
}
return valueOnly.get(fieldName);
}

/**
* Verifies the given object is neither an object nor an array
* @param msg Prefix for exception messages.
* @param idShortPath short path of ID being included to exception messages.
* @param value the node to return as text
* @return given {@code value} as text if it's neither an object nor an array, otherwise throws exception.
* @throws ValueOnlySerializationException throw if node is object or array.
*/
String readValueAsString(String msg, String idShortPath, JsonNode value)
throws ValueOnlySerializationException {
if (value == null || value.isNull()) {
return null;
}
if (value.isObject()) {
throw new ValueOnlySerializationException(
msg + " at idShort path '" + idShortPath +
"', as the passed value is a JSON object.", idShortPath);
}
if (value.isArray()) {
throw new ValueOnlySerializationException(
msg + " at idShort path '" + idShortPath +
"', as the passed value is a JSON array.", idShortPath);
}
return value.asText();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright (C) 2023 SAP SE or an SAP affiliate company.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.digitaltwin.aas4j.v3.dataformat.json.valueonly;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.digitaltwin.aas4j.v3.model.AnnotatedRelationshipElement;
import org.eclipse.digitaltwin.aas4j.v3.model.DataElement;
import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

/**
* AnnotatedRelationshipElement is serialized according to the serialization of a ReleationshipElement. Additionally, a
* third named JSON object is introduced with "annotations" as the name of the containing JSON property. The value is
* ${AnnotatedRelationshipElement/annotations}. The values of the array items are serialized depending on the type of
* the annotation data element.
*/
class AnnotatedRelationshipMapper extends RelationshipElementMapper {
private static final String ANNOTATIONS = "annotations";
AnnotatedRelationshipMapper(AnnotatedRelationshipElement relationship, String idShortPath) {
super(relationship, idShortPath);
}

@Override
public JsonNode toJson() throws ValueOnlySerializationException {
ObjectNode valueNode = (ObjectNode) super.toJson();
AnnotatedRelationshipElement annotatedRelationshipElement = (AnnotatedRelationshipElement) element;
List<SubmodelElement> annotations = new ArrayList<>(annotatedRelationshipElement.getAnnotations());
if(!annotations.isEmpty()) {
ElementsListMapper<AnnotatedRelationshipElement> listMapper = new ElementsListMapper<>(
annotatedRelationshipElement, annotations, idShortPath + "." + ANNOTATIONS);
ObjectNode dataNode = (ObjectNode) valueNode.get(element.getIdShort());
dataNode.set(ANNOTATIONS, listMapper.toJson());
}
return valueNode;
}

@Override
public void update(JsonNode valueOnly) throws ValueOnlySerializationException {
super.update(valueOnly);
AnnotatedRelationshipElement annotatedRelationshipElement = (AnnotatedRelationshipElement) element;
List<DataElement> annotations = annotatedRelationshipElement.getAnnotations();
JsonNode value = valueFromNode("Cannot update the annotated relationship", idShortPath, valueOnly);
JsonNode annotationsNode = value.get(ANNOTATIONS);
if(annotationsNode == null || annotationsNode.isNull()) {
annotations.clear();
} else {
List<SubmodelElement> elements = new ArrayList<>(annotations);
ElementsListMapper<AnnotatedRelationshipElement> listMapper = new ElementsListMapper<>(annotatedRelationshipElement, elements, idShortPath + "." + ANNOTATIONS);
listMapper.update(annotationsNode);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (C) 2023 SAP SE or an SAP affiliate company.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.digitaltwin.aas4j.v3.dataformat.json.valueonly;

import org.eclipse.digitaltwin.aas4j.v3.model.BasicEventElement;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;

/**
* BasicEventElement is serialized as named JSON object with ${BasicEventElement/idShort} as the name of the containing
* JSON property. The JSON object contains one JSON property named “observed” with the corresponding value of
* ${BasicEventElement/observed} as the standard serialization of the Reference class.
*/
class BasicEventElementMapper extends AbstractMapper<BasicEventElement> {
private static final String OBSERVED = "observed";

BasicEventElementMapper(BasicEventElement event, String idShortPath) {
super(event, idShortPath);
}

@Override
public JsonNode toJson() throws ValueOnlySerializationException {
ObjectNode node = JsonNodeFactory.instance.objectNode();
node.set(OBSERVED, new JsonValueOnlyDeserialiser().toJson(element.getObserved()));
return node;
}

@Override
public void update(JsonNode valueOnly) throws ValueOnlySerializationException {
element.setObserved(new JsonValueOnlyDeserialiser().deserialiseReference(valueOnly.get(OBSERVED), idShortPath));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright (C) 2023 SAP SE or an SAP affiliate company.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.digitaltwin.aas4j.v3.dataformat.json.valueonly;

import java.util.Base64;

import org.eclipse.digitaltwin.aas4j.v3.model.Blob;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;

/**
* Blob is serialized as named JSON object with ${Blob/idShort} as the name of the containing JSON property. The JSON
* object contains two JSON properties. The first refers to the content type named "contentType" and value
* ${Blob/contentType}. The latter refers to the value named “value” and value ${Blob/value}. The resulting value-only
* object is indistinguishable whether it contains File or Blob attributes. Therefore, the receiver needs to take the
* type of the target resource into account. Since the receiver knows in advance if a File or a Blob SubmodelElement
* shall be manipulated, it can parse the transferred value-only object accordingly as a File or Blob object.
*/
class BlobMapper extends AbstractMapper<Blob> {
private static final String CONTENT_TYPE = "contentType";
private static final String VALUE = "value";

BlobMapper(Blob blob, String idShortPath) {
super(blob, idShortPath);
}

@Override
public JsonNode toJson() throws ValueOnlySerializationException {
ObjectNode node = JsonNodeFactory.instance.objectNode();
node.set(CONTENT_TYPE, new TextNode(element.getContentType()));
node.set(VALUE, new TextNode(Base64.getEncoder().encodeToString(element.getValue())));
return node;
}

@Override
public void update(JsonNode valueOnly) throws ValueOnlySerializationException {
JsonNode contentNode = valueOnly.get(CONTENT_TYPE);
if(contentNode == null || contentNode.isNull()) {
element.setContentType(null);
} else if(contentNode.isTextual()) {
element.setContentType(contentNode.asText());
} else {
throw new ValueOnlySerializationException(
"Invalid Blob contentType at idShort path '" + idShortPath + "'.", idShortPath);
}

JsonNode valueNode = valueOnly.get(VALUE);
if(valueNode == null || valueNode.isNull()) {
element.setValue(null);
} else if(contentNode != null && contentNode.isTextual()) {
element.setValue(Base64.getDecoder().decode(valueNode.asText()));
} else {
throw new ValueOnlySerializationException(
"Invalid Blob value at idShort path '" + idShortPath + "'.", idShortPath);
}
}
}
Loading