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..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 @@ -34,6 +34,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 +103,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 77f4b51e3..caa7d2498 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 @@ -48,7 +48,6 @@ import org.eclipse.esmf.metamodel.characteristic.StructuredValue; import org.eclipse.esmf.metamodel.characteristic.Trait; -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; @@ -135,7 +134,7 @@ ImmutableMap. builder() .put( RDF.langString, DataTypeIec61360.STRING ) .build(); - private interface SubmodelElementBuilder { + interface SubmodelElementBuilder { SubmodelElement build( Property property ); } @@ -153,14 +152,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 @@ -546,10 +549,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 ) ) ) @@ -563,28 +565,30 @@ 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, @@ -593,9 +597,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 1dc71a2d2..9d61aa6a1 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 @@ -29,7 +29,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 = ""; @@ -54,6 +54,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 de59b9fda..22d017811 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; @@ -38,6 +39,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; @@ -53,6 +55,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; @@ -79,7 +82,7 @@ void generateAasxWithAspectDataForMultilanguageText() throws DeserializationExce .asInstanceOf( InstanceOfAssertFactories.LIST ) .hasSize( 2 ) .allSatisfy( langString -> - assertThat( List.of( "en", "de" ) ).contains( ((AbstractLangString) langString).getLanguage() ) ) ) ); + assertThat( List.of( "en", "de" ) ).contains( ( (AbstractLangString) langString ).getLanguage() ) ) ) ); } @Test @@ -121,6 +124,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 ); @@ -224,7 +263,7 @@ void testGenerateAasxFromAspectModelWithEitherWithComplexTypes() throws Deserial final Environment env = getAssetAdministrationShellFromAspect( TestAspect.ASPECT_WITH_EITHER_WITH_COMPLEX_TYPES ); assertThat( env.getSubmodels() ).hasSize( 1 ); assertThat( env.getSubmodels().get( 0 ).getSubmodelElements() ).hasSize( 1 ); - final SubmodelElementList elementCollection = ((SubmodelElementList) env.getSubmodels().get( 0 ).getSubmodelElements().get( 0 )); + final SubmodelElementList elementCollection = ( (SubmodelElementList) env.getSubmodels().get( 0 ).getSubmodelElements().get( 0 ) ); final Set testValues = Set.of( "testProperty", "result" ); assertThat( elementCollection.getValue() ).as( "Neither left nor right entity contained." ) .anyMatch( x -> testValues.contains( x.getIdShort() ) ); @@ -247,7 +286,7 @@ void testGenerateAasxFromAspectModelWithQuantifiable() throws DeserializationExc final DataSpecificationContent dataSpecificationContent = getDataSpecificationIec61360( "urn:samm:org.eclipse.esmf.test:1.0.0#testProperty", env ); - assertThat( ((DataSpecificationIec61360) dataSpecificationContent).getUnit() ).isEqualTo( "percent" ); + assertThat( ( (DataSpecificationIec61360) dataSpecificationContent ).getUnit() ).isEqualTo( "percent" ); } @Test @@ -406,6 +445,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 = TestResources.load( testAspect ).aspect(); final Try payload = TestResources.loadPayload( testAspect ); final JsonNode aspectData = payload.getOrElseThrow( () -> new RuntimeException( payload.getCause() ) ); 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..5ec0509d0 --- /dev/null +++ b/core/esmf-aspect-model-aas-generator/src/test/java/org/eclipse/esmf/aspectmodel/aas/IntegerCollectionMapper.java @@ -0,0 +1,51 @@ +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.urn().equals( AspectModelUrn.fromUrn( "urn:samm:org.eclipse.esmf.test:1.0.0#testList" ) ); + } + + @Override + public int getOrder() { + return 0; + } +}