Skip to content

Commit

Permalink
Merge pull request #682 from bci-oss/676-generate-valid-identifiers-f…
Browse files Browse the repository at this point in the history
…rom-aas-idshort

Fix case of SAMM model elements generated from AAS
  • Loading branch information
atextor authored Dec 9, 2024
2 parents a02b5a0 + 27e411d commit 14d5b45
Show file tree
Hide file tree
Showing 23 changed files with 336 additions and 103 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/pull-request-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive

- name: Check code style
if: matrix.os == 'ubuntu-20.04'
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/release-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive

# Required for Maven
- name: Set up JDK 17
Expand Down Expand Up @@ -50,6 +52,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive

# Even though the build itself is done using the GraalVM JDK
# (see below), we use the setup-java action to have GPG configured
Expand Down Expand Up @@ -145,6 +149,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive

- name: Setup JDK
uses: graalvm/setup-graalvm@2f25c0caae5b220866f732832d5e3e29ff493338 # v1.2.1
Expand Down Expand Up @@ -218,6 +224,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive

- name: Setup JDK
uses: graalvm/setup-graalvm@2f25c0caae5b220866f732832d5e3e29ff493338 # v1.2.1
Expand Down Expand Up @@ -362,7 +370,7 @@ jobs:
comment-template: |
Release {release_link} addresses this.
# Sign SAML-CLI Windows executable
# Sign SAMM-CLI Windows executable
- name: Get Artifact ID (Windows)
shell: bash
run: |
Expand Down
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[submodule "core/esmf-aspect-model-aas-generator/src/test/resources/submodel-templates"]
path = core/esmf-aspect-model-aas-generator/src/test/resources/submodel-templates
url = https://github.com/admin-shell-io/submodel-templates.git
branch = main
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ private boolean isRdfList( final Resource resource ) {

/**
* Returns the values of n-ary attributes on a model element (or its super elements), or if a given attribute is an rdf:List, the list
* elements. The list will be ordered by precedence, e.g., if a Property is present on both the current element and it's superelement,
* elements. The list will be ordered by precedence, e.g., if a Property is present on both the current element and its superelement,
* the assertion on the current element will be on a lower list index. Duplicate attribute assertions are removed and only the assertion
* with the highest precedence will be returned (bottom-most in the inheritance tree), this includes multiple assertions for the same
* attribute with rdf:langString values with the same language tag. For example:
Expand Down
5 changes: 5 additions & 0 deletions core/esmf-aspect-model-aas-generator/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.esmf</groupId>
<artifactId>esmf-aspect-model-validator</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH
*
* See the AUTHORS file(s) distributed with this work for additional
* information regarding authorship.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*/

package org.eclipse.esmf.aspectmodel.aas;

import org.eclipse.esmf.aspectmodel.generator.GenerationConfig;

import io.soabase.recordbuilder.core.RecordBuilder;

@RecordBuilder
public record AspectGenerationConfig(
) implements GenerationConfig {
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,48 +18,135 @@
import static org.junit.jupiter.api.Assertions.fail;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.function.Consumer;

import java.util.stream.Stream;

import org.eclipse.esmf.aspectmodel.generator.AspectArtifact;
import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
import org.eclipse.esmf.aspectmodel.serializer.AspectSerializer;
import org.eclipse.esmf.aspectmodel.shacl.violation.Violation;
import org.eclipse.esmf.aspectmodel.validation.services.AspectModelValidator;
import org.eclipse.esmf.aspectmodel.validation.services.ViolationFormatter;
import org.eclipse.esmf.metamodel.Aspect;
import org.eclipse.esmf.metamodel.AspectModel;
import org.eclipse.esmf.metamodel.Operation;
import org.eclipse.esmf.metamodel.Property;
import org.eclipse.esmf.metamodel.Unit;
import org.eclipse.esmf.test.TestAspect;
import org.eclipse.esmf.test.TestResources;

import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.DeserializationException;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonDeserializer;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.xml.XmlDeserializer;
import org.eclipse.digitaltwin.aas4j.v3.model.Environment;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;

class AasToAspectModelGeneratorTest {
@Test
void testTranslateDigitalNameplate() {
final InputStream aasx = AasToAspectModelGeneratorTest.class.getClassLoader()
.getResourceAsStream( "idta/Sample_ZVEI_Digital_Nameplate_V10.aasx" );
final AasToAspectModelGenerator aspectModelGenerator = AasToAspectModelGenerator.fromAasx( aasx );
assertThatCode( aspectModelGenerator::generateAspects ).doesNotThrowAnyException();
@ParameterizedTest
@MethodSource( "idtaSubmodelFiles" )
void testIdtaAasxFilesCanBeTranslated( final File aasxFile ) {
try ( final InputStream input = new FileInputStream( aasxFile ) ) {
final AasToAspectModelGenerator aspectModelGenerator = AasToAspectModelGenerator.fromAasx( input );
final List<Aspect> aspects = aspectModelGenerator.generate().map( AspectArtifact::getContent ).toList();
if ( aspects.isEmpty() ) {
fail( "Translation of " + aasxFile.getName() + " yielded no Aspects" );
}
final String result = AspectSerializer.INSTANCE.aspectToString( aspects.iterator().next() );
final AspectModel aspectModel = new AspectModelLoader().load( new ByteArrayInputStream( result.getBytes() ) );

aspectModel.elements().forEach( element -> {
if ( element instanceof Property || element instanceof Operation || element instanceof Unit ) {
assertThat( element.getName().charAt( 0 ) )
.describedAs( element.getName() + " is a " + element.getClass().getSimpleName() + " and must be lower case" )
.isLowerCase();
} else {
assertThat( element.getName().charAt( 0 ) )
.describedAs( element.getName() + " is a " + element.getClass().getSimpleName() + " and must be upper case" )
.isUpperCase();
}
} );

final List<Violation> violations = new AspectModelValidator().validateModel( aspectModel );
if ( !violations.isEmpty() ) {
final String report = new ViolationFormatter().apply( violations );
System.out.println( report );
System.out.println( "====" );
System.out.println( result );
fail();
}
} catch ( final IOException exception ) {
fail( exception );
} catch ( final AspectModelGenerationException aspectModelGenerationException ) {
if ( aspectModelGenerationException.getCause() instanceof final DeserializationException cause ) {
System.err.println( "Could not load AASX file: " + aasxFile.getName() + ". Consider reporting to IDTA or AAS4J project." );
} else {
fail( aspectModelGenerationException );
}
}
}

private static final List<String> IGNORED_AASX_FILES = List.of(
"IDTA02026-1-0_Template_ProvisionOf3DModels.aasx",
"IDTA 02004-1-2_Template_Handover Documentation.aasx",
"Sample_ZVEI_Digital_Nameplate_V10.aasx",
"SMT_Pure_Technical_Data_V11.aasx",
"sample-zvei-techdata-V11.aasx",
"SMT_qualified_Technical_Data_V11.aasx",
"IDTA 02010-1-0_Template_ServiceRequestNotification.aasx",
"IDTA 02011-1-0_Template_HierarchicalStructuresEnablingBoM.aasx",
"IDTA 02011-1-1_Template_HSEBoM.aasx"
);

protected static Stream<Arguments> idtaSubmodelFiles() throws URISyntaxException, IOException {
final String submodelTemplatesMissing =
"IDTA AASX files not found. Please make sure they are available; in the project root run: git submodule update --init "
+ "--recursive";
final URL resource = AasToAspectModelGeneratorTest.class.getResource( "/submodel-templates" );
try ( final Stream<Path> stream = Files.walk( Paths.get( resource.toURI() ) ) ) {
final List<Arguments> list = stream.filter( Files::isRegularFile )
.filter( file -> file.getFileName().toString().endsWith( ".aasx" ) )
.map( Path::toFile )
.filter( file -> !IGNORED_AASX_FILES.contains( file.getName() ) )
.map( file -> Arguments.of( Named.of( file.getName(), file ) ) )
.toList();
if ( list.isEmpty() ) {
fail( submodelTemplatesMissing );
}
return list.stream();
} catch ( final NullPointerException exception ) {
fail( submodelTemplatesMissing );
return Stream.empty();
}
}

@Test
void testSeeReferences() {
final InputStream inputStream = AasToAspectModelGeneratorTest.class.getClassLoader().getResourceAsStream(
"idta/IDTA 02022-1-0_Template_Wireless Communication.aasx" );
"submodel-templates/published/Wireless Communication/1/0/IDTA 02022-1-0_Template_Wireless Communication.aasx" );
final AasToAspectModelGenerator aspectModelGenerator = AasToAspectModelGenerator.fromAasx( inputStream );
final List<Aspect> aspects = aspectModelGenerator.generateAspects();
final List<Aspect> aspects = aspectModelGenerator.generate().map( AspectArtifact::getContent ).toList();

assertThatCode( aspectModelGenerator::generateAspects ).doesNotThrowAnyException();
assertThatCode( aspectModelGenerator::generate ).doesNotThrowAnyException();

aspects.stream()
.flatMap( aspect -> aspect.getProperties().stream() )
.flatMap( property -> property.getSee().stream() )
.forEach( see -> {
assertThat( see ).doesNotContain( "/ " );
} );
.forEach( see -> assertThat( see ).doesNotContain( "/ " ) );
}

@ParameterizedTest
Expand All @@ -68,14 +155,16 @@ void testRoundtripConversion( final TestAspect testAspect ) throws Deserializati
final Aspect aspect = TestResources.load( testAspect ).aspect();
final Consumer<AasToAspectModelGenerator> assertForValidator = aspectModelGenerator ->
assertThatCode( () -> {
final List<Aspect> aspects = aspectModelGenerator.generateAspects();
final List<Aspect> aspects = aspectModelGenerator.generate().map( AspectArtifact::getContent ).toList();
assertThat( aspects ).singleElement().satisfies( generatedAspect ->
assertThat( generatedAspect.urn() ).isEqualTo( aspect.urn() ) );
} ).doesNotThrowAnyException();

final byte[] content = new AspectModelAasGenerator( aspect,
AasGenerationConfigBuilder.builder().format( AasFileFormat.XML ).build() ).getContent();
assertThat( new String( content ) ).doesNotContain( "Optional[" );
final Environment aasEnvironmentFromXml = new XmlDeserializer().read(
new ByteArrayInputStream( new AspectModelAasGenerator( aspect,
AasGenerationConfigBuilder.builder().format( AasFileFormat.XML ).build() ).getContent() ) );
new ByteArrayInputStream( content ) );
assertForValidator.accept( AasToAspectModelGenerator.fromEnvironment( aasEnvironmentFromXml ) );

final Environment aasEnvironmentFromJson = new JsonDeserializer().read(
Expand All @@ -90,7 +179,7 @@ void testGetAspectModelUrnFromSubmodelIdentifier() {
// Submodel has an Aspect Model URN as identifier
final Environment aasEnvironment = loadEnvironment( "SMTWithAspectModelUrnId.aas.xml" );
final AasToAspectModelGenerator aspectModelGenerator = AasToAspectModelGenerator.fromEnvironment( aasEnvironment );
assertThat( aspectModelGenerator.generateAspects() ).singleElement().satisfies( aspect ->
assertThat( aspectModelGenerator.generate() ).map( AspectArtifact::getContent ).singleElement().satisfies( aspect ->
assertThat( aspect.urn().toString() ).isEqualTo( "urn:samm:com.example:1.0.0#Submodel1" ) );
}

Expand All @@ -99,7 +188,7 @@ void testGetAspectModelUrnFromConceptDescription() {
// Submodel has a Concept Description that points to an Aspect Model URN
final Environment aasEnvironment = loadEnvironment( "SMTWithAspectModelUrnInConceptDescription.aas.xml" );
final AasToAspectModelGenerator aspectModelGenerator = AasToAspectModelGenerator.fromEnvironment( aasEnvironment );
assertThat( aspectModelGenerator.generateAspects() ).singleElement().satisfies( aspect ->
assertThat( aspectModelGenerator.generate() ).map( AspectArtifact::getContent ).singleElement().satisfies( aspect ->
assertThat( aspect.urn().toString() ).isEqualTo( "urn:samm:com.example:1.0.0#Submodel1" ) );
}

Expand All @@ -109,7 +198,7 @@ void testConstructAspectModelUrn1() {
// It has a version and an IRI identifier and an idShort
final Environment aasEnvironment = loadEnvironment( "SMTAspectModelUrnInConstruction1.aas.xml" );
final AasToAspectModelGenerator aspectModelGenerator = AasToAspectModelGenerator.fromEnvironment( aasEnvironment );
assertThat( aspectModelGenerator.generateAspects() ).singleElement().satisfies( aspect ->
assertThat( aspectModelGenerator.generate() ).map( AspectArtifact::getContent ).singleElement().satisfies( aspect ->
assertThat( aspect.urn().toString() ).isEqualTo( "urn:samm:com.example.www:1.2.3#Submodel1" ) );
}

Expand All @@ -119,7 +208,7 @@ void testConstructAspectModelUrn2() {
// It has a version and an IRDI identifier and an idShort
final Environment aasEnvironment = loadEnvironment( "SMTAspectModelUrnInConstruction2.aas.xml" );
final AasToAspectModelGenerator aspectModelGenerator = AasToAspectModelGenerator.fromEnvironment( aasEnvironment );
assertThat( aspectModelGenerator.generateAspects() ).singleElement().satisfies( aspect ->
assertThat( aspectModelGenerator.generate() ).map( AspectArtifact::getContent ).singleElement().satisfies( aspect ->
assertThat( aspect.urn().toString() ).isEqualTo( "urn:samm:com.example:1.2.3#Submodel1" ) );
}

Expand All @@ -129,7 +218,7 @@ void testConstructAspectModelUrn3() {
// It has an IRDI identifier and an idShort, but no version
final Environment aasEnvironment = loadEnvironment( "SMTAspectModelUrnInConstruction3.aas.xml" );
final AasToAspectModelGenerator aspectModelGenerator = AasToAspectModelGenerator.fromEnvironment( aasEnvironment );
assertThat( aspectModelGenerator.generateAspects() ).singleElement().satisfies( aspect ->
assertThat( aspectModelGenerator.generate() ).map( AspectArtifact::getContent ).singleElement().satisfies( aspect ->
assertThat( aspect.urn().toString() ).isEqualTo( "urn:samm:com.example:1.0.0#Submodel1" ) );
}

Expand All @@ -139,7 +228,7 @@ void testConstructAspectModelUrn4() {
// It has an IRDI identifier, but no idShort and no version
final Environment aasEnvironment = loadEnvironment( "SMTAspectModelUrnInConstruction4.aas.xml" );
final AasToAspectModelGenerator aspectModelGenerator = AasToAspectModelGenerator.fromEnvironment( aasEnvironment );
assertThat( aspectModelGenerator.generateAspects() ).singleElement().satisfies( aspect ->
assertThat( aspectModelGenerator.generate() ).map( AspectArtifact::getContent ).singleElement().satisfies( aspect ->
assertThat( aspect.urn().toString() ).isEqualTo( "urn:samm:com.example:1.0.0#AAAAAA000abf2fd07" ) );
}

Expand All @@ -151,15 +240,4 @@ private Environment loadEnvironment( final String name ) {
}
return null;
}

private InputStream getIdtaModel( final String path ) {
try {
final URL url = new URL(
"https://github.com/admin-shell-io/submodel-templates/raw/refs/heads/main/published/" + path.replaceAll( " ", "%20" ) );
return url.openStream();
} catch ( final Exception e ) {
e.printStackTrace();
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<aas:keys>
<aas:key>
<aas:type>GlobalReference</aas:type>
<aas:value>Optional[urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEntity]</aas:value>
<aas:value>urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEntity</aas:value>
</aas:key>
</aas:keys>
</aas:dataSpecification>
Expand Down Expand Up @@ -62,7 +62,7 @@
</aas:langStringTextType>
</aas:description>
<aas:administration/>
<aas:id>Optional[urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEntity]/submodel</aas:id>
<aas:id>urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEntity/submodel</aas:id>
<aas:kind>Template</aas:kind>
<aas:semanticId>
<aas:type>ModelReference</aas:type>
Expand Down Expand Up @@ -144,7 +144,7 @@
<aas:keys>
<aas:key>
<aas:type>GlobalReference</aas:type>
<aas:value>Optional[urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEntity]</aas:value>
<aas:value>urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEntity</aas:value>
</aas:key>
</aas:keys>
</aas:dataSpecification>
Expand Down Expand Up @@ -189,7 +189,7 @@
<aas:keys>
<aas:key>
<aas:type>GlobalReference</aas:type>
<aas:value>Optional[urn:samm:org.eclipse.esmf.test:1.0.0#entityProperty]</aas:value>
<aas:value>urn:samm:org.eclipse.esmf.test:1.0.0#entityProperty</aas:value>
</aas:key>
</aas:keys>
</aas:dataSpecification>
Expand Down Expand Up @@ -237,7 +237,7 @@
<aas:keys>
<aas:key>
<aas:type>GlobalReference</aas:type>
<aas:value>Optional[urn:samm:org.eclipse.esmf.test:1.0.0#testProperty]</aas:value>
<aas:value>urn:samm:org.eclipse.esmf.test:1.0.0#testProperty</aas:value>
</aas:key>
</aas:keys>
</aas:dataSpecification>
Expand Down
Binary file not shown.
Binary file not shown.
Submodule submodel-templates added at fa0b19
Loading

0 comments on commit 14d5b45

Please sign in to comment.