From 4a2a24eb15cc431ca5eb06c9e9eb5afa02555d08 Mon Sep 17 00:00:00 2001 From: Andreas Schilling Date: Mon, 24 Jun 2024 22:34:52 +0200 Subject: [PATCH 1/2] Allow property mapping customizations for collection properties --- .../aas/AspectModelAasGenerator.java | 12 ++++ .../aas/AspectModelAasVisitor.java | 70 ++++++++++--------- .../aas/LangStringPropertyMapper.java | 18 ++--- .../esmf/aspectmodel/aas/PropertyMapper.java | 22 +++++- .../aas/AspectModelAasGeneratorTest.java | 44 ++++++++++++ .../aas/IntegerCollectionMapper.java | 53 ++++++++++++++ 6 files changed, 177 insertions(+), 42 deletions(-) create mode 100644 core/esmf-aspect-model-aas-generator/src/test/java/org/eclipse/esmf/aspectmodel/aas/IntegerCollectionMapper.java diff --git a/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasGenerator.java b/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasGenerator.java index 92c7dd628..cfce455ee 100644 --- a/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasGenerator.java +++ b/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasGenerator.java @@ -16,6 +16,7 @@ import java.io.OutputStream; import java.util.List; import java.util.function.Function; + import javax.annotation.Nullable; import org.eclipse.esmf.functions.ThrowingBiConsumer; @@ -34,6 +35,16 @@ * Generator that generates an AAS file containing an AAS submodel for a given Aspect model. */ public class AspectModelAasGenerator { + + private List> propertyMappers = List.of(); + + public AspectModelAasGenerator() { + } + + public AspectModelAasGenerator( final List> propertyMappers ) { + this.propertyMappers = propertyMappers; + } + /** * Generates an AAS file for a given Aspect. * @@ -93,6 +104,7 @@ public void generate( final AasFileFormat format, final Aspect aspect, @Nullable final Function nameMapper ) { try ( final OutputStream output = nameMapper.apply( aspect.getName() ) ) { final AspectModelAasVisitor visitor = new AspectModelAasVisitor().withPropertyMapper( new LangStringPropertyMapper() ); + propertyMappers.forEach( visitor::withPropertyMapper ); final Context context; if ( aspectData != null ) { final Submodel submodel = new DefaultSubmodel.Builder().build(); diff --git a/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasVisitor.java b/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasVisitor.java index 24dc24065..ac8db3ebd 100644 --- a/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasVisitor.java +++ b/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasVisitor.java @@ -52,7 +52,6 @@ import org.eclipse.esmf.metamodel.visitor.AspectVisitor; import org.eclipse.esmf.samm.KnownVersion; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.google.common.collect.ImmutableMap; import org.apache.commons.collections4.CollectionUtils; @@ -139,7 +138,7 @@ ImmutableMap. builder() .put( RDF.langString, DataTypeIec61360.STRING ) .build(); - private interface SubmodelElementBuilder { + interface SubmodelElementBuilder { SubmodelElement build( Property property ); } @@ -157,14 +156,18 @@ public AspectModelAasVisitor withPropertyMapper( final PropertyMapper propert @SuppressWarnings( "unchecked" ) protected PropertyMapper findPropertyMapper( final Property property ) { - return (PropertyMapper) getCustomPropertyMappers().stream() + return this. tryFindPropertyMapper( property ).orElse( (PropertyMapper) DEFAULT_MAPPER ); + } + + protected Optional> tryFindPropertyMapper( final Property property ) { + return getCustomPropertyMappers().stream() .filter( mapper -> mapper.canHandle( property ) ) - .findAny() - .orElse( DEFAULT_MAPPER ); + .map( mapper -> (PropertyMapper) mapper ) + .findFirst(); } protected List> getCustomPropertyMappers() { - return customPropertyMappers; + return customPropertyMappers.stream().sorted().toList(); } @Override @@ -551,10 +554,9 @@ public Environment visitSortedSet( final SortedSet sortedSet, final Context cont } private Environment visitCollectionProperty( final T collection, final Context context ) { - final SubmodelElementBuilder builder = property -> { + final SubmodelElementBuilder defaultBuilder = property -> { final DefaultSubmodelElementList.Builder submodelBuilder = new DefaultSubmodelElementList.Builder() .idShort( property.getName() ) - .typeValueListElement( AasSubmodelElements.DATA_ELEMENT ) .displayName( LangStringMapper.NAME.map( property.getPreferredNames() ) ) .description( LangStringMapper.TEXT.map( property.getDescriptions() ) ) .value( List.of( decideOnMapping( property, context ) ) ) @@ -568,28 +570,29 @@ private Environment visitCollectionProperty( final T coll return submodelBuilder.build(); }; - final Optional rawValue = context.getRawPropertyValue(); - return rawValue.map( node -> { - if ( node instanceof final ArrayNode arrayNode ) { - final SubmodelElementBuilder listBuilder = property -> { - final List values = getValues( collection, property, context, arrayNode ); - return new DefaultSubmodelElementList.Builder() - .idShort( property.getName() ) - .displayName( LangStringMapper.NAME.map( property.getPreferredNames() ) ) - .description( LangStringMapper.TEXT.map( property.getDescriptions() ) ) - .value( values ) - .typeValueListElement( AasSubmodelElements.SUBMODEL_ELEMENT ) - .build(); - }; - createSubmodelElement( listBuilder, context ); - return context.getEnvironment(); - } - createSubmodelElement( builder, context ); - return context.getEnvironment(); - } ).orElseGet( () -> { - createSubmodelElement( builder, context ); - return context.getEnvironment(); - } ); + final SubmodelElementBuilder listBuilder = + tryFindPropertyMapper( context.getProperty() ) + .flatMap( mapper -> collection.getDataType() + .map( type -> (SubmodelElementBuilder) ( Property property ) -> mapper.mapToAasProperty( type, property, + context ) ) ) + .or( () -> context.getRawPropertyValue() + .filter( ArrayNode.class::isInstance ) + .map( ArrayNode.class::cast ) + .map( arrayNode -> ( Property property ) -> { + final List values = getValues( collection, property, context, arrayNode ); + return new DefaultSubmodelElementList.Builder() + .idShort( property.getName() ) + .displayName( LangStringMapper.NAME.map( property.getPreferredNames() ) ) + .description( LangStringMapper.TEXT.map( property.getDescriptions() ) ) + .value( values ) + .typeValueListElement( AasSubmodelElements.SUBMODEL_ELEMENT ) + .build(); + } ) ) + .orElse( defaultBuilder ); + + createSubmodelElement( listBuilder, context ); + + return context.getEnvironment(); } private List getValues( final T collection, final Property property, final Context context, @@ -598,9 +601,10 @@ private List getValues( final T collecti .map( dataType -> { if ( Scalar.class.isAssignableFrom( dataType.getClass() ) ) { return List.of( (SubmodelElement) new DefaultBlob.Builder().value( StreamSupport.stream( arrayNode.spliterator(), false ) - .map( JsonNode::asText ) - .collect( Collectors.joining( "," ) ) - .getBytes( StandardCharsets.UTF_8 ) ).build() ); + .map( node -> node.isValueNode() ? node.asText() : node.toString() ) + .collect( Collectors.joining( "," ) ) + .getBytes( StandardCharsets.UTF_8 ) ) + .contentType( "text/plain" ).build() ); } else { final List values = StreamSupport.stream( arrayNode.spliterator(), false ) .map( node -> { diff --git a/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/LangStringPropertyMapper.java b/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/LangStringPropertyMapper.java index d60552e31..5c8663017 100644 --- a/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/LangStringPropertyMapper.java +++ b/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/LangStringPropertyMapper.java @@ -15,11 +15,14 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import org.eclipse.esmf.metamodel.Property; import org.eclipse.esmf.metamodel.Type; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.jena.vocabulary.RDF; import org.eclipse.digitaltwin.aas4j.v3.model.LangStringTextType; import org.eclipse.digitaltwin.aas4j.v3.model.MultiLanguageProperty; @@ -49,17 +52,16 @@ public MultiLanguageProperty mapToAasProperty( final Type type, final Property p } private List extractLangStrings( final Property property, final Context context ) { - return context.getRawPropertyValue() + return context.getRawPropertyValue().stream() + .flatMap( node -> node.isArray() ? StreamSupport.stream( node.spliterator(), false ) : Stream.of( node ) ) .filter( JsonNode::isObject ) - .map( node -> { + .map( ObjectNode.class::cast ) + .flatMap( node -> { final Map entries = new HashMap<>(); node.fields().forEachRemaining( field -> entries.put( field.getKey(), field.getValue().asText() ) ); - return entries; + return entries.entrySet().stream(); } ) - .map( rawEntries -> rawEntries.entrySet() - .stream() - .map( entry -> LangStringMapper.TEXT.createLangString( entry.getValue(), entry.getKey() ) ) - .toList() ) - .orElseGet( List::of ); + .map( entry -> LangStringMapper.TEXT.createLangString( entry.getValue(), entry.getKey() ) ) + .toList(); } } diff --git a/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/PropertyMapper.java b/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/PropertyMapper.java index ecf206927..5a68648fa 100644 --- a/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/PropertyMapper.java +++ b/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/PropertyMapper.java @@ -30,7 +30,7 @@ * * @param the concrete type of {@link SubmodelElement} the implementing mapper produces */ -public interface PropertyMapper { +public interface PropertyMapper extends Comparable> { static String UNKNOWN_TYPE = "Unknown"; static String UNKNOWN_EXAMPLE = ""; @@ -55,6 +55,26 @@ default boolean canHandle( final Property property ) { return true; } + /** + * Returns the ordering value for this property mapper. + * + * The order is used to determine the correct mapper if multiple matches can occur. By default mappers have {@link Integer#MAX_VALUE} + * applied as their order value, meaning they will be sorted to the very end. + * + * One example for the need of a proper ordering is, if a general mapper for a specific property type is used, but an even more specific + * mapper should be used for one exact property, that also has this type. + * + * @return the order value + */ + default int getOrder() { + return Integer.MAX_VALUE; + } + + @Override + default int compareTo( PropertyMapper otherPropertyMapper ) { + return Integer.compare( getOrder(), otherPropertyMapper.getOrder() ); + } + /** * Builds a concept description reference for the given property. * diff --git a/core/esmf-aspect-model-aas-generator/src/test/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasGeneratorTest.java b/core/esmf-aspect-model-aas-generator/src/test/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasGeneratorTest.java index 9307ff3de..a4de045b0 100644 --- a/core/esmf-aspect-model-aas-generator/src/test/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasGeneratorTest.java +++ b/core/esmf-aspect-model-aas-generator/src/test/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasGeneratorTest.java @@ -21,6 +21,7 @@ import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Set; + import javax.xml.XMLConstants; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; @@ -40,6 +41,7 @@ import org.eclipse.digitaltwin.aas4j.v3.dataformat.xml.XmlDeserializer; import org.eclipse.digitaltwin.aas4j.v3.model.AasSubmodelElements; import org.eclipse.digitaltwin.aas4j.v3.model.AbstractLangString; +import org.eclipse.digitaltwin.aas4j.v3.model.Blob; import org.eclipse.digitaltwin.aas4j.v3.model.ConceptDescription; import org.eclipse.digitaltwin.aas4j.v3.model.DataSpecificationContent; import org.eclipse.digitaltwin.aas4j.v3.model.DataSpecificationIec61360; @@ -55,6 +57,7 @@ import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementCollection; import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementList; import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultOperation; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -123,6 +126,42 @@ void generateAasxWithAspectDataForNestedEntityLists() throws DeserializationExce .isEqualTo( "2.25" ) ) ) ) ); } + @Test + void generateAasxWithAspectDataForCollectionProperty() throws DeserializationException { + final Environment env = getAssetAdministrationShellFromAspectWithData( TestAspect.ASPECT_WITH_COLLECTION_OF_SIMPLE_TYPE ); + assertThat( env.getSubmodels() ) + .singleElement() + .satisfies( subModel -> assertThat( subModel.getSubmodelElements() ) + .anySatisfy( sme -> + assertThat( sme ).asInstanceOf( type( SubmodelElementList.class ) ) + .extracting( SubmodelElementList::getValue ) + .asInstanceOf( InstanceOfAssertFactories.LIST ) + .singleElement() + .satisfies( element -> + assertThat( element ).asInstanceOf( type( Blob.class ) ) + .extracting( Blob::getValue ) + .satisfies( blobData -> assertThat( new String( blobData ) ).isEqualTo( "1,2,3,4,5,6" ) ) ) ) ); + } + + @Test + void generateAasxWithAspectDataForCollectionPropertyWithCustomMapper() throws DeserializationException { + AspectModelAasGenerator customGenerator = new AspectModelAasGenerator( List.of( new IntegerCollectionMapper() ) ); + final Environment env = getAssetAdministrationShellFromAspectWithData( TestAspect.ASPECT_WITH_COLLECTION_OF_SIMPLE_TYPE, + customGenerator ); + assertThat( env.getSubmodels() ) + .singleElement() + .satisfies( subModel -> assertThat( subModel.getSubmodelElements() ) + .anySatisfy( sme -> + assertThat( sme ).asInstanceOf( type( SubmodelElementList.class ) ) + .extracting( SubmodelElementList::getValue ) + .asInstanceOf( InstanceOfAssertFactories.LIST ) + .allSatisfy( element -> + assertThat( element ).asInstanceOf( type( DefaultProperty.class ) ) + .extracting( DefaultProperty::getValue ) + .satisfies( + intString -> assertThat( Integer.parseInt( intString ) ).isBetween( 1, 6 ) ) ) ) ); + } + @Test void testGenerateAasxFromAspectModelWithListAndAdditionalProperty() throws DeserializationException { final Environment env = getAssetAdministrationShellFromAspect( TestAspect.ASPECT_WITH_LIST_AND_ADDITIONAL_PROPERTY ); @@ -408,6 +447,11 @@ private Environment getAssetAdministrationShellFromAspect( final TestAspect test } private Environment getAssetAdministrationShellFromAspectWithData( final TestAspect testAspect ) throws DeserializationException { + return getAssetAdministrationShellFromAspectWithData( testAspect, generator ); + } + + private Environment getAssetAdministrationShellFromAspectWithData( final TestAspect testAspect, final AspectModelAasGenerator generator ) + throws DeserializationException { final Aspect aspect = loadAspect( testAspect ); final JsonNode aspectData = loadPayload( testAspect ); return loadAasx( generator.generateAsByteArray( AasFileFormat.XML, aspect, aspectData ) ); diff --git a/core/esmf-aspect-model-aas-generator/src/test/java/org/eclipse/esmf/aspectmodel/aas/IntegerCollectionMapper.java b/core/esmf-aspect-model-aas-generator/src/test/java/org/eclipse/esmf/aspectmodel/aas/IntegerCollectionMapper.java new file mode 100644 index 000000000..ef2d1fda7 --- /dev/null +++ b/core/esmf-aspect-model-aas-generator/src/test/java/org/eclipse/esmf/aspectmodel/aas/IntegerCollectionMapper.java @@ -0,0 +1,53 @@ +package org.eclipse.esmf.aspectmodel.aas; + +import java.util.List; +import java.util.stream.StreamSupport; + +import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn; +import org.eclipse.esmf.metamodel.Property; +import org.eclipse.esmf.metamodel.Type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.eclipse.digitaltwin.aas4j.v3.model.AasSubmodelElements; +import org.eclipse.digitaltwin.aas4j.v3.model.DataTypeDefXsd; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementList; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementList; + +public class IntegerCollectionMapper implements PropertyMapper { + @Override + public SubmodelElementList mapToAasProperty( final Type type, final Property property, final Context context ) { + final List values = context.getRawPropertyValue() + .stream() + .filter( JsonNode::isArray ) + .map( ArrayNode.class::cast ) + .flatMap( arrayNode -> StreamSupport.stream( arrayNode.spliterator(), false ) + .map( value -> new DefaultProperty.Builder().idShort( "intValue" ) + .valueType( DataTypeDefXsd.INT ) + .value( value.asText() ) + .build() ) ) + .toList(); + + return new DefaultSubmodelElementList.Builder() + .idShort( property.getName() ) + .displayName( LangStringMapper.NAME.map( property.getPreferredNames() ) ) + .description( LangStringMapper.TEXT.map( property.getDescriptions() ) ) + .value( (List) values ) + .typeValueListElement( AasSubmodelElements.SUBMODEL_ELEMENT ) + .build(); + } + + @Override + public boolean canHandle( final Property property ) { + return property.getAspectModelUrn() + .map( urn -> urn.equals( AspectModelUrn.fromUrn( "urn:samm:org.eclipse.esmf.test:1.0.0#testList" ) ) ) + .orElse( false ); + } + + @Override + public int getOrder() { + return 0; + } +} From 19aabacfed0faa281bc922f3756f55ead692e312 Mon Sep 17 00:00:00 2001 From: Andreas Schilling Date: Tue, 25 Jun 2024 08:39:42 +0200 Subject: [PATCH 2/2] Fix style issues --- .../esmf/aspectmodel/aas/AspectModelAasGenerator.java | 1 - .../org/eclipse/esmf/aspectmodel/aas/PropertyMapper.java | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasGenerator.java b/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasGenerator.java index cfce455ee..2735208b7 100644 --- a/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasGenerator.java +++ b/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasGenerator.java @@ -16,7 +16,6 @@ import java.io.OutputStream; import java.util.List; import java.util.function.Function; - import javax.annotation.Nullable; import org.eclipse.esmf.functions.ThrowingBiConsumer; diff --git a/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/PropertyMapper.java b/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/PropertyMapper.java index 5a68648fa..325cee8f4 100644 --- a/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/PropertyMapper.java +++ b/core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/PropertyMapper.java @@ -58,11 +58,11 @@ default boolean canHandle( final Property property ) { /** * Returns the ordering value for this property mapper. * - * The order is used to determine the correct mapper if multiple matches can occur. By default mappers have {@link Integer#MAX_VALUE} - * applied as their order value, meaning they will be sorted to the very end. + *

The order is used to determine the correct mapper if multiple matches can occur. By default mappers have + * {@link Integer#MAX_VALUE} applied as their order value, meaning they will be sorted to the very end. * - * One example for the need of a proper ordering is, if a general mapper for a specific property type is used, but an even more specific - * mapper should be used for one exact property, that also has this type. + *

One example for the need of a proper ordering is, if a general mapper for a specific property type is used, but an even more + * specific mapper should be used for one exact property, that also has this type. * * @return the order value */