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

Allow property mapping customizations for collection properties #598

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 @@ -34,6 +34,16 @@
* Generator that generates an AAS file containing an AAS submodel for a given Aspect model.
*/
public class AspectModelAasGenerator {

private List<PropertyMapper<?>> propertyMappers = List.of();

public AspectModelAasGenerator() {
}

public AspectModelAasGenerator( final List<PropertyMapper<?>> propertyMappers ) {
this.propertyMappers = propertyMappers;
}

/**
* Generates an AAS file for a given Aspect.
*
Expand Down Expand Up @@ -93,6 +103,7 @@ public void generate( final AasFileFormat format, final Aspect aspect, @Nullable
final Function<String, OutputStream> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -135,7 +134,7 @@ ImmutableMap.<Resource, DataTypeIec61360> builder()
.put( RDF.langString, DataTypeIec61360.STRING )
.build();

private interface SubmodelElementBuilder {
interface SubmodelElementBuilder {
SubmodelElement build( Property property );
}

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

@SuppressWarnings( "unchecked" )
protected <T extends SubmodelElement> PropertyMapper<T> findPropertyMapper( final Property property ) {
return (PropertyMapper<T>) getCustomPropertyMappers().stream()
return this.<T> tryFindPropertyMapper( property ).orElse( (PropertyMapper<T>) DEFAULT_MAPPER );
}

protected <T extends SubmodelElement> Optional<PropertyMapper<T>> tryFindPropertyMapper( final Property property ) {
return getCustomPropertyMappers().stream()
.filter( mapper -> mapper.canHandle( property ) )
.findAny()
.orElse( DEFAULT_MAPPER );
.map( mapper -> (PropertyMapper<T>) mapper )
.findFirst();
}

protected List<PropertyMapper<?>> getCustomPropertyMappers() {
return customPropertyMappers;
return customPropertyMappers.stream().sorted().toList();
}

@Override
Expand Down Expand Up @@ -546,10 +549,9 @@ public Environment visitSortedSet( final SortedSet sortedSet, final Context cont
}

private <T extends Collection> 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 ) ) )
Expand All @@ -563,28 +565,30 @@ private <T extends Collection> Environment visitCollectionProperty( final T coll
return submodelBuilder.build();
};

final Optional<JsonNode> rawValue = context.getRawPropertyValue();
return rawValue.map( node -> {
if ( node instanceof final ArrayNode arrayNode ) {
final SubmodelElementBuilder listBuilder = property -> {
final List<SubmodelElement> 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<SubmodelElement> 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 <T extends Collection> List<SubmodelElement> getValues( final T collection, final Property property, final Context context,
Expand All @@ -593,9 +597,10 @@ private <T extends Collection> List<SubmodelElement> 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<SubmodelElement> values = StreamSupport.stream( arrayNode.spliterator(), false )
.map( node -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -49,17 +52,16 @@ public MultiLanguageProperty mapToAasProperty( final Type type, final Property p
}

private List<LangStringTextType> 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<String, String> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
*
* @param <T> the concrete type of {@link SubmodelElement} the implementing mapper produces
*/
public interface PropertyMapper<T extends SubmodelElement> {
public interface PropertyMapper<T extends SubmodelElement> extends Comparable<PropertyMapper<T>> {
static String UNKNOWN_TYPE = "Unknown";

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

/**
* Returns the ordering value for this property mapper.
*
* <p>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.
*
* <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
* 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<T> otherPropertyMapper ) {
return Integer.compare( getOrder(), otherPropertyMapper.getOrder() );
}

/**
* Builds a concept description reference for the given property.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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<String> testValues = Set.of( "testProperty", "result" );
assertThat( elementCollection.getValue() ).as( "Neither left nor right entity contained." )
.anyMatch( x -> testValues.contains( x.getIdShort() ) );
Expand All @@ -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
Expand Down Expand Up @@ -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<JsonNode> payload = TestResources.loadPayload( testAspect );
final JsonNode aspectData = payload.getOrElseThrow( () -> new RuntimeException( payload.getCause() ) );
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SubmodelElementList> {
@Override
public SubmodelElementList mapToAasProperty( final Type type, final Property property, final Context context ) {
final List<? extends SubmodelElement> 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<SubmodelElement>) 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;
}
}
Loading