Skip to content

Commit e96699e

Browse files
Merge pull request #598 from bci-oss/feature/547-allow-collectionproperty-mapping-customization
Allow property mapping customizations for collection properties
2 parents 02ca003 + f4076fc commit e96699e

File tree

6 files changed

+178
-45
lines changed

6 files changed

+178
-45
lines changed

core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasGenerator.java

+11
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@
3434
* Generator that generates an AAS file containing an AAS submodel for a given Aspect model.
3535
*/
3636
public class AspectModelAasGenerator {
37+
38+
private List<PropertyMapper<?>> propertyMappers = List.of();
39+
40+
public AspectModelAasGenerator() {
41+
}
42+
43+
public AspectModelAasGenerator( final List<PropertyMapper<?>> propertyMappers ) {
44+
this.propertyMappers = propertyMappers;
45+
}
46+
3747
/**
3848
* Generates an AAS file for a given Aspect.
3949
*
@@ -93,6 +103,7 @@ public void generate( final AasFileFormat format, final Aspect aspect, @Nullable
93103
final Function<String, OutputStream> nameMapper ) {
94104
try ( final OutputStream output = nameMapper.apply( aspect.getName() ) ) {
95105
final AspectModelAasVisitor visitor = new AspectModelAasVisitor().withPropertyMapper( new LangStringPropertyMapper() );
106+
propertyMappers.forEach( visitor::withPropertyMapper );
96107
final Context context;
97108
if ( aspectData != null ) {
98109
final Submodel submodel = new DefaultSubmodel.Builder().build();

core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasVisitor.java

+38-33
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
import org.eclipse.esmf.metamodel.characteristic.StructuredValue;
4949
import org.eclipse.esmf.metamodel.characteristic.Trait;
5050

51-
import com.fasterxml.jackson.databind.JsonNode;
5251
import com.fasterxml.jackson.databind.node.ArrayNode;
5352
import com.google.common.collect.ImmutableMap;
5453
import org.apache.commons.collections4.CollectionUtils;
@@ -135,7 +134,7 @@ ImmutableMap.<Resource, DataTypeIec61360> builder()
135134
.put( RDF.langString, DataTypeIec61360.STRING )
136135
.build();
137136

138-
private interface SubmodelElementBuilder {
137+
interface SubmodelElementBuilder {
139138
SubmodelElement build( Property property );
140139
}
141140

@@ -153,14 +152,18 @@ public AspectModelAasVisitor withPropertyMapper( final PropertyMapper<?> propert
153152

154153
@SuppressWarnings( "unchecked" )
155154
protected <T extends SubmodelElement> PropertyMapper<T> findPropertyMapper( final Property property ) {
156-
return (PropertyMapper<T>) getCustomPropertyMappers().stream()
155+
return this.<T> tryFindPropertyMapper( property ).orElse( (PropertyMapper<T>) DEFAULT_MAPPER );
156+
}
157+
158+
protected <T extends SubmodelElement> Optional<PropertyMapper<T>> tryFindPropertyMapper( final Property property ) {
159+
return getCustomPropertyMappers().stream()
157160
.filter( mapper -> mapper.canHandle( property ) )
158-
.findAny()
159-
.orElse( DEFAULT_MAPPER );
161+
.map( mapper -> (PropertyMapper<T>) mapper )
162+
.findFirst();
160163
}
161164

162165
protected List<PropertyMapper<?>> getCustomPropertyMappers() {
163-
return customPropertyMappers;
166+
return customPropertyMappers.stream().sorted().toList();
164167
}
165168

166169
@Override
@@ -546,10 +549,9 @@ public Environment visitSortedSet( final SortedSet sortedSet, final Context cont
546549
}
547550

548551
private <T extends Collection> Environment visitCollectionProperty( final T collection, final Context context ) {
549-
final SubmodelElementBuilder builder = property -> {
552+
final SubmodelElementBuilder defaultBuilder = property -> {
550553
final DefaultSubmodelElementList.Builder submodelBuilder = new DefaultSubmodelElementList.Builder()
551554
.idShort( property.getName() )
552-
.typeValueListElement( AasSubmodelElements.DATA_ELEMENT )
553555
.displayName( LangStringMapper.NAME.map( property.getPreferredNames() ) )
554556
.description( LangStringMapper.TEXT.map( property.getDescriptions() ) )
555557
.value( List.of( decideOnMapping( property, context ) ) )
@@ -563,28 +565,30 @@ private <T extends Collection> Environment visitCollectionProperty( final T coll
563565
return submodelBuilder.build();
564566
};
565567

566-
final Optional<JsonNode> rawValue = context.getRawPropertyValue();
567-
return rawValue.map( node -> {
568-
if ( node instanceof final ArrayNode arrayNode ) {
569-
final SubmodelElementBuilder listBuilder = property -> {
570-
final List<SubmodelElement> values = getValues( collection, property, context, arrayNode );
571-
return new DefaultSubmodelElementList.Builder()
572-
.idShort( property.getName() )
573-
.displayName( LangStringMapper.NAME.map( property.getPreferredNames() ) )
574-
.description( LangStringMapper.TEXT.map( property.getDescriptions() ) )
575-
.value( values )
576-
.typeValueListElement( AasSubmodelElements.SUBMODEL_ELEMENT )
577-
.build();
578-
};
579-
createSubmodelElement( listBuilder, context );
580-
return context.getEnvironment();
581-
}
582-
createSubmodelElement( builder, context );
583-
return context.getEnvironment();
584-
} ).orElseGet( () -> {
585-
createSubmodelElement( builder, context );
586-
return context.getEnvironment();
587-
} );
568+
final SubmodelElementBuilder listBuilder =
569+
tryFindPropertyMapper( context.getProperty() )
570+
.flatMap( mapper ->
571+
collection.getDataType().map( type ->
572+
(SubmodelElementBuilder) ( Property property ) -> mapper.mapToAasProperty( type, property, context ) ) )
573+
.or( () ->
574+
context.getRawPropertyValue()
575+
.filter( ArrayNode.class::isInstance )
576+
.map( ArrayNode.class::cast )
577+
.map( arrayNode -> ( Property property ) -> {
578+
final List<SubmodelElement> values = getValues( collection, property, context, arrayNode );
579+
return new DefaultSubmodelElementList.Builder()
580+
.idShort( property.getName() )
581+
.displayName( LangStringMapper.NAME.map( property.getPreferredNames() ) )
582+
.description( LangStringMapper.TEXT.map( property.getDescriptions() ) )
583+
.value( values )
584+
.typeValueListElement( AasSubmodelElements.SUBMODEL_ELEMENT )
585+
.build();
586+
} ) )
587+
.orElse( defaultBuilder );
588+
589+
createSubmodelElement( listBuilder, context );
590+
591+
return context.getEnvironment();
588592
}
589593

590594
private <T extends Collection> List<SubmodelElement> getValues( final T collection, final Property property, final Context context,
@@ -593,9 +597,10 @@ private <T extends Collection> List<SubmodelElement> getValues( final T collecti
593597
.map( dataType -> {
594598
if ( Scalar.class.isAssignableFrom( dataType.getClass() ) ) {
595599
return List.of( (SubmodelElement) new DefaultBlob.Builder().value( StreamSupport.stream( arrayNode.spliterator(), false )
596-
.map( JsonNode::asText )
597-
.collect( Collectors.joining( "," ) )
598-
.getBytes( StandardCharsets.UTF_8 ) ).build() );
600+
.map( node -> node.isValueNode() ? node.asText() : node.toString() )
601+
.collect( Collectors.joining( "," ) )
602+
.getBytes( StandardCharsets.UTF_8 ) )
603+
.contentType( "text/plain" ).build() );
599604
} else {
600605
final List<SubmodelElement> values = StreamSupport.stream( arrayNode.spliterator(), false )
601606
.map( node -> {

core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/LangStringPropertyMapper.java

+10-8
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@
1515
import java.util.HashMap;
1616
import java.util.List;
1717
import java.util.Map;
18+
import java.util.stream.Stream;
19+
import java.util.stream.StreamSupport;
1820

1921
import org.eclipse.esmf.metamodel.Property;
2022
import org.eclipse.esmf.metamodel.Type;
2123

2224
import com.fasterxml.jackson.databind.JsonNode;
25+
import com.fasterxml.jackson.databind.node.ObjectNode;
2326
import org.apache.jena.vocabulary.RDF;
2427
import org.eclipse.digitaltwin.aas4j.v3.model.LangStringTextType;
2528
import org.eclipse.digitaltwin.aas4j.v3.model.MultiLanguageProperty;
@@ -49,17 +52,16 @@ public MultiLanguageProperty mapToAasProperty( final Type type, final Property p
4952
}
5053

5154
private List<LangStringTextType> extractLangStrings( final Property property, final Context context ) {
52-
return context.getRawPropertyValue()
55+
return context.getRawPropertyValue().stream()
56+
.flatMap( node -> node.isArray() ? StreamSupport.stream( node.spliterator(), false ) : Stream.of( node ) )
5357
.filter( JsonNode::isObject )
54-
.map( node -> {
58+
.map( ObjectNode.class::cast )
59+
.flatMap( node -> {
5560
final Map<String, String> entries = new HashMap<>();
5661
node.fields().forEachRemaining( field -> entries.put( field.getKey(), field.getValue().asText() ) );
57-
return entries;
62+
return entries.entrySet().stream();
5863
} )
59-
.map( rawEntries -> rawEntries.entrySet()
60-
.stream()
61-
.map( entry -> LangStringMapper.TEXT.createLangString( entry.getValue(), entry.getKey() ) )
62-
.toList() )
63-
.orElseGet( List::of );
64+
.map( entry -> LangStringMapper.TEXT.createLangString( entry.getValue(), entry.getKey() ) )
65+
.toList();
6466
}
6567
}

core/esmf-aspect-model-aas-generator/src/main/java/org/eclipse/esmf/aspectmodel/aas/PropertyMapper.java

+21-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
*
3030
* @param <T> the concrete type of {@link SubmodelElement} the implementing mapper produces
3131
*/
32-
public interface PropertyMapper<T extends SubmodelElement> {
32+
public interface PropertyMapper<T extends SubmodelElement> extends Comparable<PropertyMapper<T>> {
3333
static String UNKNOWN_TYPE = "Unknown";
3434

3535
static String UNKNOWN_EXAMPLE = "";
@@ -54,6 +54,26 @@ default boolean canHandle( final Property property ) {
5454
return true;
5555
}
5656

57+
/**
58+
* Returns the ordering value for this property mapper.
59+
*
60+
* <p>The order is used to determine the correct mapper if multiple matches can occur. By default mappers have
61+
* {@link Integer#MAX_VALUE} applied as their order value, meaning they will be sorted to the very end.
62+
*
63+
* <p>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
64+
* specific mapper should be used for one exact property, that also has this type.
65+
*
66+
* @return the order value
67+
*/
68+
default int getOrder() {
69+
return Integer.MAX_VALUE;
70+
}
71+
72+
@Override
73+
default int compareTo( PropertyMapper<T> otherPropertyMapper ) {
74+
return Integer.compare( getOrder(), otherPropertyMapper.getOrder() );
75+
}
76+
5777
/**
5878
* Builds a concept description reference for the given property.
5979
*

core/esmf-aspect-model-aas-generator/src/test/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasGeneratorTest.java

+47-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.nio.charset.StandardCharsets;
2222
import java.util.List;
2323
import java.util.Set;
24+
2425
import javax.xml.XMLConstants;
2526
import javax.xml.transform.stream.StreamSource;
2627
import javax.xml.validation.Schema;
@@ -38,6 +39,7 @@
3839
import org.eclipse.digitaltwin.aas4j.v3.dataformat.xml.XmlDeserializer;
3940
import org.eclipse.digitaltwin.aas4j.v3.model.AasSubmodelElements;
4041
import org.eclipse.digitaltwin.aas4j.v3.model.AbstractLangString;
42+
import org.eclipse.digitaltwin.aas4j.v3.model.Blob;
4143
import org.eclipse.digitaltwin.aas4j.v3.model.ConceptDescription;
4244
import org.eclipse.digitaltwin.aas4j.v3.model.DataSpecificationContent;
4345
import org.eclipse.digitaltwin.aas4j.v3.model.DataSpecificationIec61360;
@@ -53,6 +55,7 @@
5355
import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementCollection;
5456
import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementList;
5557
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultOperation;
58+
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty;
5659
import org.junit.jupiter.api.Test;
5760
import org.junit.jupiter.params.ParameterizedTest;
5861
import org.junit.jupiter.params.provider.EnumSource;
@@ -79,7 +82,7 @@ void generateAasxWithAspectDataForMultilanguageText() throws DeserializationExce
7982
.asInstanceOf( InstanceOfAssertFactories.LIST )
8083
.hasSize( 2 )
8184
.allSatisfy( langString ->
82-
assertThat( List.of( "en", "de" ) ).contains( ((AbstractLangString) langString).getLanguage() ) ) ) );
85+
assertThat( List.of( "en", "de" ) ).contains( ( (AbstractLangString) langString ).getLanguage() ) ) ) );
8386
}
8487

8588
@Test
@@ -121,6 +124,42 @@ void generateAasxWithAspectDataForNestedEntityLists() throws DeserializationExce
121124
.isEqualTo( "2.25" ) ) ) ) );
122125
}
123126

127+
@Test
128+
void generateAasxWithAspectDataForCollectionProperty() throws DeserializationException {
129+
final Environment env = getAssetAdministrationShellFromAspectWithData( TestAspect.ASPECT_WITH_COLLECTION_OF_SIMPLE_TYPE );
130+
assertThat( env.getSubmodels() )
131+
.singleElement()
132+
.satisfies( subModel -> assertThat( subModel.getSubmodelElements() )
133+
.anySatisfy( sme ->
134+
assertThat( sme ).asInstanceOf( type( SubmodelElementList.class ) )
135+
.extracting( SubmodelElementList::getValue )
136+
.asInstanceOf( InstanceOfAssertFactories.LIST )
137+
.singleElement()
138+
.satisfies( element ->
139+
assertThat( element ).asInstanceOf( type( Blob.class ) )
140+
.extracting( Blob::getValue )
141+
.satisfies( blobData -> assertThat( new String( blobData ) ).isEqualTo( "1,2,3,4,5,6" ) ) ) ) );
142+
}
143+
144+
@Test
145+
void generateAasxWithAspectDataForCollectionPropertyWithCustomMapper() throws DeserializationException {
146+
AspectModelAasGenerator customGenerator = new AspectModelAasGenerator( List.of( new IntegerCollectionMapper() ) );
147+
final Environment env = getAssetAdministrationShellFromAspectWithData( TestAspect.ASPECT_WITH_COLLECTION_OF_SIMPLE_TYPE,
148+
customGenerator );
149+
assertThat( env.getSubmodels() )
150+
.singleElement()
151+
.satisfies( subModel -> assertThat( subModel.getSubmodelElements() )
152+
.anySatisfy( sme ->
153+
assertThat( sme ).asInstanceOf( type( SubmodelElementList.class ) )
154+
.extracting( SubmodelElementList::getValue )
155+
.asInstanceOf( InstanceOfAssertFactories.LIST )
156+
.allSatisfy( element ->
157+
assertThat( element ).asInstanceOf( type( DefaultProperty.class ) )
158+
.extracting( DefaultProperty::getValue )
159+
.satisfies(
160+
intString -> assertThat( Integer.parseInt( intString ) ).isBetween( 1, 6 ) ) ) ) );
161+
}
162+
124163
@Test
125164
void testGenerateAasxFromAspectModelWithListAndAdditionalProperty() throws DeserializationException {
126165
final Environment env = getAssetAdministrationShellFromAspect( TestAspect.ASPECT_WITH_LIST_AND_ADDITIONAL_PROPERTY );
@@ -224,7 +263,7 @@ void testGenerateAasxFromAspectModelWithEitherWithComplexTypes() throws Deserial
224263
final Environment env = getAssetAdministrationShellFromAspect( TestAspect.ASPECT_WITH_EITHER_WITH_COMPLEX_TYPES );
225264
assertThat( env.getSubmodels() ).hasSize( 1 );
226265
assertThat( env.getSubmodels().get( 0 ).getSubmodelElements() ).hasSize( 1 );
227-
final SubmodelElementList elementCollection = ((SubmodelElementList) env.getSubmodels().get( 0 ).getSubmodelElements().get( 0 ));
266+
final SubmodelElementList elementCollection = ( (SubmodelElementList) env.getSubmodels().get( 0 ).getSubmodelElements().get( 0 ) );
228267
final Set<String> testValues = Set.of( "testProperty", "result" );
229268
assertThat( elementCollection.getValue() ).as( "Neither left nor right entity contained." )
230269
.anyMatch( x -> testValues.contains( x.getIdShort() ) );
@@ -247,7 +286,7 @@ void testGenerateAasxFromAspectModelWithQuantifiable() throws DeserializationExc
247286
final DataSpecificationContent dataSpecificationContent = getDataSpecificationIec61360(
248287
"urn:samm:org.eclipse.esmf.test:1.0.0#testProperty", env );
249288

250-
assertThat( ((DataSpecificationIec61360) dataSpecificationContent).getUnit() ).isEqualTo( "percent" );
289+
assertThat( ( (DataSpecificationIec61360) dataSpecificationContent ).getUnit() ).isEqualTo( "percent" );
251290
}
252291

253292
@Test
@@ -406,6 +445,11 @@ private Environment getAssetAdministrationShellFromAspect( final TestAspect test
406445
}
407446

408447
private Environment getAssetAdministrationShellFromAspectWithData( final TestAspect testAspect ) throws DeserializationException {
448+
return getAssetAdministrationShellFromAspectWithData( testAspect, generator );
449+
}
450+
451+
private Environment getAssetAdministrationShellFromAspectWithData( final TestAspect testAspect, final AspectModelAasGenerator generator )
452+
throws DeserializationException {
409453
final Aspect aspect = TestResources.load( testAspect ).aspect();
410454
final Try<JsonNode> payload = TestResources.loadPayload( testAspect );
411455
final JsonNode aspectData = payload.getOrElseThrow( () -> new RuntimeException( payload.getCause() ) );
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.eclipse.esmf.aspectmodel.aas;
2+
3+
import java.util.List;
4+
import java.util.stream.StreamSupport;
5+
6+
import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn;
7+
import org.eclipse.esmf.metamodel.Property;
8+
import org.eclipse.esmf.metamodel.Type;
9+
10+
import com.fasterxml.jackson.databind.JsonNode;
11+
import com.fasterxml.jackson.databind.node.ArrayNode;
12+
import org.eclipse.digitaltwin.aas4j.v3.model.AasSubmodelElements;
13+
import org.eclipse.digitaltwin.aas4j.v3.model.DataTypeDefXsd;
14+
import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
15+
import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementList;
16+
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty;
17+
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementList;
18+
19+
public class IntegerCollectionMapper implements PropertyMapper<SubmodelElementList> {
20+
@Override
21+
public SubmodelElementList mapToAasProperty( final Type type, final Property property, final Context context ) {
22+
final List<? extends SubmodelElement> values = context.getRawPropertyValue()
23+
.stream()
24+
.filter( JsonNode::isArray )
25+
.map( ArrayNode.class::cast )
26+
.flatMap( arrayNode -> StreamSupport.stream( arrayNode.spliterator(), false )
27+
.map( value -> new DefaultProperty.Builder().idShort( "intValue" )
28+
.valueType( DataTypeDefXsd.INT )
29+
.value( value.asText() )
30+
.build() ) )
31+
.toList();
32+
33+
return new DefaultSubmodelElementList.Builder()
34+
.idShort( property.getName() )
35+
.displayName( LangStringMapper.NAME.map( property.getPreferredNames() ) )
36+
.description( LangStringMapper.TEXT.map( property.getDescriptions() ) )
37+
.value( (List<SubmodelElement>) values )
38+
.typeValueListElement( AasSubmodelElements.SUBMODEL_ELEMENT )
39+
.build();
40+
}
41+
42+
@Override
43+
public boolean canHandle( final Property property ) {
44+
return property.urn().equals( AspectModelUrn.fromUrn( "urn:samm:org.eclipse.esmf.test:1.0.0#testList" ) );
45+
}
46+
47+
@Override
48+
public int getOrder() {
49+
return 0;
50+
}
51+
}

0 commit comments

Comments
 (0)