diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/AspectModelLoader.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/AspectModelLoader.java index 827966d15..52d9e5391 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/AspectModelLoader.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/AspectModelLoader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH + * Copyright (c) 2025 Robert Bosch Manufacturing Solutions GmbH * * See the AUTHORS file(s) distributed with this work for additional * information regarding authorship. @@ -17,6 +17,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.net.JarURLConnection; import java.net.URI; import java.nio.file.Path; import java.util.ArrayDeque; @@ -121,7 +122,7 @@ public AspectModelLoader( final ResolutionStrategy resolutionStrategy ) { */ public AspectModelLoader( final List resolutionStrategies ) { TurtleLoader.init(); - if ( resolutionStrategies.size() == 1 ) { + if ( 1 == resolutionStrategies.size() ) { resolutionStrategy = resolutionStrategies.get( 0 ); } else if ( resolutionStrategies.isEmpty() ) { resolutionStrategy = DEFAULT_STRATEGY.get(); @@ -416,7 +417,7 @@ public AspectModel loadNamespacePackage( final byte[] binaryContent, final URI l * {@code https://example.com/package.zip}, the files in the package will have a location URI such as * {@code jar:file:/some/path/package.zip!/com.example.namespace/1.0.0/AspectModel.ttl} or * {@code jar:https://example.com/package.zip!/com.example.namespace/1.0.0/AspectModel.ttl}, respectively, as described in - * the JavaDoc for {@link java.net.JarURLConnection}. + * the JavaDoc for {@link JarURLConnection}. * * @param location the source location * @param inputStream the input stream to load the ZIP content from @@ -473,7 +474,8 @@ private String replaceLegacyBammUrn( final String urn ) { private boolean containsType( final Model model, final String urn ) { if ( model.contains( model.createResource( urn ), RDF.type, (RDFNode) null ) ) { return true; - } else if ( urn.startsWith( AspectModelUrn.PROTOCOL_AND_NAMESPACE_PREFIX ) ) { + } + if ( urn.startsWith( AspectModelUrn.PROTOCOL_AND_NAMESPACE_PREFIX ) ) { // when deriving a URN from file (via "fileToUrn" method - mainly in samm-cli scenarios), // we assume new "samm" format, but could actually have been the old "bamm" return model.contains( model.createResource( toLegacyBammUrn( urn ) ), RDF.type, (RDFNode) null ); @@ -495,7 +497,7 @@ private Optional applyResolutionStrategy( final String urn ) { try { final AspectModelUrn aspectModelUrn = AspectModelUrn.fromUrn( replaceLegacyBammUrn( urn ) ); - if ( aspectModelUrn.getElementType() != ElementType.NONE ) { + if ( ElementType.NONE != aspectModelUrn.getElementType() ) { return Optional.empty(); } final AspectModelFile resolutionResult = resolutionStrategy.apply( aspectModelUrn, this ); @@ -586,14 +588,18 @@ private void resolve( final List inputFiles, final LoaderContex @Override public boolean containsDefinition( final AspectModelFile aspectModelFile, final AspectModelUrn urn ) { final Model model = aspectModelFile.sourceModel(); + boolean result = model.contains( model.createResource( urn.toString() ), RDF.type, (RDFNode) null ); + if ( result ) { + LOG.debug( "Checking if model contains {}: {}", urn, result ); + return result; + } if ( model.getNsPrefixMap().values().stream().anyMatch( prefixUri -> prefixUri.startsWith( "urn:bamm:" ) ) ) { - final boolean result = model.contains( + result = model.contains( model.createResource( urn.toString().replace( AspectModelUrn.PROTOCOL_AND_NAMESPACE_PREFIX, "urn:bamm:" ) ), RDF.type, (RDFNode) null ); LOG.debug( "Checking if model contains {}: {}", urn, result ); return result; } - final boolean result = model.contains( model.createResource( urn.toString() ), RDF.type, (RDFNode) null ); LOG.debug( "Checking if model contains {}: {}", urn, result ); return result; } @@ -674,7 +680,7 @@ public AspectModel loadAspectModelFiles( final Collection input .findFirst() .ifPresent( aspect -> mergedModel.setNsPrefix( "", aspect.urn().getUrnPrefix() ) ); for ( final AspectModelFile file : files ) { - if ( file.aspects().size() > 1 ) { + if ( 1 < file.aspects().size() ) { throw new AspectLoadingException( "Aspect Model file " + file.humanReadableLocation() + " contains " + file.aspects().size() + " aspects, but may only contain one." ); @@ -709,7 +715,7 @@ private void setNamespaces( final Collection files, final Colle MetaModelBaseAttributes namespaceDefinition = null; AspectModelFile fileContainingNamespaceDefinition = null; final List elementsForUrn = elementsGroupedByNamespaceUrn.get( namespaceUrn ); - if ( elementsForUrn != null ) { + if ( null != elementsForUrn ) { for ( final ModelElement element : elementsForUrn ) { final AspectModelFile elementFile = element.getSourceFile(); if ( elementFile.sourceModel().contains( null, RDF.type, SammNs.SAMM.Namespace() ) ) { diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/FileSystemStrategy.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/FileSystemStrategy.java index 72e9fa1df..9453424ca 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/FileSystemStrategy.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/FileSystemStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH + * Copyright (c) 2025 Robert Bosch Manufacturing Solutions GmbH * * See the AUTHORS file(s) distributed with this work for additional * information regarding authorship. @@ -33,14 +33,12 @@ import io.vavr.control.Try; import org.apache.jena.rdf.model.Model; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.jena.riot.RiotException; /** * Resolution strategy for Aspect model URNs that finds Aspect model files in the local file system. */ public class FileSystemStrategy implements ResolutionStrategy { - private static final Logger LOG = LoggerFactory.getLogger( FileSystemStrategy.class ); protected final ModelsRoot modelsRoot; /** @@ -79,14 +77,14 @@ public FileSystemStrategy( final ModelsRoot modelsRoot ) { * * @param aspectModelUrn The model URN * @return The model on success, {@link IllegalArgumentException} if the model file can not be read, - * {@link org.apache.jena.riot.RiotException} on parser error, {@link MalformedURLException} if the AspectModelUrn is invalid, + * {@link RiotException} on parser error, {@link MalformedURLException} if the AspectModelUrn is invalid, * {@link FileNotFoundException} if no file containing the element was found */ @Override public AspectModelFile apply( final AspectModelUrn aspectModelUrn, final ResolutionStrategySupport resolutionStrategySupport ) { final List checkedLocations = new ArrayList<>(); - final File namedResourceFile = modelsRoot.determineAspectModelFile( aspectModelUrn ); + final File namedResourceFile = modelsRoot.resolveAspectModelFile( aspectModelUrn ); if ( namedResourceFile.exists() ) { final Try tryFile = Try.of( () -> AspectModelFileLoader.load( namedResourceFile ) ); if ( tryFile.isFailure() ) { @@ -94,7 +92,13 @@ public AspectModelFile apply( final AspectModelUrn aspectModelUrn, final Resolut new ModelResolutionException.LoadingFailure( aspectModelUrn, namedResourceFile.getAbsolutePath(), tryFile.getCause().getMessage(), tryFile.getCause() ) ); } - return tryFile.get(); + final RawAspectModelFile loadedFile = tryFile.get(); + if ( resolutionStrategySupport.containsDefinition( loadedFile, aspectModelUrn ) ) { + return loadedFile; + } else { + checkedLocations.add( new ModelResolutionException.LoadingFailure( aspectModelUrn, namedResourceFile.getAbsolutePath(), + "File does not contain the element definition" ) ); + } } else { checkedLocations.add( new ModelResolutionException.LoadingFailure( aspectModelUrn, namedResourceFile.getAbsolutePath(), "File does not exist" ) ); diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/fs/ModelsRoot.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/fs/ModelsRoot.java index 05154c295..b117758a3 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/fs/ModelsRoot.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/fs/ModelsRoot.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH + * Copyright (c) 2025 Robert Bosch Manufacturing Solutions GmbH * * See the AUTHORS file(s) distributed with this work for additional * information regarding authorship. @@ -14,14 +14,23 @@ package org.eclipse.esmf.aspectmodel.resolver.fs; import java.io.File; +import java.io.IOException; import java.net.URI; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Objects; import java.util.stream.Stream; import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + public abstract class ModelsRoot { + + private static final Logger LOG = LoggerFactory.getLogger( ModelsRoot.class ); + private static final File EMPTY_FILE = new File( "" ); private final Path root; protected ModelsRoot( final Path root ) { @@ -43,6 +52,38 @@ public Stream paths() { public abstract Path directoryForNamespace( final AspectModelUrn urn ); public File determineAspectModelFile( final AspectModelUrn urn ) { - return directoryForNamespace( urn ).resolve( urn.getName() + ".ttl" ).toFile(); + return constructAspectModelFilePath( urn ).toFile(); + } + + /** + * Resolve the aspect model file for the given {@link AspectModelUrn}. + * + *

Constructs the file path by resolving the namespace directory. + * Validates the file using its canonical path. + * + *

Returns an empty file if the resolution fails.s + * + * @param urn the {@link AspectModelUrn} representing the aspect model. + * @return the resolved {@link File}, or an empty file if resolution fails. + */ + public File resolveAspectModelFile( final AspectModelUrn urn ) { + Path path = constructAspectModelFilePath( urn ); + return resolveByCanonicalPath( path ); + } + + private Path constructAspectModelFilePath( final AspectModelUrn urn ) { + return directoryForNamespace( urn ).resolve( urn.getName() + ".ttl" ); + } + + private static File resolveByCanonicalPath( final Path path ) { + File file = path.toFile(); + try { + if ( file.exists() && Objects.equals( path.normalize().toString(), file.getCanonicalPath() ) ) { + return file; + } + } catch ( IOException exception ) { + LOG.error( "Error resolving canonical path for file: {}", file.getPath(), exception ); + } + return EMPTY_FILE; } } diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/resolver/fs/ModelsRootTest.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/resolver/fs/ModelsRootTest.java new file mode 100644 index 000000000..b6c5187bf --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/resolver/fs/ModelsRootTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025 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.resolver.fs; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.lang.reflect.Method; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; + +class ModelsRootTest { + + private static final File EMPTY_FILE = new File( "" ); + + @Test + void resolveByCanonicalPathShouldReturnFileWhenCanonicalPathMatches() throws Exception { + Path testPath = Paths.get( "src/test/resources/resolve", "Aspect.ttl" ).toAbsolutePath(); + + File result = invokeResolveByCanonicalPath( testPath ); + + assertThat( result ) + .matches( File::exists ) + .isEqualTo( testPath.toFile() ); + } + + @Test + void resolveByCanonicalPathShouldReturnFileWhenCanonicalPathMatchesForSpecificPath() throws Exception { + Path testPath = Paths.get( "src/test/resources/../resources/resolve", "Aspect.ttl" ).toAbsolutePath(); + + File result = invokeResolveByCanonicalPath( testPath ); + + assertThat( result ) + .matches( File::exists ) + .isEqualTo( testPath.toFile() ); + } + + @Test + void resolveByCanonicalPathShouldReturnEmptyFileWhenCanonicalPathDoesNotMatch() throws Exception { + Path invalidPath = Paths.get( "src/test/resources/resolve", "aspect.ttl" ).toAbsolutePath(); + + File result = invokeResolveByCanonicalPath( invalidPath ); + + assertThat( result ).isEqualTo( EMPTY_FILE ); + } + + private static File invokeResolveByCanonicalPath( Path path ) throws Exception { + Method method = ModelsRoot.class.getDeclaredMethod( "resolveByCanonicalPath", Path.class ); + method.setAccessible( true ); + return (File) method.invoke( null, path ); + } +} \ No newline at end of file diff --git a/core/esmf-aspect-meta-model-java/src/test/resources/resolve/Aspect.ttl b/core/esmf-aspect-meta-model-java/src/test/resources/resolve/Aspect.ttl new file mode 100644 index 000000000..8bc3b7d3a --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/test/resources/resolve/Aspect.ttl @@ -0,0 +1,19 @@ +# Copyright (c) 2025 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 + +@prefix : . +@prefix samm: . + +:TestAspect a samm:Aspect ; + samm:name "TestAspect" ; + samm:preferredName "Test Aspect"@en ; + samm:properties ( ) ; + samm:operations ( ) . diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/aas/to/AasToAspectCommand.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/aas/to/AasToAspectCommand.java index ed61989bf..173b44dbc 100644 --- a/tools/samm-cli/src/main/java/org/eclipse/esmf/aas/to/AasToAspectCommand.java +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/aas/to/AasToAspectCommand.java @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2025 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.aas.to; import java.io.BufferedWriter; @@ -58,7 +71,7 @@ public class AasToAspectCommand extends AbstractCommand { public void run() { final String path = parentCommand.parentCommand.getInput(); final String extension = FilenameUtils.getExtension( path ); - if ( !extension.equals( "xml" ) && !extension.equals( "json" ) && !extension.equals( "aasx" ) ) { + if ( !"xml".equals( extension ) && !"json".equals( extension ) && !"aasx".equals( extension ) ) { throw new CommandException( "Input file name must be an .xml, .aasx or .json file" ); } generateAspects( AasToAspectModelGenerator.fromFile( new File( path ) ) ); @@ -76,12 +89,12 @@ private void generateAspects( final AasToAspectModelGenerator generator ) { for ( final Aspect aspect : filteredAspects ) { final String aspectString = AspectSerializer.INSTANCE.aspectToString( aspect ); - final File targetFile = modelsRoot.determineAspectModelFile( aspect.urn() ); - LOG.info( "Writing {}", targetFile.getAbsolutePath() ); - final File directory = targetFile.getParentFile(); + final File directory = modelsRoot.directoryForNamespace( aspect.urn() ).toFile(); if ( !directory.exists() && !directory.mkdirs() ) { throw new CommandException( "Could not create directory: " + directory.getAbsolutePath() ); } + final File targetFile = modelsRoot.determineAspectModelFile( aspect.urn() ); + LOG.info( "Writing {}", targetFile.getAbsolutePath() ); try ( final Writer writer = new BufferedWriter( new FileWriter( targetFile ) ) ) { writer.write( aspectString ); } catch ( final IOException exception ) {