diff --git a/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/aspectmodel/AspectModelFile.java b/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/aspectmodel/AspectModelFile.java index ddf41147b..ccdab96f9 100644 --- a/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/aspectmodel/AspectModelFile.java +++ b/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/aspectmodel/AspectModelFile.java @@ -19,6 +19,7 @@ import org.eclipse.esmf.metamodel.ModelElement; import org.eclipse.esmf.metamodel.ModelElementGroup; +import org.eclipse.esmf.metamodel.Namespace; import org.apache.jena.rdf.model.Model; @@ -31,10 +32,12 @@ default List headerComment() { Optional sourceLocation(); + default Namespace namespace() { + throw new UnsupportedOperationException( "Uninitialized Aspect Model" ); + } + @Override default List elements() { throw new UnsupportedOperationException( "Uninitialized Aspect Model" ); } - - // boolean isAutoMigrated(); } \ No newline at end of file diff --git a/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/vocabulary/SAMM.java b/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/vocabulary/SAMM.java index 4f1116a1e..39b4a4eb7 100644 --- a/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/vocabulary/SAMM.java +++ b/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/vocabulary/SAMM.java @@ -50,6 +50,11 @@ public String getNamespace() { return getUri() + "#"; } + @SuppressWarnings( "checkstyle:MethodName" ) + public Resource Namespace() { + return resource( "Namespace" ); + } + public Property listType() { return property( "listType" ); } diff --git a/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/vocabulary/SimpleRdfNamespace.java b/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/vocabulary/SimpleRdfNamespace.java new file mode 100644 index 000000000..151f499f2 --- /dev/null +++ b/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/vocabulary/SimpleRdfNamespace.java @@ -0,0 +1,34 @@ +/* + * 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.metamodel.vocabulary; + +public class SimpleRdfNamespace implements RdfNamespace { + private final String shortForm; + private final String uri; + + public SimpleRdfNamespace( final String shortForm, final String uri ) { + this.shortForm = shortForm; + this.uri = uri; + } + + @Override + public String getShortForm() { + return shortForm; + } + + @Override + public String getUri() { + return uri; + } +} diff --git a/core/esmf-aspect-meta-model-java/pom.xml b/core/esmf-aspect-meta-model-java/pom.xml index 9429f57ae..ca705e25d 100644 --- a/core/esmf-aspect-meta-model-java/pom.xml +++ b/core/esmf-aspect-meta-model-java/pom.xml @@ -58,6 +58,11 @@ com.google.guava guava + + io.soabase.record-builder + record-builder-processor + provided + @@ -153,6 +158,11 @@ lombok ${lombok-version} + + io.soabase.record-builder + record-builder-processor + ${record-builder-version} + diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/RdfUtil.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/RdfUtil.java new file mode 100644 index 000000000..2ceabab5c --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/RdfUtil.java @@ -0,0 +1,143 @@ +/* + * 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; + +import static java.util.stream.Collectors.toSet; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.eclipse.esmf.aspectmodel.resolver.services.TurtleLoader; +import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn; +import org.eclipse.esmf.aspectmodel.urn.ElementType; +import org.eclipse.esmf.metamodel.vocabulary.SammNs; + +import com.google.common.collect.Streams; +import org.apache.jena.rdf.model.Literal; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.Statement; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.XSD; + +public class RdfUtil { + public static Model getModelElementDefinition( final Resource element ) { + final Model result = ModelFactory.createDefaultModel(); + element.getModel().listStatements( element, null, (RDFNode) null ).toList().forEach( statement -> { + final Resource subject = statement.getSubject(); + final Resource newSubject = subject.isAnon() + ? result.createResource( subject.getId() ) + : result.createResource( subject.getURI() ); + final Property newPredicate = result.createProperty( statement.getPredicate().getURI() ); + final RDFNode newObject; + if ( statement.getObject().isURIResource() ) { + newObject = result.createResource( statement.getObject().asResource().getURI() ); + } else if ( statement.getObject().isLiteral() ) { + newObject = statement.getObject(); + } else if ( statement.getObject().isAnon() ) { + newObject = result.createResource( statement.getObject().asResource().getId() ); + result.add( getModelElementDefinition( statement.getObject().asResource() ) ); + } else { + newObject = statement.getObject(); + } + result.add( newSubject, newPredicate, newObject ); + } ); + return result; + } + + public static Set getAllUrnsInModel( final Model model ) { + return Streams.stream( model.listStatements().mapWith( statement -> { + final Stream subjectUri = statement.getSubject().isURIResource() + ? Stream.of( statement.getSubject().getURI() ) + : Stream.empty(); + final Stream propertyUri = Stream.of( statement.getPredicate().getURI() ); + final Stream objectUri = statement.getObject().isURIResource() + ? Stream.of( statement.getObject().asResource().getURI() ) + : Stream.empty(); + + return Stream.of( subjectUri, propertyUri, objectUri ) + .flatMap( Function.identity() ) + .flatMap( urn -> AspectModelUrn.from( urn ).toJavaOptional().stream() ); + } ) ).flatMap( Function.identity() ).collect( toSet() ); + } + + public static void cleanPrefixes( final Model model ) { + final Map originalPrefixMap = new HashMap<>( model.getNsPrefixMap() ); + model.clearNsPrefixMap(); + // SAMM prefixes + getAllUrnsInModel( model ).forEach( urn -> { + switch ( urn.getElementType() ) { + case META_MODEL -> model.setNsPrefix( SammNs.SAMM.getShortForm(), SammNs.SAMM.getNamespace() ); + case CHARACTERISTIC -> model.setNsPrefix( SammNs.SAMMC.getShortForm(), SammNs.SAMMC.getNamespace() ); + case ENTITY -> model.setNsPrefix( SammNs.SAMME.getShortForm(), SammNs.SAMME.getNamespace() ); + case UNIT -> model.setNsPrefix( SammNs.UNIT.getShortForm(), SammNs.UNIT.getNamespace() ); + default -> { + // nothing to do + } + } + } ); + // XSD + Stream.concat( + Streams.stream( model.listObjects() ) + .filter( RDFNode::isLiteral ) + .map( RDFNode::asLiteral ) + .map( Literal::getDatatypeURI ) + .filter( type -> type.startsWith( XSD.NS ) ) + .filter( type -> !type.equals( XSD.xstring.getURI() ) ), + Streams.stream( model.listObjects() ) + .filter( RDFNode::isURIResource ) + .map( RDFNode::asResource ) + .map( Resource::getURI ) + .filter( type -> type.startsWith( XSD.NS ) ) ) + .findAny() + .ifPresent( resource -> model.setNsPrefix( "xsd", XSD.NS ) ); + // Empty (namespace) prefix + Streams.stream( model.listStatements( null, RDF.type, (RDFNode) null ) ) + .map( Statement::getSubject ) + .filter( Resource::isURIResource ) + .map( Resource::getURI ) + .map( AspectModelUrn::fromUrn ) + .findAny() + .ifPresent( urn -> model.setNsPrefix( "", urn.getUrnPrefix() ) ); + // Add back custom prefixes not already covered: + // - if the prefix or URI is not set already + // - it's not XSD (no need to add it here if it's not added above) + // - if it's a SAMM URN, it's a regular namespace (not a meta model namespace) + originalPrefixMap.forEach( ( prefix, uri ) -> { + if ( !model.getNsPrefixMap().containsKey( prefix ) + && !model.getNsPrefixMap().containsValue( uri ) + && !uri.equals( XSD.NS ) + && ( !uri.startsWith( "urn:samm:" ) || AspectModelUrn.fromUrn( uri + "x" ).getElementType() == ElementType.NONE ) + ) { + model.setNsPrefix( prefix, uri ); + } + } ); + } + + /** + * Convenience method to load an RDF/Turtle model from its String representation + * + * @param ttlRepresentation the RDF/Turtle representation of the model + * @return the parsed model + */ + public static Model createModel( final String ttlRepresentation ) { + return TurtleLoader.loadTurtle( ttlRepresentation ).get(); + } +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/AspectChangeManager.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/AspectChangeManager.java new file mode 100644 index 000000000..2fd66bc7d --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/AspectChangeManager.java @@ -0,0 +1,188 @@ +/* + * 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.edit; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import org.eclipse.esmf.aspectmodel.AspectModelFile; +import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader; +import org.eclipse.esmf.aspectmodel.resolver.modelfile.RawAspectModelFile; +import org.eclipse.esmf.metamodel.AspectModel; +import org.eclipse.esmf.metamodel.impl.DefaultAspectModel; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The AspectChangeManager is the central place to to changes/edits/refactorings of an {@link AspectModel}. The AspectChangeManager + * wraps an AspectModel and allows applying instances of the {@link Change} class using the {@link #applyChange(Change)} method. + * Calling this method returns a {@link ChangeReport} that describes the performed changes in a structured way. Use the + * {@link ChangeReportFormatter} to render the ChangeReport to a structured string representation. + *
+ * Note the following points: + *
    + *
  • Only one AspectChangeManager must wrap a given AspectModel at any time
  • + *
  • All changes are done in-memory. In order to write them to the file system, use the + * {@link org.eclipse.esmf.aspectmodel.serializer.AspectSerializer}
  • + *
  • After performing an {@link #applyChange(Change)}, {@link #undoChange()} or {@link #redoChange()} operation, and until the + * next call of one of them, the methods {@link #modifiedFiles()}, {@link #createdFiles()} and {@link #removedFiles()} indicate + * corresponding changes in the AspectModel's files. + *
+ */ +public class AspectChangeManager implements ChangeContext { + private static final Logger LOG = LoggerFactory.getLogger( AspectChangeManager.class ); + + private final Deque undoStack = new ArrayDeque<>(); + private final Deque redoStack = new ArrayDeque<>(); + private final DefaultAspectModel aspectModel; + private final AspectChangeManagerConfig config; + private final Map fileState = new HashMap<>(); + + private enum FileState { + CREATED, CHANGED, REMOVED + } + + public AspectChangeManager( final AspectChangeManagerConfig config, final AspectModel aspectModel ) { + this.config = config; + resetFileStates(); + if ( aspectModel instanceof final DefaultAspectModel defaultAspectModel ) { + this.aspectModel = defaultAspectModel; + } else { + throw new ModelChangeException( "AspectModel must be an instance of DefaultAspectModel" ); + } + } + + public AspectChangeManager( final AspectModel aspectModel ) { + this( AspectChangeManagerConfigBuilder.builder().build(), aspectModel ); + } + + public synchronized ChangeReport applyChange( final Change change ) { + resetFileStates(); + final ChangeReport result = change.fire( this ); + updateAspectModelAfterChange(); + undoStack.offerLast( change.reverse() ); + return result; + } + + public synchronized void undoChange() { + if ( undoStack.isEmpty() ) { + return; + } + resetFileStates(); + final Change change = undoStack.pollLast(); + change.fire( this ); + updateAspectModelAfterChange(); + redoStack.offerLast( change.reverse() ); + } + + public synchronized void redoChange() { + if ( redoStack.isEmpty() ) { + return; + } + resetFileStates(); + final Change change = redoStack.pollLast(); + change.fire( this ); + updateAspectModelAfterChange(); + undoStack.offerLast( change.reverse() ); + } + + private void updateAspectModelAfterChange() { + final AspectModel updatedModel = new AspectModelLoader().loadAspectModelFiles( aspectModel.files() ); + aspectModel.setMergedModel( updatedModel.mergedModel() ); + aspectModel.setElements( updatedModel.elements() ); + aspectModel.setFiles( updatedModel.files() ); + + final Map updatedFileState = new HashMap<>(); + for ( final Map.Entry stateEntry : fileState.entrySet() ) { + final AspectModelFile file = stateEntry.getKey(); + final FileState state = stateEntry.getValue(); + + if ( file instanceof final RawAspectModelFile rawFile ) { + final Optional updatedAspectModelFile = aspectModel.files().stream() + .filter( f -> f.sourceLocation().isPresent() ) + .filter( f -> f.sourceLocation().equals( file.sourceLocation() ) ) + .findFirst(); + if ( updatedAspectModelFile.isEmpty() ) { + continue; + } + updatedFileState.put( updatedAspectModelFile.get(), state ); + } else { + updatedFileState.put( file, state ); + } + } + fileState.clear(); + fileState.putAll( updatedFileState ); + } + + @Override + public Stream aspectModelFiles() { + return aspectModel.files().stream(); + } + + @Override + public AspectChangeManagerConfig config() { + return config; + } + + @Override + public Stream createdFiles() { + return fileState.entrySet().stream() + .filter( entry -> entry.getValue() == FileState.CREATED ) + .map( Map.Entry::getKey ); + } + + @Override + public Stream modifiedFiles() { + return fileState.entrySet().stream() + .filter( entry -> entry.getValue() == FileState.CHANGED ) + .map( Map.Entry::getKey ); + } + + @Override + public Stream removedFiles() { + return fileState.entrySet().stream() + .filter( entry -> entry.getValue() == FileState.REMOVED ) + .map( Map.Entry::getKey ); + } + + @Override + public void resetFileStates() { + fileState.clear(); + } + + @Override + public void indicateFileIsAdded( final AspectModelFile file ) { + fileState.put( file, FileState.CREATED ); + aspectModel.files().add( file ); + } + + @Override + public void indicateFileIsRemoved( final AspectModelFile file ) { + fileState.put( file, FileState.REMOVED ); + aspectModel.files().remove( file ); + } + + @Override + public void indicateFileHasChanged( final AspectModelFile file ) { + // If the file was newly created, keep this state even if we now change the file content + if ( fileState.get( file ) != FileState.CREATED ) { + fileState.put( file, FileState.CHANGED ); + } + } +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/AspectChangeManagerConfig.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/AspectChangeManagerConfig.java new file mode 100644 index 000000000..2af3383cd --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/AspectChangeManagerConfig.java @@ -0,0 +1,30 @@ +/* + * 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.edit; + +import java.util.List; + +import io.soabase.recordbuilder.core.RecordBuilder; + +@RecordBuilder +public record AspectChangeManagerConfig( + List defaultFileHeader, + boolean detailedChangeReport +) { + public AspectChangeManagerConfig { + if ( defaultFileHeader == null ) { + defaultFileHeader = List.of(); + } + } +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/Change.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/Change.java new file mode 100644 index 000000000..c79f0d38f --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/Change.java @@ -0,0 +1,38 @@ +/* + * 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.edit; + +/** + * This interface represents a single modification to an Aspect Model. Instances of Change should be applied to an Aspect Model + * using {@link AspectChangeManager}; see the description about the change mechanism there. Instances of Change must not + * perform file system operations; synching "adding", "removing" and "modifying" files with the file system is a separate step. + */ +public interface Change { + /** + * "Run" this change. This method should not be directly called on a Change object, but will be executed by the AspectChangeManager. + * The passed {@link ChangeContext} provides information to the Change implementation about the current state of the AspectModel. + * + * @param changeContext the change context + * @return the report describing what has been changed + */ + ChangeReport fire( ChangeContext changeContext ); + + /** + * Returns a Change that is the reverse operation of this one, i.e., running this change and the reverse change after another + * effectively cancels out any changes. + * + * @return the Change representing the reverse operation + */ + Change reverse(); +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/ChangeContext.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/ChangeContext.java new file mode 100644 index 000000000..dcc190742 --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/ChangeContext.java @@ -0,0 +1,42 @@ +/* + * 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.edit; + +import java.util.stream.Stream; + +import org.eclipse.esmf.aspectmodel.AspectModelFile; + +/** + * The ChangeContext encapsulates the functionality provided to {@link Change} implementations to access the current set of + * Aspect Model Files and indicate changes. + */ +public interface ChangeContext { + Stream aspectModelFiles(); + + AspectChangeManagerConfig config(); + + Stream createdFiles(); + + Stream modifiedFiles(); + + Stream removedFiles(); + + void indicateFileIsAdded( AspectModelFile file ); + + void indicateFileIsRemoved( AspectModelFile file ); + + void indicateFileHasChanged( AspectModelFile file ); + + void resetFileStates(); +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/ChangeGroup.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/ChangeGroup.java new file mode 100644 index 000000000..5f829aff8 --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/ChangeGroup.java @@ -0,0 +1,57 @@ +/* + * 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.edit; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A Change that groups other changes + */ +public class ChangeGroup implements Change { + private final String summary; + private final List changes; + + public ChangeGroup( final String summary, final Change... changes ) { + this( summary, Arrays.asList( changes ) ); + } + + public ChangeGroup( final Change... changes ) { + this( null, Arrays.asList( changes ) ); + } + + public ChangeGroup( final String summary, final List changes ) { + this.summary = summary; + this.changes = changes; + } + + public ChangeGroup( final List changes ) { + this( null, changes ); + } + + @Override + public ChangeReport fire( final ChangeContext changeContext ) { + return new ChangeReport.MultipleEntries( summary, changes.stream().map( change -> change.fire( changeContext ) ).toList() ); + } + + @Override + public Change reverse() { + final List reversedChanges = new ArrayList<>( changes.size() ); + for ( int i = changes.size() - 1; i >= 0; i-- ) { + reversedChanges.add( changes.get( i ).reverse() ); + } + return new ChangeGroup( reversedChanges ); + } +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/ChangeReport.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/ChangeReport.java new file mode 100644 index 000000000..77600adc2 --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/ChangeReport.java @@ -0,0 +1,37 @@ +/* + * 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.edit; + +import java.util.List; +import java.util.Map; + +/** + * A structured representation of a number of {@link Change}s + */ +public interface ChangeReport { + ChangeReport NO_CHANGES = new ChangeReport() { + }; + + record SimpleEntry( String text ) implements ChangeReport { + } + + record EntryWithDetails( String summary, Map details ) implements ChangeReport { + } + + record MultipleEntries( String summary, List entries ) implements ChangeReport { + public MultipleEntries( final List entries ) { + this( null, entries ); + } + } +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/ChangeReportFormatter.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/ChangeReportFormatter.java new file mode 100644 index 000000000..6fec26c70 --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/ChangeReportFormatter.java @@ -0,0 +1,134 @@ +/* + * 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.edit; + +import java.io.StringWriter; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; + +import org.eclipse.esmf.aspectmodel.RdfUtil; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; + +/** + * Takes a {@link ChangeReport} as an input and renders it as a string + */ +public class ChangeReportFormatter implements BiFunction { + public static final ChangeReportFormatter INSTANCE = new ChangeReportFormatter(); + + private ChangeReportFormatter() { + } + + private void handleSimpleEntry( final StringBuilder builder, final ChangeReport.SimpleEntry simpleEntry, final String indent ) { + builder.append( indent ); + builder.append( "- " ); + builder.append( simpleEntry.text() ); + builder.append( "\n" ); + } + + private void handleMultipleEntries( final StringBuilder builder, final ChangeReport.MultipleEntries multipleEntries, final String indent, + final int indentationLevel, final AspectChangeManagerConfig config ) { + if ( multipleEntries.summary() != null ) { + builder.append( indent ); + builder.append( "- " ); + builder.append( multipleEntries.summary() ); + builder.append( "\n" ); + } + final List entries = multipleEntries.entries(); + for ( int i = 0; i < entries.size(); i++ ) { + final ChangeReport entry = entries.get( i ); + final int entryIndentation = multipleEntries.summary() == null + ? indentationLevel + : indentationLevel + 1; + append( builder, entry, config, entryIndentation ); + if ( i < entries.size() - 1 ) { + builder.append( "\n" ); + } + } + } + + private void handleEntryWithDetails( final StringBuilder builder, final ChangeReport.EntryWithDetails entryWithDetails, + final String indent, final AspectChangeManagerConfig config ) { + builder.append( indent ); + builder.append( "- " ); + builder.append( entryWithDetails.summary() ); + builder.append( "\n" ); + for ( final Map.Entry entry : entryWithDetails.details().entrySet() ) { + if ( config.detailedChangeReport() && entry.getValue() instanceof final Model model ) { + builder.append( indent ); + builder.append( " - " ); + builder.append( entry.getKey() ); + builder.append( ": " ); + builder.append( "\n" ); + show( model ).lines() + .forEach( line -> { + builder.append( indent ); + builder.append( " " ); + builder.append( line ); + builder.append( "\n" ); + } ); + } else if ( !config.detailedChangeReport() && entry.getValue() instanceof final Model model ) { + final int numberOfStatements = model.listStatements().toList().size(); + if ( numberOfStatements > 0 ) { + builder.append( indent ); + builder.append( " - " ); + builder.append( entry.getKey() ); + builder.append( ": " ); + builder.append( numberOfStatements ); + builder.append( " RDF statements" ); + builder.append( "\n" ); + } + } else { + builder.append( indent ); + builder.append( " - " ); + builder.append( entry.getKey() ); + builder.append( ": " ); + builder.append( entry.getValue().toString() ); + builder.append( "\n" ); + } + } + } + + private void append( final StringBuilder builder, final ChangeReport report, final AspectChangeManagerConfig config, + final int indentationLevel ) { + final String indent = " ".repeat( indentationLevel ); + if ( report instanceof final ChangeReport.SimpleEntry simpleEntry ) { + handleSimpleEntry( builder, simpleEntry, indent ); + } else if ( report instanceof final ChangeReport.MultipleEntries multipleEntries ) { + handleMultipleEntries( builder, multipleEntries, indent, indentationLevel, config ); + } else if ( report instanceof final ChangeReport.EntryWithDetails entryWithDetails ) { + handleEntryWithDetails( builder, entryWithDetails, indent, config ); + } + } + + private String show( final Model model ) { + final Model copy = ModelFactory.createDefaultModel(); + copy.add( model ); + RdfUtil.cleanPrefixes( copy ); + final StringWriter stringWriter = new StringWriter(); + stringWriter.append( "--------------------\n" ); + copy.write( stringWriter, "TURTLE" ); + stringWriter.append( "--------------------\n" ); + return stringWriter.toString(); + } + + @Override + public String apply( final ChangeReport changeReport, final AspectChangeManagerConfig config ) { + final StringBuilder builder = new StringBuilder(); + append( builder, changeReport, config, 0 ); + return builder.toString(); + } +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/ModelChangeException.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/ModelChangeException.java new file mode 100644 index 000000000..6ef443f43 --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/ModelChangeException.java @@ -0,0 +1,25 @@ +/* + * 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.edit; + +import java.io.Serial; + +public class ModelChangeException extends RuntimeException { + @Serial + private static final long serialVersionUID = 6040601725774787289L; + + public ModelChangeException( final String message ) { + super( message ); + } +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/AbstractChange.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/AbstractChange.java new file mode 100644 index 000000000..1d2c76c68 --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/AbstractChange.java @@ -0,0 +1,30 @@ +/* + * 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.edit.change; + +import java.net.URI; +import java.util.Optional; + +import org.eclipse.esmf.aspectmodel.AspectModelFile; +import org.eclipse.esmf.aspectmodel.edit.Change; + +public abstract class AbstractChange implements Change { + protected String show( final AspectModelFile aspectModelFile ) { + return show( aspectModelFile.sourceLocation() ); + } + + protected String show( final Optional sourceLocation ) { + return sourceLocation.map( URI::toString ).orElse( "(unknown file)" ); + } +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/AddAspectModelFile.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/AddAspectModelFile.java new file mode 100644 index 000000000..e41ac836e --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/AddAspectModelFile.java @@ -0,0 +1,70 @@ +/* + * 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.edit.change; + +import java.util.Map; + +import org.eclipse.esmf.aspectmodel.AspectModelFile; +import org.eclipse.esmf.aspectmodel.edit.Change; +import org.eclipse.esmf.aspectmodel.edit.ChangeContext; +import org.eclipse.esmf.aspectmodel.edit.ChangeReport; +import org.eclipse.esmf.aspectmodel.edit.ModelChangeException; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; + +/** + * Represents the operation of adding an {@link AspectModelFile} to an AspectModel + */ +public class AddAspectModelFile extends AbstractChange { + private final AspectModelFile newFile; + + public AddAspectModelFile( final AspectModelFile newFile ) { + this.newFile = newFile; + } + + @Override + public ChangeReport fire( final ChangeContext changeContext ) { + changeContext.indicateFileIsAdded( newFile ); + final Model contentToAdd = ModelFactory.createDefaultModel(); + contentToAdd.add( newFile.sourceModel() ); + return new ChangeReport.EntryWithDetails( "Add file " + show( newFile ), + Map.of( "Model content to add", contentToAdd ) ); + } + + @Override + public Change reverse() { + return new Change() { + @Override + public ChangeReport fire( final ChangeContext changeContext ) { + final AspectModelFile file = fileToRemove( changeContext ); + changeContext.indicateFileIsRemoved( file ); + return new ChangeReport.EntryWithDetails( "Remove file " + show( file ), + Map.of( "model content to remove", file.sourceModel() ) ); + } + + @Override + public Change reverse() { + return AddAspectModelFile.this; + } + + private AspectModelFile fileToRemove( final ChangeContext changeContext ) { + return changeContext.aspectModelFiles() + .filter( file -> file.sourceLocation().equals( newFile.sourceLocation() ) ) + .findFirst() + .orElseThrow( () -> new ModelChangeException( "Unable to remove Aspect Model File" ) ); + } + }; + } +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/AddElementDefinition.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/AddElementDefinition.java new file mode 100644 index 000000000..a7bad278a --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/AddElementDefinition.java @@ -0,0 +1,62 @@ +/* + * 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.edit.change; + +import org.eclipse.esmf.aspectmodel.AspectModelFile; +import org.eclipse.esmf.aspectmodel.edit.Change; +import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; + +/** + * Adds the definition of a model element to an AspectModelFile. The definition is given as a set of RDF statements (a {@link Model}). + */ +public class AddElementDefinition extends EditAspectModel { + private final AspectModelUrn elementUrn; + private final Model definition; + private final AspectModelFile targetFile; + + public AddElementDefinition( final AspectModelUrn elementUrn, final Model definition, final AspectModelFile targetFile ) { + this.elementUrn = elementUrn; + this.definition = definition; + this.targetFile = targetFile; + } + + @Override + protected ModelChanges calculateChangesForFile( final AspectModelFile aspectModelFile ) { + return aspectModelFile.equals( targetFile ) + ? new ModelChanges( definition, ModelFactory.createDefaultModel(), + "Add definition of " + elementUrn ) + : ModelChanges.NONE; + } + + @Override + public Change reverse() { + return new EditAspectModel() { + @Override + protected ModelChanges calculateChangesForFile( final AspectModelFile aspectModelFile ) { + return aspectModelFile.sourceLocation().equals( targetFile.sourceLocation() ) + ? new ModelChanges( ModelFactory.createDefaultModel(), definition, + "Remove definition of " + elementUrn ) + : ModelChanges.NONE; + } + + @Override + public Change reverse() { + return AddElementDefinition.this; + } + }; + } +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/EditAspectModel.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/EditAspectModel.java new file mode 100644 index 000000000..ef0f88ce3 --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/EditAspectModel.java @@ -0,0 +1,79 @@ +/* + * 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.edit.change; + +import java.util.Map; +import java.util.stream.Collectors; + +import org.eclipse.esmf.aspectmodel.AspectModelFile; +import org.eclipse.esmf.aspectmodel.edit.ChangeContext; +import org.eclipse.esmf.aspectmodel.edit.ChangeReport; + +import org.apache.jena.rdf.model.Model; + +/** + * Abstract base class for all Changes that change the content (i.e., the underlying RDF) of an Aspect Model file. + */ +public abstract class EditAspectModel extends AbstractChange { + /** + * Represents the changes to perform on the RDF model + * + * @param add the set of statements to add + * @param remove the set of statements to remove + * @param description the description of this change set (used in the {@link ChangeReport}) + */ + protected record ModelChanges( Model add, Model remove, String description ) { + public static final ModelChanges NONE = new ModelChanges( null, null, "" ); + } + + /** + * Each AspectModelFile is changed separately, so extending classes need to calculate changes for a given file. + * + * @param aspectModelFile the AspectModelFile to change + * @return the set of changes + */ + protected abstract ModelChanges calculateChangesForFile( AspectModelFile aspectModelFile ); + + @Override + public ChangeReport fire( final ChangeContext changeContext ) { + final Map changesPerFile = changeContext.aspectModelFiles() + .map( file -> Map.entry( file, calculateChangesForFile( file ) ) ) + .filter( entry -> entry.getValue() != ModelChanges.NONE ) + .collect( Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue ) ); + + changesPerFile.forEach( ( file, modelChanges ) -> { + if ( changeContext.aspectModelFiles().anyMatch( file::equals ) ) { + file.sourceModel().add( modelChanges.add() ); + file.sourceModel().remove( modelChanges.remove() ); + + if ( !modelChanges.add().isEmpty() || !modelChanges.remove().isEmpty() ) { + changeContext.indicateFileHasChanged( file ); + } + } + } ); + + return new ChangeReport.MultipleEntries( + changesPerFile.entrySet().stream(). map( entry -> { + final AspectModelFile file = entry.getKey(); + final ModelChanges modelChanges = entry.getValue(); + return new ChangeReport.EntryWithDetails( modelChanges.description(), Map.of( + "Add content in " + show( file ), modelChanges.add(), + "Remove content from " + show( file ), modelChanges.remove() ) + .entrySet().stream() + .filter( descriptionEntry -> !descriptionEntry.getValue().isEmpty() ) + .collect( Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue ) ) ); + } ).toList() + ); + } +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/MoveElementToExistingFile.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/MoveElementToExistingFile.java new file mode 100644 index 000000000..46f5a3c88 --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/MoveElementToExistingFile.java @@ -0,0 +1,81 @@ +/* + * 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.edit.change; + +import java.net.URI; +import java.util.Optional; + +import org.eclipse.esmf.aspectmodel.AspectModelFile; +import org.eclipse.esmf.aspectmodel.RdfUtil; +import org.eclipse.esmf.aspectmodel.edit.Change; +import org.eclipse.esmf.aspectmodel.edit.ChangeContext; +import org.eclipse.esmf.aspectmodel.edit.ChangeGroup; +import org.eclipse.esmf.aspectmodel.edit.ChangeReport; +import org.eclipse.esmf.aspectmodel.edit.ModelChangeException; +import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn; +import org.eclipse.esmf.metamodel.ModelElement; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.Resource; + +/** + * Refactoring operation: Moves a model element to another, existing file in the same namespace. + */ +public class MoveElementToExistingFile extends StructuralChange { + private final AspectModelUrn elementUrn; + private final Optional targetFileLocation; + private ChangeGroup changes = null; + + public MoveElementToExistingFile( final ModelElement modelElement, final AspectModelFile targetFile ) { + this( modelElement.urn(), targetFile ); + if ( modelElement.isAnonymous() ) { + throw new ModelChangeException( "Can not move anonymous model element" ); + } + } + + public MoveElementToExistingFile( final AspectModelUrn elementUrn, final AspectModelFile targetFile ) { + this.elementUrn = elementUrn; + targetFileLocation = targetFile.sourceLocation(); + } + + @Override + public ChangeReport fire( final ChangeContext changeContext ) { + final AspectModelFile targetFile = changeContext.aspectModelFiles() + .filter( file -> file.sourceLocation().equals( targetFileLocation ) ) + .findFirst() + .orElseThrow( () -> new ModelChangeException( "Can not determine target file to move element" ) ); + + // Find source file with element definition + final AspectModelFile sourceFile = sourceFile( changeContext, elementUrn ); + if ( sourceFile == targetFile ) { + return ChangeReport.NO_CHANGES; + } + final Resource elementResource = sourceFile.sourceModel().createResource( elementUrn.toString() ); + final Model definition = RdfUtil.getModelElementDefinition( elementResource ); + + // Perform move of element definition + changes = new ChangeGroup( + "Move element " + elementUrn + " to file " + show( targetFile ), + new RemoveElementDefinition( elementUrn ), + new AddElementDefinition( elementUrn, definition, targetFile ) + ); + + return changes.fire( changeContext ); + } + + @Override + public Change reverse() { + return changes.reverse(); + } +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/MoveElementToNewFile.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/MoveElementToNewFile.java new file mode 100644 index 000000000..394f4037e --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/MoveElementToNewFile.java @@ -0,0 +1,99 @@ +/* + * 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.edit.change; + +import java.net.URI; +import java.util.List; +import java.util.Optional; + +import org.eclipse.esmf.aspectmodel.RdfUtil; +import org.eclipse.esmf.aspectmodel.edit.Change; +import org.eclipse.esmf.aspectmodel.edit.ChangeContext; +import org.eclipse.esmf.aspectmodel.edit.ChangeGroup; +import org.eclipse.esmf.aspectmodel.edit.ChangeReport; +import org.eclipse.esmf.aspectmodel.edit.ModelChangeException; +import org.eclipse.esmf.aspectmodel.resolver.modelfile.RawAspectModelFile; +import org.eclipse.esmf.aspectmodel.resolver.modelfile.RawAspectModelFileBuilder; +import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn; +import org.eclipse.esmf.metamodel.ModelElement; +import org.eclipse.esmf.metamodel.vocabulary.SammNs; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.XSD; + +/** + * Refactoring operation: Moves a model element to a new file in the same namespace. + */ +public class MoveElementToNewFile extends StructuralChange { + private final List headerComment; + private final Optional sourceLocation; + private final AspectModelUrn elementUrn; + private ChangeGroup changes = null; + + public MoveElementToNewFile( final ModelElement modelElement, final Optional sourceLocation ) { + this( modelElement, null, sourceLocation ); + } + + public MoveElementToNewFile( final ModelElement modelElement, final List headerComment, final Optional sourceLocation ) { + this( modelElement.urn(), headerComment, sourceLocation ); + if ( modelElement.isAnonymous() ) { + throw new ModelChangeException( "Can not move anonymous model element" ); + } + } + + public MoveElementToNewFile( final AspectModelUrn elementUrn, final List headerComment, final Optional sourceLocation ) { + this.headerComment = headerComment; + this.sourceLocation = sourceLocation; + this.elementUrn = elementUrn; + } + + @Override + public ChangeReport fire( final ChangeContext changeContext ) { + // Prepare new file + final List fileHeader = Optional.ofNullable( headerComment ) + .or( () -> Optional.ofNullable( changeContext.config().defaultFileHeader() ) ) + .orElse( List.of() ); + final RawAspectModelFile targetFile = RawAspectModelFileBuilder.builder() + .headerComment( fileHeader ) + .sourceLocation( sourceLocation ) + .build(); + final Model targetModel = targetFile.sourceModel(); + targetModel.setNsPrefix( SammNs.SAMM.getShortForm(), SammNs.SAMM.getNamespace() ); + targetModel.setNsPrefix( SammNs.SAMMC.getShortForm(), SammNs.SAMMC.getNamespace() ); + targetModel.setNsPrefix( SammNs.UNIT.getShortForm(), SammNs.UNIT.getNamespace() ); + targetModel.setNsPrefix( SammNs.SAMME.getShortForm(), SammNs.SAMME.getNamespace() ); + targetModel.setNsPrefix( "xsd", XSD.NS ); + targetModel.setNsPrefix( "", elementUrn.getUrnPrefix() ); + + // Find source file with element definition + final Model sourceModel = sourceFile( changeContext, elementUrn ).sourceModel(); + final Resource elementResource = sourceModel.createResource( elementUrn.toString() ); + final Model definition = RdfUtil.getModelElementDefinition( elementResource ); + + // Perform move of element definition + changes = new ChangeGroup( + "Move element " + elementUrn + " to new file " + show( targetFile ), + new RemoveElementDefinition( elementUrn ), + new AddAspectModelFile( targetFile ), + new AddElementDefinition( elementUrn, definition, targetFile ) + ); + return changes.fire( changeContext ); + } + + @Override + public Change reverse() { + return changes.reverse(); + } +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/MoveElementToOtherNamespaceExistingFile.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/MoveElementToOtherNamespaceExistingFile.java new file mode 100644 index 000000000..3232d6111 --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/MoveElementToOtherNamespaceExistingFile.java @@ -0,0 +1,64 @@ +/* + * 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.edit.change; + +import org.eclipse.esmf.aspectmodel.AspectModelFile; +import org.eclipse.esmf.aspectmodel.edit.Change; +import org.eclipse.esmf.aspectmodel.edit.ChangeContext; +import org.eclipse.esmf.aspectmodel.edit.ChangeGroup; +import org.eclipse.esmf.aspectmodel.edit.ChangeReport; +import org.eclipse.esmf.aspectmodel.edit.ModelChangeException; +import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn; +import org.eclipse.esmf.metamodel.ModelElement; +import org.eclipse.esmf.metamodel.Namespace; + +/** + * Refactoring operation: Moves a model element to another, existing file in another namespace. + */ +public class MoveElementToOtherNamespaceExistingFile extends StructuralChange { + private final AspectModelUrn elementUrn; + private final AspectModelFile targetFile; + private final Namespace targetNamespace; + private ChangeGroup changes = null; + + public MoveElementToOtherNamespaceExistingFile( final ModelElement modelElement, final AspectModelFile targetFile, + final Namespace targetNamespace ) { + this( modelElement.urn(), targetFile, targetNamespace ); + if ( modelElement.isAnonymous() ) { + throw new ModelChangeException( "Can not move anonymous model element" ); + } + } + + public MoveElementToOtherNamespaceExistingFile( final AspectModelUrn elementUrn, final AspectModelFile targetFile, + final Namespace targetNamespace ) { + this.elementUrn = elementUrn; + this.targetFile = targetFile; + this.targetNamespace = targetNamespace; + } + + @Override + public ChangeReport fire( final ChangeContext changeContext ) { + changes = new ChangeGroup( + "Move element " + elementUrn + " to file " + show( targetFile ) + " in namespace " + targetNamespace.elementUrnPrefix(), + new MoveElementToExistingFile( elementUrn, targetFile ), + new RenameUrn( elementUrn, AspectModelUrn.fromUrn( targetNamespace.elementUrnPrefix() + elementUrn.getName() ) ) + ); + return changes.fire( changeContext ); + } + + @Override + public Change reverse() { + return changes.reverse(); + } +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/MoveElementToOtherNamespaceNewFile.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/MoveElementToOtherNamespaceNewFile.java new file mode 100644 index 000000000..68d5db6b8 --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/MoveElementToOtherNamespaceNewFile.java @@ -0,0 +1,83 @@ +/* + * 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.edit.change; + +import java.net.URI; +import java.util.List; +import java.util.Optional; + +import org.eclipse.esmf.aspectmodel.edit.Change; +import org.eclipse.esmf.aspectmodel.edit.ChangeContext; +import org.eclipse.esmf.aspectmodel.edit.ChangeGroup; +import org.eclipse.esmf.aspectmodel.edit.ChangeReport; +import org.eclipse.esmf.aspectmodel.edit.ModelChangeException; +import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn; +import org.eclipse.esmf.metamodel.ModelElement; +import org.eclipse.esmf.metamodel.Namespace; + +/** + * Refactoring operation: Moves a model element to a new file in another namespace. + */ +public class MoveElementToOtherNamespaceNewFile extends StructuralChange { + private final List headerComment; + private final Optional sourceLocation; + private final AspectModelUrn elementUrn; + private final Namespace targetNamespace; + private ChangeGroup changes = null; + + public MoveElementToOtherNamespaceNewFile( final ModelElement modelElement, final Namespace targetNamespace, + final Optional sourceLocation ) { + this( modelElement, targetNamespace, null, sourceLocation ); + } + + public MoveElementToOtherNamespaceNewFile( final ModelElement modelElement, final Namespace targetNamespace, + final List headerComment, final Optional sourceLocation ) { + this( modelElement.urn(), targetNamespace, headerComment, sourceLocation ); + if ( modelElement.isAnonymous() ) { + throw new ModelChangeException( "Can not move anonymous model element" ); + } + } + + public MoveElementToOtherNamespaceNewFile( final AspectModelUrn elementUrn, final Namespace targetNamespace, + final Optional sourceLocation ) { + this( elementUrn, targetNamespace, null, sourceLocation ); + } + + public MoveElementToOtherNamespaceNewFile( final AspectModelUrn elementUrn, final Namespace targetNamespace, + final List headerComment, final Optional sourceLocation ) { + this.elementUrn = elementUrn; + this.targetNamespace = targetNamespace; + this.headerComment = headerComment; + this.sourceLocation = sourceLocation; + } + + @Override + public ChangeReport fire( final ChangeContext changeContext ) { + final List fileHeader = Optional.ofNullable( headerComment ) + .or( () -> Optional.ofNullable( changeContext.config().defaultFileHeader() ) ) + .orElse( List.of() ); + changes = new ChangeGroup( + "Move element " + elementUrn + " to new file " + show( sourceLocation ) + + " in namespace " + targetNamespace.elementUrnPrefix(), + new MoveElementToNewFile( elementUrn, fileHeader, sourceLocation ), + new RenameUrn( elementUrn, AspectModelUrn.fromUrn( targetNamespace.elementUrnPrefix() + elementUrn.getName() ) ) + ); + return changes.fire( changeContext ); + } + + @Override + public Change reverse() { + return changes.reverse(); + } +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/MoveRenameAspectModelFile.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/MoveRenameAspectModelFile.java new file mode 100644 index 000000000..d75cd2c86 --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/MoveRenameAspectModelFile.java @@ -0,0 +1,67 @@ +/* + * 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.edit.change; + +import java.net.URI; +import java.util.Optional; + +import org.eclipse.esmf.aspectmodel.AspectModelFile; +import org.eclipse.esmf.aspectmodel.edit.Change; +import org.eclipse.esmf.aspectmodel.edit.ChangeContext; +import org.eclipse.esmf.aspectmodel.edit.ChangeGroup; +import org.eclipse.esmf.aspectmodel.edit.ChangeReport; +import org.eclipse.esmf.aspectmodel.edit.ModelChangeException; +import org.eclipse.esmf.aspectmodel.resolver.modelfile.RawAspectModelFileBuilder; + +/** + * Refactoring operation: Renames/moves a file. This is done by changing its source location. + */ +public class MoveRenameAspectModelFile extends StructuralChange { + private final AspectModelFile file; + private final Optional newLocation; + private ChangeGroup changes = null; + + public MoveRenameAspectModelFile( final AspectModelFile file, final URI newLocation ) { + this( file, Optional.of( newLocation ) ); + } + + public MoveRenameAspectModelFile( final AspectModelFile file, final Optional newLocation ) { + this.file = file; + this.newLocation = newLocation; + } + + @Override + public ChangeReport fire( final ChangeContext changeContext ) { + final AspectModelFile targetFile = changeContext.aspectModelFiles() + .filter( file -> file.sourceLocation().equals( file.sourceLocation() ) ) + .findFirst() + .orElseThrow( () -> new ModelChangeException( "Can not find file to move/rename" ) ); + + final AspectModelFile replacementFile = RawAspectModelFileBuilder.builder() + .sourceLocation( newLocation ) + .headerComment( targetFile.headerComment() ) + .sourceModel( targetFile.sourceModel() ) + .build(); + changes = new ChangeGroup( + new RemoveAspectModelFile( targetFile ), + new AddAspectModelFile( replacementFile ) + ); + return changes.fire( changeContext ); + } + + @Override + public Change reverse() { + return changes.reverse(); + } +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/RemoveAspectModelFile.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/RemoveAspectModelFile.java new file mode 100644 index 000000000..93ba6d15e --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/RemoveAspectModelFile.java @@ -0,0 +1,44 @@ +/* + * 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.edit.change; + +import java.util.Map; + +import org.eclipse.esmf.aspectmodel.AspectModelFile; +import org.eclipse.esmf.aspectmodel.edit.Change; +import org.eclipse.esmf.aspectmodel.edit.ChangeContext; +import org.eclipse.esmf.aspectmodel.edit.ChangeReport; + +/** + * Refactoring operation: Removes an AspectModelFile from an Aspect Model + */ +public class RemoveAspectModelFile extends AbstractChange { + private final AspectModelFile fileToRemove; + + public RemoveAspectModelFile( final AspectModelFile fileToRemove ) { + this.fileToRemove = fileToRemove; + } + + @Override + public ChangeReport fire( final ChangeContext changeContext ) { + changeContext.indicateFileIsRemoved( fileToRemove ); + return new ChangeReport.EntryWithDetails( "Remove file " + show( fileToRemove ), + Map.of( "model content", fileToRemove.sourceModel() ) ); + } + + @Override + public Change reverse() { + return new AddAspectModelFile( fileToRemove ); + } +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/RemoveElementDefinition.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/RemoveElementDefinition.java new file mode 100644 index 000000000..16ad6ea6f --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/RemoveElementDefinition.java @@ -0,0 +1,70 @@ +/* + * 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.edit.change; + +import org.eclipse.esmf.aspectmodel.AspectModelFile; +import org.eclipse.esmf.aspectmodel.RdfUtil; +import org.eclipse.esmf.aspectmodel.edit.Change; +import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDF; + +/** + * Removes the definition of a model element from an AspectModelFile. The definition is given as a set of RDF statements (a {@link Model}). + */ +public class RemoveElementDefinition extends EditAspectModel { + private final AspectModelUrn elementUrn; + private AspectModelFile fileWithOriginalDefinition; + private Model definition; + + public RemoveElementDefinition( final AspectModelUrn elementUrn ) { + this.elementUrn = elementUrn; + } + + @Override + protected ModelChanges calculateChangesForFile( final AspectModelFile aspectModelFile ) { + final Model model = aspectModelFile.sourceModel(); + final Resource elementResource = model.createResource( elementUrn.toString() ); + if ( !model.contains( elementResource, RDF.type, (RDFNode) null ) ) { + return ModelChanges.NONE; + } + + fileWithOriginalDefinition = aspectModelFile; + final Model add = ModelFactory.createDefaultModel(); + definition = RdfUtil.getModelElementDefinition( elementResource ); + return new ModelChanges( add, definition, "Remove definition of " + elementUrn ); + } + + @Override + public Change reverse() { + return new EditAspectModel() { + @Override + protected ModelChanges calculateChangesForFile( final AspectModelFile aspectModelFile ) { + return aspectModelFile.sourceLocation().equals( fileWithOriginalDefinition.sourceLocation() ) + ? new ModelChanges( definition, ModelFactory.createDefaultModel(), + "Add back definition of " + elementUrn ) + : ModelChanges.NONE; + } + + @Override + public Change reverse() { + return RemoveElementDefinition.this; + } + }; + } +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/RenameElement.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/RenameElement.java new file mode 100644 index 000000000..3fcd92d40 --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/RenameElement.java @@ -0,0 +1,39 @@ +/* + * 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.edit.change; + +import org.eclipse.esmf.aspectmodel.edit.ModelChangeException; +import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn; +import org.eclipse.esmf.metamodel.ModelElement; + +/** + * Refactoring operation: Renames a model element, i.e., its local name but not its namespace + */ +public class RenameElement extends RenameUrn { + public RenameElement( final ModelElement modelElement, final String newName ) { + this( modelElement.urn(), newName ); + if ( modelElement.isAnonymous() ) { + throw new ModelChangeException( "Can not rename anonymous model element" ); + } + } + + public RenameElement( final AspectModelUrn urn, final String newName ) { + super( urn, AspectModelUrn.fromUrn( urn.getUrnPrefix() + newName ) ); + } + + @Override + protected String changeDescription() { + return "Rename " + from() + " to " + to().getName(); + } +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/RenameUrn.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/RenameUrn.java new file mode 100644 index 000000000..a909cd901 --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/RenameUrn.java @@ -0,0 +1,116 @@ +/* + * 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.edit.change; + +import org.eclipse.esmf.aspectmodel.AspectModelFile; +import org.eclipse.esmf.aspectmodel.edit.Change; +import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.Statement; +import org.apache.jena.rdf.model.StmtIterator; + +/** + * RDF-level refactoring operation: Renames all occurances of a URN to something else. For model-level refactoring, instead of this class, + * please use {@link RenameElement}, {@link MoveElementToOtherNamespaceExistingFile} or {@link MoveElementToOtherNamespaceNewFile}. + */ +public class RenameUrn extends EditAspectModel { + private final AspectModelUrn from; + private final AspectModelUrn to; + + public RenameUrn( final AspectModelUrn from, final AspectModelUrn to ) { + this.from = from; + this.to = to; + } + + public AspectModelUrn from() { + return from; + } + + public AspectModelUrn to() { + return to; + } + + @Override + protected ModelChanges calculateChangesForFile( final AspectModelFile aspectModelFile ) { + final Model addModel = ModelFactory.createDefaultModel(); + final Model removeModel = ModelFactory.createDefaultModel(); + + int updatedTriples = 0; + for ( final StmtIterator it = aspectModelFile.sourceModel().listStatements(); it.hasNext(); ) { + final Statement statement = it.next(); + boolean updateTriple = false; + final Resource addSubject; + final Resource removeSubject; + final Property predicate; + final RDFNode addObject; + final RDFNode removeObject; + if ( statement.getSubject().isURIResource() ) { + if ( statement.getSubject().getURI().equals( from.toString() ) ) { + addSubject = addModel.createResource( to.toString() ); + removeSubject = removeModel.createResource( from.toString() ); + updateTriple = true; + } else { + addSubject = statement.getSubject(); + removeSubject = statement.getSubject(); + } + } else { + addSubject = addModel.createResource( statement.getSubject().getId() ); + removeSubject = removeModel.createResource( statement.getSubject().getId() ); + } + + if ( statement.getPredicate().getURI().equals( from.toString() ) ) { + predicate = addModel.createProperty( to.toString() ); + updateTriple = true; + } else { + predicate = statement.getPredicate(); + } + if ( statement.getObject().isURIResource() && statement.getObject().asResource().getURI().equals( from.toString() ) ) { + addObject = addModel.createResource( to.toString() ); + removeObject = removeModel.createResource( from.toString() ); + updateTriple = true; + } else { + if ( statement.getObject().isAnon() ) { + addObject = addModel.createResource( statement.getObject().asResource().getId() ); + removeObject = removeModel.createResource( statement.getObject().asResource().getId() ); + } else { + addObject = statement.getObject(); + removeObject = statement.getObject(); + } + } + if ( updateTriple ) { + addModel.add( addSubject, predicate, addObject ); + removeModel.add( removeSubject, predicate, removeObject ); + updatedTriples++; + } + } + + return updatedTriples > 0 + ? new ModelChanges( addModel, removeModel, changeDescription() ) + : ModelChanges.NONE; + } + + protected String changeDescription() { + return "Rename " + from() + " to " + to(); + } + + @Override + public Change reverse() { + return new RenameUrn( to, from ); + } +} diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/StructuralChange.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/StructuralChange.java new file mode 100644 index 000000000..aa568b8b5 --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/change/StructuralChange.java @@ -0,0 +1,37 @@ +/* + * 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.edit.change; + +import org.eclipse.esmf.aspectmodel.AspectModelFile; +import org.eclipse.esmf.aspectmodel.edit.ChangeContext; +import org.eclipse.esmf.aspectmodel.edit.ModelChangeException; +import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn; + +import com.google.common.collect.Streams; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDF; + +public abstract class StructuralChange extends AbstractChange { + protected AspectModelFile sourceFile( final ChangeContext changeContext, final AspectModelUrn elementUrn ) { + return changeContext.aspectModelFiles() + .filter( aspectModelFile -> { + final Resource elementResource = aspectModelFile.sourceModel().createResource( elementUrn.toString() ); + return Streams.stream( aspectModelFile.sourceModel().listStatements( elementResource, RDF.type, (RDFNode) null ) ) + .count() == 1; + } ) + .findFirst() + .orElseThrow( () -> new ModelChangeException( "Could not locate file containing definition of " + elementUrn ) ); + } +} 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 f48435df5..323202c06 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 @@ -13,8 +13,6 @@ package org.eclipse.esmf.aspectmodel.loader; -import static java.util.stream.Collectors.toSet; - import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; @@ -22,23 +20,25 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.net.URI; import java.nio.file.Path; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Deque; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.function.Function; import java.util.function.Supplier; -import java.util.stream.Stream; +import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.eclipse.esmf.aspectmodel.AspectModelFile; +import org.eclipse.esmf.aspectmodel.RdfUtil; import org.eclipse.esmf.aspectmodel.resolver.AspectModelFileLoader; import org.eclipse.esmf.aspectmodel.resolver.EitherStrategy; import org.eclipse.esmf.aspectmodel.resolver.FileSystemStrategy; @@ -48,13 +48,16 @@ import org.eclipse.esmf.aspectmodel.resolver.fs.FlatModelsRoot; import org.eclipse.esmf.aspectmodel.resolver.modelfile.DefaultAspectModelFile; import org.eclipse.esmf.aspectmodel.resolver.modelfile.MetaModelFile; +import org.eclipse.esmf.aspectmodel.resolver.modelfile.RawAspectModelFile; import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn; import org.eclipse.esmf.aspectmodel.urn.ElementType; import org.eclipse.esmf.aspectmodel.urn.UrnSyntaxException; import org.eclipse.esmf.aspectmodel.versionupdate.MetaModelVersionMigrator; import org.eclipse.esmf.metamodel.AspectModel; import org.eclipse.esmf.metamodel.ModelElement; +import org.eclipse.esmf.metamodel.Namespace; import org.eclipse.esmf.metamodel.impl.DefaultAspectModel; +import org.eclipse.esmf.metamodel.impl.DefaultNamespace; import org.eclipse.esmf.metamodel.vocabulary.SammNs; import com.google.common.collect.Streams; @@ -139,7 +142,7 @@ public AspectModel load( final Collection files ) { .toList(); final LoaderContext loaderContext = new LoaderContext(); resolve( migratedFiles, loaderContext ); - return buildAspectModel( loaderContext.loadedFiles() ); + return loadAspectModelFiles( loaderContext.loadedFiles() ); } /** @@ -164,30 +167,36 @@ public AspectModel loadUrns( final Collection urns ) { loaderContext.unresolvedUrns().add( inputUrn.toString() ); } resolve( List.of(), loaderContext ); - return buildAspectModel( loaderContext.loadedFiles() ); + return loadAspectModelFiles( loaderContext.loadedFiles() ); } /** - * Load an Aspect Model from an input stream + * Load an Aspect Model from an input stream and optionally set the source location for this input * * @param inputStream the input stream + * @param sourceLocation the source location for the model * @return the Aspect Model */ - public AspectModel load( final InputStream inputStream ) { - final AspectModelFile rawFile = AspectModelFileLoader.load( inputStream ); + public AspectModel load( final InputStream inputStream, final Optional sourceLocation ) { + final AspectModelFile rawFile = AspectModelFileLoader.load( inputStream, sourceLocation ); final AspectModelFile migratedModel = migrate( rawFile ); final LoaderContext loaderContext = new LoaderContext(); resolve( List.of( migratedModel ), loaderContext ); - return buildAspectModel( loaderContext.loadedFiles() ); + return loadAspectModelFiles( loaderContext.loadedFiles() ); } - @FunctionalInterface - public interface InputStreamProvider { - InputStream get() throws IOException; + /** + * Load an Aspect Model from an input stream + * + * @param inputStream the input stream + * @return the Aspect Model + */ + public AspectModel load( final InputStream inputStream ) { + return load( inputStream, Optional.empty() ); } /** - * Load Namespace Package (Archive) an Aspect Model from a File + * Load a Namespace Package (Archive) from a File * * @param namespacePackage the archive file * @return the Aspect Model @@ -197,16 +206,16 @@ public AspectModel loadNamespacePackage( final File namespacePackage ) { throw new RuntimeException( new FileNotFoundException( "The specified file does not exist or is not a file." ) ); } - try ( InputStream inputStream = new FileInputStream( namespacePackage ) ) { + try ( final InputStream inputStream = new FileInputStream( namespacePackage ) ) { return loadNamespacePackage( inputStream ); - } catch ( IOException e ) { + } catch ( final IOException e ) { LOG.error( "Error reading the file: {}", namespacePackage.getAbsolutePath(), e ); throw new RuntimeException( "Error reading the file: " + namespacePackage.getAbsolutePath(), e ); } } /** - * Load Namespace Package (Archive) an Aspect Model from an InputStream + * Load a Namespace Package (Archive) from an InputStream * * @param inputStream the input stream * @return the Aspect Model @@ -215,7 +224,7 @@ public AspectModel loadNamespacePackage( final InputStream inputStream ) { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { inputStream.transferTo( baos ); - } catch ( IOException e ) { + } catch ( final IOException e ) { throw new RuntimeException( e ); } final boolean hasAspectModelsFolder = containsFolderInNamespacePackage( new ByteArrayInputStream( baos.toByteArray() ) ); @@ -223,43 +232,43 @@ public AspectModel loadNamespacePackage( final InputStream inputStream ) { } private AspectModel loadNamespacePackageFromStream( final InputStream inputStream, final boolean hasAspectModelsFolder ) { - List aspectModelFiles = new ArrayList<>(); + final List aspectModelFiles = new ArrayList<>(); - try ( ZipInputStream zis = new ZipInputStream( inputStream ) ) { + try ( final ZipInputStream zis = new ZipInputStream( inputStream ) ) { ZipEntry entry; - while ( (entry = zis.getNextEntry()) != null ) { - boolean isRelevantEntry = - (hasAspectModelsFolder && entry.getName().contains( String.format( "%s/", ASPECT_MODELS_FOLDER ) ) && entry.getName() - .endsWith( ".ttl" )) - || (!hasAspectModelsFolder && entry.getName().endsWith( ".ttl" )); + while ( ( entry = zis.getNextEntry() ) != null ) { + final boolean isRelevantEntry = + ( hasAspectModelsFolder && entry.getName().contains( String.format( "%s/", ASPECT_MODELS_FOLDER ) ) && entry.getName() + .endsWith( ".ttl" ) ) + || ( !hasAspectModelsFolder && entry.getName().endsWith( ".ttl" ) ); if ( isRelevantEntry ) { - AspectModelFile aspectModelFile = migrate( AspectModelFileLoader.load( zis ) ); + final AspectModelFile aspectModelFile = migrate( AspectModelFileLoader.load( zis ) ); aspectModelFiles.add( aspectModelFile ); } } zis.closeEntry(); - } catch ( IOException e ) { + } catch ( final IOException e ) { LOG.error( "Error reading the Archive input stream", e ); throw new RuntimeException( "Error reading the Archive input stream", e ); } - LoaderContext loaderContext = new LoaderContext(); + final LoaderContext loaderContext = new LoaderContext(); resolve( aspectModelFiles, loaderContext ); - return buildAspectModel( loaderContext.loadedFiles() ); + return loadAspectModelFiles( loaderContext.loadedFiles() ); } private boolean containsFolderInNamespacePackage( final InputStream inputStream ) { - try ( ZipInputStream zis = new ZipInputStream( inputStream ) ) { + try ( final ZipInputStream zis = new ZipInputStream( inputStream ) ) { ZipEntry entry; - while ( (entry = zis.getNextEntry()) != null ) { + while ( ( entry = zis.getNextEntry() ) != null ) { if ( entry.isDirectory() && entry.getName().contains( String.format( "%s/", ASPECT_MODELS_FOLDER ) ) ) { return true; } } - } catch ( IOException e ) { + } catch ( final IOException e ) { throw new RuntimeException( e ); } return false; @@ -280,23 +289,6 @@ private LoaderContext() { } } - private Set getAllUrnsInModel( final Model model ) { - return Streams.stream( model.listStatements().mapWith( statement -> { - final Stream subjectUri = statement.getSubject().isURIResource() - ? Stream.of( statement.getSubject().getURI() ) - : Stream.empty(); - final Stream propertyUri = Stream.of( statement.getPredicate().getURI() ); - final Stream objectUri = statement.getObject().isURIResource() - ? Stream.of( statement.getObject().asResource().getURI() ) - : Stream.empty(); - - return Stream.of( subjectUri, propertyUri, objectUri ) - .flatMap( Function.identity() ) - .flatMap( urn -> AspectModelUrn.from( urn ).toJavaOptional().stream() ) - .map( AspectModelUrn::toString ); - } ) ).flatMap( Function.identity() ).collect( toSet() ); - } - /** * Adapter that enables the resolver to handle URNs with the legacy "urn:bamm:" prefix. * @@ -359,7 +351,8 @@ private void urnsFromModelNeedResolution( final AspectModelFile modelFile, final .filter( uri -> uri.startsWith( "urn:samm:" ) ) .forEach( urn -> context.resolvedUrns().add( urn ) ); - getAllUrnsInModel( modelFile.sourceModel() ).stream() + RdfUtil.getAllUrnsInModel( modelFile.sourceModel() ).stream() + .map( AspectModelUrn::toString ) .filter( urn -> !context.resolvedUrns().contains( urn ) ) .filter( urn -> !urn.startsWith( XSD.NS ) ) .filter( urn -> !urn.startsWith( RDF.uri ) ) @@ -399,7 +392,44 @@ private void resolve( final List inputFiles, final LoaderContex } } - private AspectModel buildAspectModel( final Collection inputFiles ) { + /** + * Checks if a given model contains the definition of a model element. + * + * @param aspectModelFile the model file + * @param urn the URN of the model element + * @return true if the model contains the definition of the model element + */ + @Override + public boolean containsDefinition( final AspectModelFile aspectModelFile, final AspectModelUrn urn ) { + final Model model = aspectModelFile.sourceModel(); + if ( model.getNsPrefixMap().values().stream().anyMatch( prefixUri -> prefixUri.startsWith( "urn:bamm:" ) ) ) { + final boolean result = model.contains( model.createResource( urn.toString().replace( "urn:samm:", "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; + } + + /** + * Creates a new empty Aspect Model. + * + * @return A new empty Aspect Model + */ + public AspectModel emptyModel() { + return new DefaultAspectModel( new ArrayList<>(), ModelFactory.createDefaultModel(), new ArrayList<>() ); + } + + /** + * Creates a new Aspect Model from a collection of {@link AspectModelFile}s. The AspectModelFiles can be {@link RawAspectModelFile} + * (i.e., not contain {@link ModelElement} instances yet); this method takes care of instantiating the model elements. + * + * @param inputFiles the list of input files + * @return the Aspect Model + */ + public AspectModel loadAspectModelFiles( final Collection inputFiles ) { final Model mergedModel = ModelFactory.createDefaultModel(); mergedModel.add( MetaModelFile.metaModelDefinitions() ); for ( final AspectModelFile file : inputFiles ) { @@ -407,12 +437,16 @@ private AspectModel buildAspectModel( final Collection inputFil } final List elements = new ArrayList<>(); + final List files = new ArrayList<>(); + final Map namespaceDefinitions = new HashMap<>(); for ( final AspectModelFile file : inputFiles ) { final DefaultAspectModelFile aspectModelFile = new DefaultAspectModelFile( file.sourceModel(), file.headerComment(), file.sourceLocation() ); + files.add( aspectModelFile ); final Model model = file.sourceModel(); final ModelElementFactory modelElementFactory = new ModelElementFactory( mergedModel, Map.of(), element -> aspectModelFile ); final List fileElements = model.listStatements( null, RDF.type, (RDFNode) null ).toList().stream() + .filter( statement -> !statement.getObject().isURIResource() || !statement.getResource().equals( SammNs.SAMM.Namespace() ) ) .map( Statement::getSubject ) .filter( RDFNode::isURIResource ) .map( resource -> mergedModel.createResource( resource.getURI() ) ) @@ -421,27 +455,74 @@ private AspectModel buildAspectModel( final Collection inputFil aspectModelFile.setElements( fileElements ); elements.addAll( fileElements ); } - return new DefaultAspectModel( mergedModel, elements ); + + setNamespaces( files, elements ); + return new DefaultAspectModel( files, mergedModel, elements ); } /** - * Checks if a given model contains the definition of a model element. + * Sets up the namespace references in the collection of newly created AspectModelFiles * - * @param aspectModelFile the model file - * @param urn the URN of the model element - * @return true if the model contains the definition of the model element + * @param files the files + * @param elements the collection of all model elements across all files */ - @Override - public boolean containsDefinition( final AspectModelFile aspectModelFile, final AspectModelUrn urn ) { - final Model model = aspectModelFile.sourceModel(); - if ( model.getNsPrefixMap().values().stream().anyMatch( prefixUri -> prefixUri.startsWith( "urn:bamm:" ) ) ) { - final boolean result = model.contains( model.createResource( urn.toString().replace( "urn:samm:", "urn:bamm:" ) ), RDF.type, - (RDFNode) null ); - LOG.debug( "Checking if model contains {}: {}", urn, result ); - return result; + private void setNamespaces( final Collection files, final Collection elements ) { + final Map> elementsGroupedByNamespaceUrn = elements.stream() + .filter( element -> !element.isAnonymous() ) + .collect( Collectors.groupingBy( element -> element.urn().getNamespaceIdentifier() ) ); + for ( final AspectModelFile file : files ) { + final Optional optionalNamespaceUrn = + Optional.ofNullable( file.sourceModel().getNsPrefixURI( "" ) ) + .map( urnPrefix -> urnPrefix.split( "#" )[0] ) + .or( () -> file.elements().stream() + .filter( element -> !element.isAnonymous() ) + .map( element -> element.urn().getNamespaceIdentifier() ) + .findAny() ); + if ( optionalNamespaceUrn.isEmpty() ) { + continue; + } + + final String namespaceUrn = optionalNamespaceUrn.get(); + MetaModelBaseAttributes namespaceDefinition = null; + AspectModelFile fileContainingNamespaceDefinition = null; + final List elementsForUrn = elementsGroupedByNamespaceUrn.get( namespaceUrn ); + if ( elementsForUrn != null ) { + for ( final ModelElement element : elementsForUrn ) { + final AspectModelFile elementFile = element.getSourceFile(); + if ( elementFile.sourceModel().contains( null, RDF.type, SammNs.SAMM.Namespace() ) ) { + final Model model = elementFile.sourceModel(); + final ModelElementFactory modelElementFactory = new ModelElementFactory( model, Map.of(), r -> null ); + final Resource namespaceResource = model.listStatements( null, RDF.type, SammNs.SAMM.Namespace() ) + .mapWith( Statement::getSubject ) + .toList().iterator().next(); + namespaceDefinition = modelElementFactory.createBaseAttributes( namespaceResource ); + fileContainingNamespaceDefinition = elementFile; + break; + } + } + } + + final Namespace namespace = new DefaultNamespace( namespaceUrn, elementsGroupedByNamespaceUrn.get( namespaceUrn ), + Optional.ofNullable( fileContainingNamespaceDefinition ), Optional.ofNullable( namespaceDefinition ) ); + ( (DefaultAspectModelFile) file ).setNamespace( namespace ); } - final boolean result = model.contains( model.createResource( urn.toString() ), RDF.type, (RDFNode) null ); - LOG.debug( "Checking if model contains {}: {}", urn, result ); - return result; + } + + /** + * Creates a new Aspect Model that contains the closure of two input Aspect Models + * + * @param aspectModel1 the first input Aspect Model + * @param aspectModel2 the second input Aspect Model + * @return the merged Aspect Model + */ + public AspectModel merge( final AspectModel aspectModel1, final AspectModel aspectModel2 ) { + final List files = new ArrayList<>( aspectModel1.files() ); + final Set locations = aspectModel1.files().stream() + .flatMap( f -> f.sourceLocation().stream() ) + .collect( Collectors.toSet() ); + for ( final AspectModelFile file : aspectModel2.files() ) { + file.sourceLocation().filter( uri -> !locations.contains( uri ) ).ifPresent( uri -> files.add( file ) ); + } + return loadAspectModelFiles( files ); } } diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/AspectModelFileLoader.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/AspectModelFileLoader.java index 07df2bfa7..6accf3f75 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/AspectModelFileLoader.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/AspectModelFileLoader.java @@ -73,16 +73,23 @@ private static String content( final InputStream inputStream ) { } private static List headerComment( final String content ) { - return content.lines() + final List list = content.lines() .dropWhile( String::isBlank ) .takeWhile( line -> line.startsWith( "#" ) || isBlank( line ) ) .map( line -> line.startsWith( "#" ) ? line.substring( 1 ).trim() : line ) .toList(); + return !list.isEmpty() && list.get( list.size() - 1 ).isEmpty() + ? list.subList( 0, list.size() - 1 ) + : list; } public static RawAspectModelFile load( final InputStream inputStream ) { + return load( inputStream, Optional.empty() ); + } + + public static RawAspectModelFile load( final InputStream inputStream, final Optional sourceLocation ) { final AspectModelFile fromString = load( content( inputStream ) ); - return new RawAspectModelFile( fromString.sourceModel(), fromString.headerComment(), Optional.empty() ); + return new RawAspectModelFile( fromString.sourceModel(), fromString.headerComment(), sourceLocation ); } public static RawAspectModelFile load( final Model model ) { diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/ClasspathStrategy.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/ClasspathStrategy.java index 2da0781b1..3f3798f8e 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/ClasspathStrategy.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/ClasspathStrategy.java @@ -152,7 +152,7 @@ private Stream getFilesFromJar( final String directory, final File jarFi public AspectModelFile apply( final AspectModelUrn aspectModelUrn, final ResolutionStrategySupport resolutionStrategySupport ) { final String modelsRootTrailingSlash = modelsRoot.isEmpty() ? "" : "/"; final String directory = String.format( "%s%s%s/%s", modelsRoot, modelsRootTrailingSlash, - aspectModelUrn.getNamespace(), aspectModelUrn.getVersion() ); + aspectModelUrn.getNamespaceMainPart(), aspectModelUrn.getVersion() ); final URL namedResourceFile = resourceUrl( directory, aspectModelUrn.getName() + ".ttl" ); if ( namedResourceFile != null ) { diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/fs/StructuredModelsRoot.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/fs/StructuredModelsRoot.java index 1e0ec28f7..b21626308 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/fs/StructuredModelsRoot.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/fs/StructuredModelsRoot.java @@ -39,7 +39,7 @@ public StructuredModelsRoot( final Path path ) { @Override public Path directoryForNamespace( final AspectModelUrn urn ) { - return rootPath().resolve( urn.getNamespace() ).resolve( urn.getVersion() ); + return rootPath().resolve( urn.getNamespaceMainPart() ).resolve( urn.getVersion() ); } @Override diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/modelfile/DefaultAspectModelFile.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/modelfile/DefaultAspectModelFile.java index 4e4820cd4..07471d8d4 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/modelfile/DefaultAspectModelFile.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/modelfile/DefaultAspectModelFile.java @@ -20,6 +20,7 @@ import org.eclipse.esmf.aspectmodel.AspectModelFile; import org.eclipse.esmf.metamodel.ModelElement; +import org.eclipse.esmf.metamodel.Namespace; import org.apache.jena.rdf.model.Model; @@ -28,6 +29,7 @@ public final class DefaultAspectModelFile implements AspectModelFile { private final List headerComment; private final Optional sourceLocation; private List elements; + private Namespace namespace = null; public DefaultAspectModelFile( final Model sourceModel, final List headerComment, final Optional sourceLocation ) { this.sourceModel = sourceModel; @@ -61,10 +63,19 @@ public List elements() { return elements; } + @Override + public Namespace namespace() { + return namespace; + } + public void setElements( final List elements ) { this.elements = elements; } + public void setNamespace( final Namespace namespace ) { + this.namespace = namespace; + } + @Override public boolean equals( final Object obj ) { if ( obj == this ) { @@ -87,10 +98,6 @@ public int hashCode() { @Override public String toString() { - return "DefaultAspectModelFile[" - + "sourceModel=" + sourceModel + ", " - + "headerComment=" + headerComment + ", " - + "sourceLocation=" + sourceLocation + ", " - + "elements=" + elements + ']'; + return sourceLocation().map( URI::toString ).orElse( "(unknown file)" ); } } diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/modelfile/RawAspectModelFile.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/modelfile/RawAspectModelFile.java index 4fc7d3076..f9c634d47 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/modelfile/RawAspectModelFile.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/modelfile/RawAspectModelFile.java @@ -19,12 +19,31 @@ import org.eclipse.esmf.aspectmodel.AspectModelFile; +import io.soabase.recordbuilder.core.RecordBuilder; import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; @SuppressWarnings( "OptionalUsedAsFieldOrParameterType" ) +@RecordBuilder public record RawAspectModelFile( Model sourceModel, List headerComment, Optional sourceLocation ) implements AspectModelFile { + public RawAspectModelFile { + if ( sourceModel == null ) { + sourceModel = ModelFactory.createDefaultModel(); + } + if ( headerComment == null ) { + headerComment = List.of(); + } + if ( sourceLocation == null ) { + sourceLocation = Optional.empty(); + } + } + + @Override + public String toString() { + return sourceLocation().map( URI::toString ).orElse( "(unknown file)" ); + } } diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/services/TurtleLoader.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/services/TurtleLoader.java index 42091b4df..1367aa631 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/services/TurtleLoader.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/services/TurtleLoader.java @@ -78,7 +78,7 @@ public static Try loadTurtle( final URL url ) { } /** - * Loads a Turtle model from an input stream + * Loads a Turtle model from a String containing RDF/Turtle * * @param modelContent The model content * @return The model on success, a corresponding exception otherwise diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/metamodel/impl/DefaultAspectModel.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/metamodel/impl/DefaultAspectModel.java index 28cd2eddf..5e0bfe7aa 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/metamodel/impl/DefaultAspectModel.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/metamodel/impl/DefaultAspectModel.java @@ -14,9 +14,9 @@ package org.eclipse.esmf.metamodel.impl; import java.util.List; -import java.util.Optional; import java.util.stream.Collectors; +import org.eclipse.esmf.aspectmodel.AspectModelFile; import org.eclipse.esmf.metamodel.AspectModel; import org.eclipse.esmf.metamodel.ModelElement; import org.eclipse.esmf.metamodel.Namespace; @@ -24,23 +24,27 @@ import org.apache.jena.rdf.model.Model; public class DefaultAspectModel implements AspectModel { - private final Model mergedModel; - private final List elements; + private Model mergedModel; + private List elements; + private List files; - public DefaultAspectModel( final Model mergedModel, final List elements ) { + public DefaultAspectModel( final List files, final Model mergedModel, final List elements ) { + this.files = files; this.mergedModel = mergedModel; this.elements = elements; } @Override public List namespaces() { - return elements().stream() - .filter( element -> !Namespace.ANONYMOUS.equals( element.urn().getUrnPrefix() ) ) - .collect( Collectors.groupingBy( element -> element.urn().getUrnPrefix() ) ) - .entrySet() - .stream() - . map( entry -> new DefaultNamespace( entry.getKey(), entry.getValue(), Optional.empty() ) ) - .toList(); + return files().stream() + .map( AspectModelFile::namespace ) + .collect( Collectors.toSet() ) + .stream().toList(); + } + + @Override + public List files() { + return files; } @Override @@ -52,4 +56,16 @@ public List elements() { public Model mergedModel() { return mergedModel; } + + public void setFiles( final List files ) { + this.files = files; + } + + public void setMergedModel( final Model mergedModel ) { + this.mergedModel = mergedModel; + } + + public void setElements( final List elements ) { + this.elements = elements; + } } diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/metamodel/impl/DefaultNamespace.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/metamodel/impl/DefaultNamespace.java index 63389bff2..c16fadff9 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/metamodel/impl/DefaultNamespace.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/metamodel/impl/DefaultNamespace.java @@ -17,24 +17,40 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.Set; import org.eclipse.esmf.aspectmodel.AspectModelFile; import org.eclipse.esmf.aspectmodel.VersionNumber; +import org.eclipse.esmf.aspectmodel.loader.MetaModelBaseAttributes; +import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn; import org.eclipse.esmf.metamodel.ModelElement; import org.eclipse.esmf.metamodel.Namespace; +import org.eclipse.esmf.metamodel.datatype.LangString; public class DefaultNamespace implements Namespace { + private final Optional baseAttributes; private final String packagePart; private final VersionNumber versionNumber; private final List elements; private final Optional source; - public DefaultNamespace( final String packagePart, final VersionNumber versionNumber, final List elements, + public DefaultNamespace( final AspectModelUrn aspectModelUrn, final List elements, final Optional source ) { + this( aspectModelUrn, elements, source, Optional.empty() ); + } + + public DefaultNamespace( final AspectModelUrn aspectModelUrn, final List elements, + final Optional source, final Optional baseAttributes ) { + this( aspectModelUrn.getNamespaceMainPart(), VersionNumber.parse( aspectModelUrn.getVersion() ), elements, source, baseAttributes ); + } + + public DefaultNamespace( final String packagePart, final VersionNumber versionNumber, final List elements, + final Optional source, final Optional baseAttributes ) { this.packagePart = packagePart; this.versionNumber = versionNumber; this.source = source; this.elements = elements; + this.baseAttributes = baseAttributes; } /** @@ -43,8 +59,9 @@ public DefaultNamespace( final String packagePart, final VersionNumber versionNu * @param uri the namespace uri * @param elements the list of elements in the namspace */ - public DefaultNamespace( final String uri, final List elements, final Optional source ) { - this( uri.split( ":" )[2], VersionNumber.parse( uri.split( ":" )[3].replace( "#", "" ) ), elements, source ); + public DefaultNamespace( final String uri, final List elements, final Optional source, + final Optional baseAttributes ) { + this( uri.split( ":" )[2], VersionNumber.parse( uri.split( ":" )[3].replace( "#", "" ) ), elements, source, baseAttributes ); } // /** @@ -82,6 +99,21 @@ public String getName() { return "urn:samm:%s:%s".formatted( packagePart, versionNumber ); } + @Override + public List getSee() { + return baseAttributes.map( MetaModelBaseAttributes::getSee ).orElseGet( Namespace.super::getSee ); + } + + @Override + public Set getPreferredNames() { + return baseAttributes.map( MetaModelBaseAttributes::getPreferredNames ).orElseGet( Namespace.super::getPreferredNames ); + } + + @Override + public Set getDescriptions() { + return baseAttributes.map( MetaModelBaseAttributes::getDescriptions ).orElseGet( Namespace.super::getDescriptions ); + } + @Override public boolean equals( final Object o ) { if ( this == o ) { diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/edit/AspectChangeManagerTest.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/edit/AspectChangeManagerTest.java new file mode 100644 index 000000000..c76fc1add --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/edit/AspectChangeManagerTest.java @@ -0,0 +1,428 @@ +/* + * 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.edit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.esmf.aspectmodel.RdfUtil.createModel; + +import java.net.URI; +import java.util.List; +import java.util.Optional; + +import org.eclipse.esmf.aspectmodel.AspectModelFile; +import org.eclipse.esmf.aspectmodel.edit.change.AddAspectModelFile; +import org.eclipse.esmf.aspectmodel.edit.change.AddElementDefinition; +import org.eclipse.esmf.aspectmodel.edit.change.MoveElementToExistingFile; +import org.eclipse.esmf.aspectmodel.edit.change.MoveElementToNewFile; +import org.eclipse.esmf.aspectmodel.edit.change.MoveElementToOtherNamespaceExistingFile; +import org.eclipse.esmf.aspectmodel.edit.change.MoveElementToOtherNamespaceNewFile; +import org.eclipse.esmf.aspectmodel.edit.change.MoveRenameAspectModelFile; +import org.eclipse.esmf.aspectmodel.edit.change.RemoveAspectModelFile; +import org.eclipse.esmf.aspectmodel.edit.change.RenameElement; +import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader; +import org.eclipse.esmf.aspectmodel.resolver.modelfile.RawAspectModelFileBuilder; +import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn; +import org.eclipse.esmf.metamodel.Aspect; +import org.eclipse.esmf.metamodel.AspectModel; +import org.eclipse.esmf.metamodel.Namespace; +import org.eclipse.esmf.metamodel.Property; +import org.eclipse.esmf.metamodel.impl.DefaultNamespace; +import org.eclipse.esmf.metamodel.vocabulary.SammNs; +import org.eclipse.esmf.test.TestAspect; +import org.eclipse.esmf.test.TestResources; + +import org.apache.jena.vocabulary.RDF; +import org.junit.jupiter.api.Test; + +public class AspectChangeManagerTest { + @Test + void testRenameElement() { + final AspectModel aspectModel = TestResources.load( TestAspect.ASPECT ); + final Aspect aspect = aspectModel.aspect(); + + final String originalName = aspect.getName(); + assertThat( aspectModel.aspect().urn().getName() ).isEqualTo( originalName ); + + final String newName = "RenamedAspect"; + final AspectChangeManager changeManager = new AspectChangeManager( aspectModel ); + final Change renameAspect = new RenameElement( aspect, newName ); + changeManager.applyChange( renameAspect ); + assertThat( aspectModel.aspect().getName() ).isEqualTo( newName ); + assertThat( aspectModel.files().get( 0 ).sourceModel().listStatements( null, RDF.type, SammNs.SAMM.Aspect() ) + .nextStatement().getSubject().getURI() ).endsWith( newName ); + assertThat( changeManager.modifiedFiles() ).hasSize( 1 ); + + changeManager.undoChange(); + assertThat( changeManager.modifiedFiles() ).hasSize( 1 ); + assertThat( aspectModel.aspect().getName() ).isEqualTo( originalName ); + changeManager.redoChange(); + assertThat( changeManager.modifiedFiles() ).hasSize( 1 ); + assertThat( aspectModel.aspect().getName() ).isEqualTo( newName ); + } + + @Test + void testUndoRedo() { + final AspectModel aspectModel = TestResources.load( TestAspect.ASPECT ); + final Aspect aspect = aspectModel.aspect(); + + final AspectModelUrn aspectUrn = aspect.urn(); + final String originalName = aspect.getName(); + assertThat( aspectModel.aspect().urn().getName() ).isEqualTo( originalName ); + + final String newName = "RenamedAspect"; + final AspectChangeManager changeManager = new AspectChangeManager( aspectModel ); + final Change renameAspect = new RenameElement( aspectUrn, newName ); + changeManager.applyChange( renameAspect ); + assertThat( aspectModel.aspect().getName() ).isEqualTo( newName ); + changeManager.undoChange(); + assertThat( aspectModel.aspect().getName() ).isEqualTo( originalName ); + changeManager.redoChange(); + assertThat( aspectModel.aspect().getName() ).isEqualTo( newName ); + } + + @Test + void testChangeGroups() { + final AspectModel aspectModel = TestResources.load( TestAspect.ASPECT_WITH_PROPERTY ); + final Aspect aspect = aspectModel.aspect(); + + final AspectModelUrn aspectUrn = aspect.urn(); + final Property property = aspect.getProperties().get( 0 ); + + final String newAspectName = "RenamedAspect"; + final String newPropertyName = "renamedProperty"; + final AspectChangeManager changeManager = new AspectChangeManager( aspectModel ); + final Change renameAspect = new RenameElement( aspectUrn, newAspectName ); + final Change renameProperty = new RenameElement( property.urn(), newPropertyName ); + + final Change group = new ChangeGroup( renameAspect, renameProperty ); + changeManager.applyChange( group ); + assertThat( aspectModel.aspect().urn().getName() ).isEqualTo( newAspectName ); + assertThat( aspectModel.aspect().getProperties().get( 0 ).getName() ).isEqualTo( newPropertyName ); + } + + @Test + void testCreateFile() { + final AspectModel aspectModel = new AspectModelLoader().emptyModel(); + assertThat( aspectModel.files() ).isEmpty(); + assertThat( aspectModel.elements() ).isEmpty(); + final AspectChangeManager changeManager = new AspectChangeManager( aspectModel ); + final AspectModelFile aspectModelFile = RawAspectModelFileBuilder.builder() + .sourceLocation( Optional.of( URI.create( "file:///temp/test.ttl" ) ) ) + .build(); + final Change addFile = new AddAspectModelFile( aspectModelFile ); + changeManager.applyChange( addFile ); + assertThat( changeManager.createdFiles() ).hasSize( 1 ); + assertThat( aspectModel.files() ).hasSize( 1 ); + changeManager.undoChange(); + assertThat( changeManager.createdFiles() ).isEmpty(); + assertThat( changeManager.removedFiles() ).hasSize( 1 ); + assertThat( aspectModel.files() ).isEmpty(); + changeManager.redoChange(); + assertThat( changeManager.createdFiles() ).hasSize( 1 ); + assertThat( aspectModel.files() ).hasSize( 1 ); + } + + @Test + void testRemoveFile() { + final AspectModelFile aspectModelFile = RawAspectModelFileBuilder.builder() + .sourceLocation( Optional.of( URI.create( "file:///temp/test.ttl" ) ) ) + .build(); + final AspectModel aspectModel = new AspectModelLoader().loadAspectModelFiles( List.of( aspectModelFile ) ); + assertThat( aspectModel.files() ).hasSize( 1 ); + assertThat( aspectModel.elements() ).isEmpty(); + final AspectChangeManager changeManager = new AspectChangeManager( aspectModel ); + changeManager.applyChange( new RemoveAspectModelFile( aspectModel.files().get( 0 ) ) ); + assertThat( changeManager.removedFiles() ).hasSize( 1 ); + assertThat( aspectModel.files() ).isEmpty(); + changeManager.undoChange(); + assertThat( changeManager.removedFiles() ).isEmpty(); + assertThat( aspectModel.files() ).hasSize( 1 ); + changeManager.redoChange(); + assertThat( changeManager.removedFiles() ).hasSize( 1 ); + assertThat( aspectModel.files() ).isEmpty(); + } + + @Test + void testCreateFileWithElementDefinition() { + final AspectModel aspectModel = new AspectModelLoader().emptyModel(); + assertThat( aspectModel.files() ).isEmpty(); + assertThat( aspectModel.elements() ).isEmpty(); + final AspectChangeManager changeManager = new AspectChangeManager( aspectModel ); + final AspectModelFile aspectModelFile = RawAspectModelFileBuilder.builder() + .sourceLocation( Optional.of( URI.create( "file:///temp/test.ttl" ) ) ) + .sourceModel( createModel( """ + @prefix samm: . + @prefix xsd: . + @prefix : . + + :Aspect a samm:Aspect ; + samm:description "This is a test description"@en ; + samm:properties ( ) ; + samm:operations ( ) . + """ + ) ) + .build(); + final Change addFile = new AddAspectModelFile( aspectModelFile ); + changeManager.applyChange( addFile ); + assertThat( changeManager.createdFiles() ).hasSize( 1 ); + assertThat( aspectModel.files() ).hasSize( 1 ); + assertThat( aspectModel.aspects() ).hasSize( 1 ); + assertThat( aspectModel.aspect().getName() ).isEqualTo( "Aspect" ); + changeManager.undoChange(); + assertThat( changeManager.createdFiles() ).isEmpty(); + assertThat( aspectModel.files() ).isEmpty(); + changeManager.redoChange(); + assertThat( changeManager.createdFiles() ).hasSize( 1 ); + assertThat( aspectModel.files() ).hasSize( 1 ); + } + + @Test + void testCreateFileThenAddElementDefinition() { + final AspectModel aspectModel = new AspectModelLoader().emptyModel(); + assertThat( aspectModel.files() ).isEmpty(); + assertThat( aspectModel.elements() ).isEmpty(); + final AspectChangeManager changeManager = new AspectChangeManager( aspectModel ); + final AspectModelFile aspectModelFile = RawAspectModelFileBuilder.builder() + .sourceLocation( Optional.of( URI.create( "file:///temp/test.ttl" ) ) ) + .build(); + + final Change changes = new ChangeGroup( + new AddAspectModelFile( aspectModelFile ), + new AddElementDefinition( AspectModelUrn.fromUrn( "urn:samm:org.eclipse.esmf.test:1.0.0#Aspect" ), + createModel( """ + @prefix samm: . + @prefix xsd: . + @prefix : . + + :Aspect a samm:Aspect ; + samm:description "This is a test description"@en ; + samm:properties ( ) ; + samm:operations ( ) . + """ ), aspectModelFile ) + ); + + changeManager.applyChange( changes ); + assertThat( changeManager.createdFiles() ).hasSize( 1 ); + assertThat( aspectModel.files() ).hasSize( 1 ); + assertThat( aspectModel.aspects() ).hasSize( 1 ); + assertThat( aspectModel.aspect().getName() ).isEqualTo( "Aspect" ); + changeManager.undoChange(); + assertThat( changeManager.createdFiles() ).isEmpty(); + assertThat( changeManager.removedFiles() ).hasSize( 1 ); + assertThat( aspectModel.files() ).isEmpty(); + changeManager.redoChange(); + assertThat( changeManager.createdFiles() ).hasSize( 1 ); + assertThat( aspectModel.files() ).hasSize( 1 ); + } + + @Test + void testMoveElementToNewFile() { + final AspectModel aspectModel = TestResources.load( TestAspect.ASPECT ); + assertThat( aspectModel.files() ).hasSize( 1 ); + final URI originalSourceLocation = aspectModel.aspect().getSourceFile().sourceLocation().get(); + final AspectChangeManager changeManager = new AspectChangeManager( aspectModel ); + final URI sourceLocation = URI.create( "file:///temp/test.ttl" ); + final Change move = new MoveElementToNewFile( aspectModel.aspect(), Optional.of( sourceLocation ) ); + + changeManager.applyChange( move ); + assertThat( changeManager.createdFiles() ).hasSize( 1 ); + assertThat( changeManager.modifiedFiles() ).hasSize( 1 ); + assertThat( aspectModel.files() ).hasSize( 2 ); + assertThat( aspectModel.aspect().getSourceFile().sourceLocation() ).contains( sourceLocation ); + assertThat( aspectModel.aspect().getSourceFile().sourceModel().listStatements( null, RDF.type, SammNs.SAMM.Aspect() ).nextStatement() + .getSubject().getURI() ).isEqualTo( aspectModel.aspect().urn().toString() ); + + changeManager.undoChange(); + assertThat( changeManager.createdFiles() ).isEmpty(); + assertThat( changeManager.removedFiles() ).hasSize( 1 ); + assertThat( changeManager.modifiedFiles() ).hasSize( 1 ); + assertThat( aspectModel.files() ).hasSize( 1 ); + assertThat( aspectModel.aspect().getSourceFile().sourceLocation() ).contains( originalSourceLocation ); + assertThat( aspectModel.aspect().getSourceFile().sourceModel().listStatements( null, RDF.type, SammNs.SAMM.Aspect() ).nextStatement() + .getSubject().getURI() ).isEqualTo( aspectModel.aspect().urn().toString() ); + + changeManager.redoChange(); + assertThat( changeManager.createdFiles() ).hasSize( 1 ); + assertThat( changeManager.modifiedFiles() ).hasSize( 1 ); + assertThat( aspectModel.files() ).hasSize( 2 ); + assertThat( aspectModel.aspect().getSourceFile().sourceLocation() ).contains( sourceLocation ); + assertThat( aspectModel.aspect().getSourceFile().sourceModel().listStatements( null, RDF.type, SammNs.SAMM.Aspect() ).nextStatement() + .getSubject().getURI() ).isEqualTo( aspectModel.aspect().urn().toString() ); + } + + @Test + void testMoveElementToExistingFile() { + final Optional file1Location = Optional.of( URI.create( "file:///file1.ttl" ) ); + final Optional file2Location = Optional.of( URI.create( "file:///file2.ttl" ) ); + final AspectModel aspectModel = new AspectModelLoader().emptyModel(); + final AspectChangeManager changeManager = new AspectChangeManager( aspectModel ); + final AspectModelFile file1 = RawAspectModelFileBuilder.builder() + .sourceLocation( file1Location ) + .sourceModel( createModel( """ + @prefix samm: . + @prefix xsd: . + @prefix : . + + :Aspect a samm:Aspect ; + samm:description "This is a test description"@en ; + samm:properties ( ) ; + samm:operations ( ) . + """ + ) ) + .build(); + final AspectModelFile file2 = RawAspectModelFileBuilder.builder().sourceLocation( file2Location ).build(); + + changeManager.applyChange( new ChangeGroup( + new AddAspectModelFile( file1 ), + new AddAspectModelFile( file2 ) + ) ); + assertThat( aspectModel.aspect().getSourceFile().sourceLocation() ).isEqualTo( file1Location ); + + final Change move = new MoveElementToExistingFile( aspectModel.aspect(), file2 ); + changeManager.applyChange( move ); + assertThat( changeManager.modifiedFiles() ).hasSize( 2 ); + assertThat( changeManager.createdFiles() ).isEmpty(); + assertThat( changeManager.removedFiles() ).isEmpty(); + + assertThat( aspectModel.aspect().getSourceFile().sourceLocation() ).isEqualTo( file2Location ); + changeManager.undoChange(); + assertThat( changeManager.modifiedFiles() ).hasSize( 2 ); + assertThat( changeManager.createdFiles() ).isEmpty(); + assertThat( changeManager.removedFiles() ).isEmpty(); + assertThat( aspectModel.aspect().getSourceFile().sourceLocation() ).isEqualTo( file1Location ); + } + + @Test + void testMoveElementToOtherNamespaceNewFile() { + final AspectModel aspectModel = TestResources.load( TestAspect.ASPECT ); + assertThat( aspectModel.files() ).hasSize( 1 ); + final URI originalSourceLocation = aspectModel.aspect().getSourceFile().sourceLocation().get(); + final AspectChangeManager changeManager = new AspectChangeManager( aspectModel ); + final URI sourceLocation = URI.create( "file:///temp/test.ttl" ); + + final AspectModelUrn targetUrn = AspectModelUrn.fromUrn( "urn:samm:org.eclipse.esmf.example.new:1.0.0#Aspect" ); + final Namespace targetNamespace = new DefaultNamespace( targetUrn, List.of(), Optional.empty() ); + final Change move = new MoveElementToOtherNamespaceNewFile( aspectModel.aspect(), targetNamespace, Optional.of( sourceLocation ) ); + + changeManager.applyChange( move ); + assertThat( changeManager.modifiedFiles() ).hasSize( 1 ); + assertThat( changeManager.createdFiles() ).hasSize( 1 ); + assertThat( aspectModel.files() ).hasSize( 2 ); + assertThat( aspectModel.aspect().getSourceFile().sourceLocation() ).contains( sourceLocation ); + assertThat( aspectModel.aspect().getSourceFile().sourceModel().listStatements( null, RDF.type, SammNs.SAMM.Aspect() ).nextStatement() + .getSubject().getURI() ).isEqualTo( aspectModel.aspect().urn().toString() ); + assertThat( aspectModel.aspect().urn() ).isEqualTo( targetUrn ); + + changeManager.undoChange(); + assertThat( changeManager.modifiedFiles() ).hasSize( 1 ); + assertThat( changeManager.removedFiles() ).hasSize( 1 ); + assertThat( changeManager.createdFiles() ).isEmpty(); + assertThat( aspectModel.files() ).hasSize( 1 ); + assertThat( aspectModel.aspect().getSourceFile().sourceLocation() ).contains( originalSourceLocation ); + assertThat( aspectModel.aspect().getSourceFile().sourceModel().listStatements( null, RDF.type, SammNs.SAMM.Aspect() ).nextStatement() + .getSubject().getURI() ).isEqualTo( aspectModel.aspect().urn().toString() ); + + changeManager.redoChange(); + assertThat( changeManager.modifiedFiles() ).hasSize( 1 ); + assertThat( changeManager.createdFiles() ).hasSize( 1 ); + assertThat( aspectModel.files() ).hasSize( 2 ); + assertThat( aspectModel.aspect().getSourceFile().sourceLocation() ).contains( sourceLocation ); + assertThat( aspectModel.aspect().getSourceFile().sourceModel().listStatements( null, RDF.type, SammNs.SAMM.Aspect() ).nextStatement() + .getSubject().getURI() ).isEqualTo( aspectModel.aspect().urn().toString() ); + } + + @Test + void testMoveElementToOtherNamespaceExistingFile() { + final Optional file1Location = Optional.of( URI.create( "file:///file1.ttl" ) ); + final Optional file2Location = Optional.of( URI.create( "file:///file2.ttl" ) ); + final AspectModel aspectModel = new AspectModelLoader().emptyModel(); + final AspectChangeManager changeManager = new AspectChangeManager( aspectModel ); + final AspectModelFile file1 = RawAspectModelFileBuilder.builder() + .sourceLocation( file1Location ) + .sourceModel( createModel( """ + @prefix samm: . + @prefix xsd: . + @prefix : . + + :Aspect a samm:Aspect ; + samm:description "This is a test description"@en ; + samm:properties ( ) ; + samm:operations ( ) . + """ + ) ) + .build(); + final AspectModelFile file2 = RawAspectModelFileBuilder.builder().sourceLocation( file2Location ).build(); + + changeManager.applyChange( new ChangeGroup( + new AddAspectModelFile( file1 ), + new AddAspectModelFile( file2 ) + ) ); + changeManager.resetFileStates(); + assertThat( aspectModel.aspect().getSourceFile().sourceLocation() ).isEqualTo( file1Location ); + + final AspectModelUrn targetUrn = AspectModelUrn.fromUrn( "urn:samm:org.eclipse.esmf.example.new:1.0.0#Aspect" ); + final Namespace targetNamespace = new DefaultNamespace( targetUrn, List.of(), Optional.empty() ); + final Change move = new MoveElementToOtherNamespaceExistingFile( aspectModel.aspect(), file2, targetNamespace ); + + changeManager.applyChange( move ); + assertThat( changeManager.modifiedFiles() ).hasSize( 2 ); + assertThat( changeManager.createdFiles() ).isEmpty(); + + assertThat( aspectModel.aspect().getSourceFile().sourceLocation() ).isEqualTo( file2Location ); + assertThat( aspectModel.aspect().getSourceFile().sourceModel().listStatements( null, RDF.type, SammNs.SAMM.Aspect() ).nextStatement() + .getSubject().getURI() ).isEqualTo( aspectModel.aspect().urn().toString() ); + assertThat( aspectModel.aspect().urn() ).isEqualTo( targetUrn ); + + changeManager.undoChange(); + assertThat( changeManager.modifiedFiles() ).hasSize( 2 ); + assertThat( changeManager.createdFiles() ).isEmpty(); + assertThat( aspectModel.aspect().getSourceFile().sourceLocation() ).isEqualTo( file1Location ); + assertThat( aspectModel.aspect().getSourceFile().sourceModel().listStatements( null, RDF.type, SammNs.SAMM.Aspect() ).nextStatement() + .getSubject().getURI() ).isEqualTo( aspectModel.aspect().urn().toString() ); + } + + @Test + void testMoveRenameFile() { + final AspectModel aspectModel = TestResources.load( TestAspect.ASPECT ); + final Aspect aspect = aspectModel.aspect(); + + assertThat( aspectModel.files() ).hasSize( 1 ); + + final URI originalLocation = aspect.getSourceFile().sourceLocation().get(); + final AspectChangeManager changeManager = new AspectChangeManager( aspectModel ); + final URI newLocation = URI.create( "file:///temp/test.ttl" ); + final Change renameFile = new MoveRenameAspectModelFile( aspect.getSourceFile(), newLocation ); + + changeManager.applyChange( renameFile ); + assertThat( changeManager.modifiedFiles() ).isEmpty(); + assertThat( changeManager.createdFiles() ).hasSize( 1 ); + assertThat( changeManager.removedFiles() ).hasSize( 1 ); + assertThat( aspectModel.files() ).hasSize( 1 ); + assertThat( aspectModel.aspect().getSourceFile().sourceLocation() ).contains( newLocation ); + + changeManager.undoChange(); + assertThat( changeManager.modifiedFiles() ).isEmpty(); + assertThat( changeManager.createdFiles() ).hasSize( 1 ); + assertThat( changeManager.removedFiles() ).hasSize( 1 ); + assertThat( aspectModel.files() ).hasSize( 1 ); + assertThat( aspectModel.aspect().getSourceFile().sourceLocation() ).contains( originalLocation ); + + changeManager.redoChange(); + assertThat( changeManager.modifiedFiles() ).isEmpty(); + assertThat( changeManager.createdFiles() ).hasSize( 1 ); + assertThat( changeManager.removedFiles() ).hasSize( 1 ); + assertThat( aspectModel.files() ).hasSize( 1 ); + assertThat( aspectModel.aspect().getSourceFile().sourceLocation() ).contains( newLocation ); + } +} diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/edit/RdfUtilTest.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/edit/RdfUtilTest.java new file mode 100644 index 000000000..857ccbb11 --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/edit/RdfUtilTest.java @@ -0,0 +1,84 @@ +/* + * 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.edit; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.eclipse.esmf.aspectmodel.RdfUtil; +import org.eclipse.esmf.metamodel.AspectModel; +import org.eclipse.esmf.metamodel.vocabulary.SammNs; +import org.eclipse.esmf.test.TestAspect; +import org.eclipse.esmf.test.TestResources; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.RDFList; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDF; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +public class RdfUtilTest { + @ParameterizedTest + @EnumSource( value = TestAspect.class ) + void testEveryElementDefinitionContainsTypeAssertion( final TestAspect aspect ) { + final AspectModel aspectModel = TestResources.load( aspect ); + aspectModel.elements().stream() + .filter( element -> !element.isAnonymous() ) + .map( element -> element.getSourceFile().sourceModel().createResource( element.urn().toString() ) ) + .forEach( elementResource -> { + final Model definition = RdfUtil.getModelElementDefinition( elementResource ); + assertThat( definition.listStatements( null, RDF.type, (RDFNode) null ).toList() ).hasSizeGreaterThan( 0 ); + assertThat( definition.listStatements( elementResource, RDF.type, (RDFNode) null ).toList() ).hasSize( 1 ); + } ); + } + + @Test + void testGetElementDefinitionForFlatDefinition() { + final AspectModel aspectModel = TestResources.load( TestAspect.ASPECT ); + final Model sourceModel = aspectModel.aspect().getSourceFile().sourceModel(); + final Resource aspectResource = sourceModel.createResource( aspectModel.aspect().urn().toString() ); + final Model definition = RdfUtil.getModelElementDefinition( aspectResource ); + assertThat( definition.size() ).isEqualTo( sourceModel.size() ); + } + + @Test + void testGetElementDefinitionForNestedDefinition() { + final AspectModel aspectModel = TestResources.load( TestAspect.ASPECT_WITH_PROPERTY ); + final Model sourceModel = aspectModel.aspect().getSourceFile().sourceModel(); + final Resource aspectResource = sourceModel.createResource( aspectModel.aspect().urn().toString() ); + final Model definition = RdfUtil.getModelElementDefinition( aspectResource ); + assertThat( definition.listStatements( null, RDF.type, (RDFNode) null ).toList() ).hasSize( 1 ); + final List propertiesList = definition.listStatements( aspectResource, SammNs.SAMM.properties(), (RDFNode) null ) + .nextStatement() + .getObject().as( RDFList.class ).asJavaList(); + assertThat( propertiesList ).hasSize( 1 ); + } + + @Test + void testGetElementDefinitionForTraitWithConstraints() { + final AspectModel aspectModel = TestResources.load( TestAspect.ASPECT_WITH_CONSTRAINTS ); + final Model sourceModel = aspectModel.aspect().getSourceFile().sourceModel(); + + final Resource traitResource = sourceModel.createResource( TestAspect.TEST_NAMESPACE + "TestRegularExpressionConstraint" ); + final Model definition = RdfUtil.getModelElementDefinition( traitResource ); + final Resource constraint = definition.getResource( traitResource.getURI() ).getProperty( SammNs.SAMMC.constraint() ).getObject() + .asResource(); + assertThat( constraint.getProperty( RDF.type ).getObject().asResource() ).isEqualTo( SammNs.SAMMC.RegularExpressionConstraint() ); + assertThat( constraint.getProperty( SammNs.SAMM.value() ).getObject().asLiteral().getLexicalForm() ).contains( "a-z" ); + } +} diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/loader/AspectModelInstantiatorTest.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/loader/AspectModelInstantiatorTest.java index 483e9e558..aba77898a 100644 --- a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/loader/AspectModelInstantiatorTest.java +++ b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/loader/AspectModelInstantiatorTest.java @@ -25,6 +25,7 @@ import org.eclipse.esmf.metamodel.Aspect; import org.eclipse.esmf.metamodel.ComplexType; import org.eclipse.esmf.metamodel.Entity; +import org.eclipse.esmf.metamodel.Namespace; import org.eclipse.esmf.metamodel.Property; import org.eclipse.esmf.metamodel.Scalar; import org.eclipse.esmf.metamodel.ScalarValue; @@ -43,12 +44,12 @@ public class AspectModelInstantiatorTest extends AbstractAspectModelInstantiatorTest { @ParameterizedTest @EnumSource( value = TestAspect.class ) - public void testLoadAspectExpectSuccess( final TestAspect aspect ) { + void testLoadAspectExpectSuccess( final TestAspect aspect ) { assertThatCode( () -> loadAspect( aspect ) ).doesNotThrowAnyException(); } @Test - public void testAspectTransformationExpectSuccess() { + void testAspectTransformationExpectSuccess() { final Aspect aspect = loadAspect( TestAspect.ASPECT_WITH_SEE ); final AspectModelUrn expectedAspectModelUrn = TestAspect.ASPECT_WITH_SEE.getUrn(); assertBaseAttributes( aspect, expectedAspectModelUrn, "AspectWithSee", "Test Aspect With See", @@ -56,7 +57,7 @@ public void testAspectTransformationExpectSuccess() { } @Test - public void testPropertyInstantiationExpectSuccess() { + void testPropertyInstantiationExpectSuccess() { final AspectModelUrn expectedAspectModelUrn = AspectModelUrn.fromUrn( TestModel.TEST_NAMESPACE + "testProperty" ); final Aspect aspect = loadAspect( TestAspect.ASPECT_WITH_PROPERTY ); @@ -75,7 +76,7 @@ public void testPropertyInstantiationExpectSuccess() { } @Test - public void testOptionalPropertyInstantiationExpectSuccess() { + void testOptionalPropertyInstantiationExpectSuccess() { final AspectModelUrn expectedAspectModelUrn = AspectModelUrn.fromUrn( TestModel.TEST_NAMESPACE + "testProperty" ); final Aspect aspect = loadAspect( TestAspect.ASPECT_WITH_OPTIONAL_PROPERTY ); @@ -94,7 +95,7 @@ public void testOptionalPropertyInstantiationExpectSuccess() { } @Test - public void testNotInPayloadPropertyInstantiationExpectSuccess() { + void testNotInPayloadPropertyInstantiationExpectSuccess() { final AspectModelUrn expectedAspectModelUrn = AspectModelUrn.fromUrn( TestModel.TEST_NAMESPACE + "description" ); final Aspect aspect = loadAspect( TestAspect.ASPECT_WITH_ENTITY_ENUMERATION_AND_NOT_IN_PAYLOAD_PROPERTIES ); @@ -112,7 +113,7 @@ public void testNotInPayloadPropertyInstantiationExpectSuccess() { } @Test - public void testPropertyWithPayloadNameInstantiationExpectSuccess() { + void testPropertyWithPayloadNameInstantiationExpectSuccess() { final AspectModelUrn expectedAspectModelUrn = AspectModelUrn.fromUrn( TestModel.TEST_NAMESPACE + "testProperty" ); final Aspect aspect = loadAspect( TestAspect.ASPECT_WITH_PROPERTY_WITH_PAYLOAD_NAME ); @@ -131,7 +132,7 @@ public void testPropertyWithPayloadNameInstantiationExpectSuccess() { } @Test - public void testEitherCharacteristicInstantiationExpectSuccess() { + void testEitherCharacteristicInstantiationExpectSuccess() { final AspectModelUrn expectedAspectModelUrn = AspectModelUrn.fromUrn( TestModel.TEST_NAMESPACE + "TestEither" ); final Aspect aspect = loadAspect( TestAspect.ASPECT_WITH_EITHER ); @@ -148,7 +149,7 @@ public void testEitherCharacteristicInstantiationExpectSuccess() { } @Test - public void testSingleEntityCharacteristicInstantiationExpectSuccess() { + void testSingleEntityCharacteristicInstantiationExpectSuccess() { final AspectModelUrn expectedAspectModelUrn = AspectModelUrn.fromUrn( TestModel.TEST_NAMESPACE + "EntityCharacteristic" ); final Aspect aspect = loadAspect( TestAspect.ASPECT_WITH_ENTITY ); @@ -163,7 +164,7 @@ public void testSingleEntityCharacteristicInstantiationExpectSuccess() { } @Test - public void testAbstractEntityInstantiationExpectSuccess() { + void testAbstractEntityInstantiationExpectSuccess() { final AspectModelUrn expectedAspectModelUrn = AspectModelUrn.fromUrn( TestModel.TEST_NAMESPACE + "AbstractTestEntity" ); final Aspect aspect = loadAspect( TestAspect.ASPECT_WITH_ABSTRACT_ENTITY ); @@ -179,7 +180,7 @@ public void testAbstractEntityInstantiationExpectSuccess() { } @Test - public void testCollectionWithAbstractEntityInstantiationExpectSuccess() { + void testCollectionWithAbstractEntityInstantiationExpectSuccess() { final AspectModelUrn expectedAspectModelUrn = AspectModelUrn.fromUrn( TestModel.TEST_NAMESPACE + "AbstractTestEntity" ); final Aspect aspect = loadAspect( TestAspect.ASPECT_WITH_COLLECTION_WITH_ABSTRACT_ENTITY ); @@ -194,7 +195,7 @@ public void testCollectionWithAbstractEntityInstantiationExpectSuccess() { } @Test - public void testCodeCharacteristicInstantiationExpectSuccess() { + void testCodeCharacteristicInstantiationExpectSuccess() { final AspectModelUrn expectedAspectModelUrn = AspectModelUrn.fromUrn( TestModel.TEST_NAMESPACE + "TestCode" ); final Aspect aspect = loadAspect( TestAspect.ASPECT_WITH_CODE ); @@ -210,7 +211,7 @@ public void testCodeCharacteristicInstantiationExpectSuccess() { } @Test - public void testCollectionAspectInstantiationExpectSuccess() { + void testCollectionAspectInstantiationExpectSuccess() { final Aspect aspect = loadAspect( TestAspect.ASPECT_WITH_LIST ); final AspectModelUrn expectedAspectModelUrn = TestAspect.ASPECT_WITH_LIST.getUrn(); @@ -223,7 +224,7 @@ public void testCollectionAspectInstantiationExpectSuccess() { } @Test - public void testAspectWithTwoCollectionsInstantiationExpectSuccess() { + void testAspectWithTwoCollectionsInstantiationExpectSuccess() { final Aspect aspect = loadAspect( TestAspect.ASPECT_WITH_TWO_LISTS ); final AspectModelUrn expectedAspectModelUrn = TestAspect.ASPECT_WITH_TWO_LISTS.getUrn(); @@ -237,7 +238,7 @@ public void testAspectWithTwoCollectionsInstantiationExpectSuccess() { } @Test - public void testAspectWithListAndAdditionalPropertyInstantiationExpectSuccess() { + void testAspectWithListAndAdditionalPropertyInstantiationExpectSuccess() { final Aspect aspect = loadAspect( TestAspect.ASPECT_WITH_LIST_AND_ADDITIONAL_PROPERTY ); final AspectModelUrn expectedAspectModelUrn = TestAspect.ASPECT_WITH_LIST_AND_ADDITIONAL_PROPERTY.getUrn(); @@ -250,13 +251,13 @@ public void testAspectWithListAndAdditionalPropertyInstantiationExpectSuccess() } @Test - public void testAspectWithRecursivePropertyWithOptional() { + void testAspectWithRecursivePropertyWithOptional() { final Aspect aspect = loadAspect( TestAspect.ASPECT_WITH_RECURSIVE_PROPERTY_WITH_OPTIONAL ); assertThat( aspect.getProperties().size() ).isEqualTo( 1 ); final Property firstProperty = aspect.getProperties().get( 0 ); - final Property secondProperty = ((DefaultEntity) firstProperty.getCharacteristic().get().getDataType().get()).getProperties() + final Property secondProperty = ( (DefaultEntity) firstProperty.getCharacteristic().get().getDataType().get() ).getProperties() .get( 0 ); - final Property thirdProperty = ((DefaultEntity) secondProperty.getCharacteristic().get().getDataType().get()).getProperties() + final Property thirdProperty = ( (DefaultEntity) secondProperty.getCharacteristic().get().getDataType().get() ).getProperties() .get( 0 ); assertThat( firstProperty ).isNotEqualTo( secondProperty ); assertThat( secondProperty ).isEqualTo( thirdProperty ); @@ -265,7 +266,7 @@ public void testAspectWithRecursivePropertyWithOptional() { } @Test - public void testMetaModelBaseAttributesFactoryMethod() { + void testMetaModelBaseAttributesFactoryMethod() { final AspectModelUrn urn = AspectModelUrn.fromUrn( "urn:samm:org.eclipse.esmf.samm:1.0.0#TestAspect" ); final MetaModelBaseAttributes baseAttributes = MetaModelBaseAttributes.builder().withUrn( urn ).build(); assertThat( baseAttributes.urn() ).isEqualTo( urn ); @@ -275,7 +276,7 @@ public void testMetaModelBaseAttributesFactoryMethod() { } @Test - public void testMetaModelBaseAttributesBuilder() { + void testMetaModelBaseAttributesBuilder() { final AspectModelUrn urn = AspectModelUrn.fromUrn( "urn:samm:org.eclipse.esmf.samm:1.0.0#TestAspect" ); final MetaModelBaseAttributes baseAttributes = MetaModelBaseAttributes.builder() .withUrn( urn ) @@ -291,4 +292,15 @@ public void testMetaModelBaseAttributesBuilder() { .allMatch( description -> description.getLanguageTag().equals( Locale.ENGLISH ) ); assertThat( baseAttributes.getSee() ).hasSize( 2 ).contains( "see1", "see2" ); } + + @Test + void testLoadAspectWithNamespaceDescription() { + final Aspect aspect = loadAspect( TestAspect.ASPECT_WITH_NAMESPACE_DESCRIPTION ); + final Namespace namespace = aspect.getSourceFile().namespace(); + assertThat( namespace.packagePart() ).isEqualTo( aspect.urn().getNamespaceMainPart() ); + assertThat( namespace.version().toString() ).isEqualTo( aspect.urn().getVersion() ); + assertThat( namespace.getPreferredName( Locale.ENGLISH ) ).isEqualTo( "Test namespace" ); + assertThat( namespace.getDescription( Locale.ENGLISH ) ).isEqualTo( "Test of the namespace pseudo element" ); + assertThat( namespace.getSee() ).hasSize( 1 ).contains( "http://example.com/" ); + } } diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/loader/AspectModelLoaderTest.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/loader/AspectModelLoaderTest.java index b7423deec..df81741a9 100644 --- a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/loader/AspectModelLoaderTest.java +++ b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/loader/AspectModelLoaderTest.java @@ -32,6 +32,7 @@ import org.eclipse.esmf.metamodel.AbstractEntity; import org.eclipse.esmf.metamodel.AspectModel; import org.eclipse.esmf.metamodel.ComplexType; +import org.eclipse.esmf.metamodel.HasDescription; import org.eclipse.esmf.samm.KnownVersion; import org.eclipse.esmf.test.TestAspect; import org.eclipse.esmf.test.TestResources; @@ -61,8 +62,8 @@ void testLoadAspectModelFromZipArchiveFile() { final AspectModel aspectModel = new AspectModelLoader().loadNamespacePackage( new File( archivePath.toString() ) ); assertThat( aspectModel.namespaces() ).hasSize( 2 ); - assertThat( aspectModel.namespaces().get( 0 ).getName() ).contains( "urn:samm:org.eclipse.examples:1.1.0" ); - assertThat( aspectModel.namespaces().get( 1 ).getName() ).contains( "urn:samm:org.eclipse.examples:1.0.0" ); + assertThat( aspectModel.namespaces() ).map( HasDescription::getName ).contains( "urn:samm:org.eclipse.examples:1.1.0" ); + assertThat( aspectModel.namespaces() ).map( HasDescription::getName ).contains( "urn:samm:org.eclipse.examples:1.0.0" ); final List aspectsNames = List.of( "Movement2", "Movement3", "Movement", "SimpleAspect" ); @@ -79,8 +80,8 @@ void testLoadAspectModelFromZipArchiveInputStream() throws FileNotFoundException final AspectModel aspectModel = new AspectModelLoader().loadNamespacePackage( new FileInputStream( archivePath.toString() ) ); assertThat( aspectModel.namespaces() ).hasSize( 2 ); - assertThat( aspectModel.namespaces().get( 0 ).getName() ).contains( "urn:samm:org.eclipse.examples:1.1.0" ); - assertThat( aspectModel.namespaces().get( 1 ).getName() ).contains( "urn:samm:org.eclipse.examples:1.0.0" ); + assertThat( aspectModel.namespaces() ).map( HasDescription::getName ).contains( "urn:samm:org.eclipse.examples:1.1.0" ); + assertThat( aspectModel.namespaces() ).map( HasDescription::getName ).contains( "urn:samm:org.eclipse.examples:1.0.0" ); final List aspectsNames = List.of( "Movement2", "Movement3", "Movement", "SimpleAspect" ); @@ -100,8 +101,8 @@ void testLoadAspectModelFromZipArchive2_0_0() throws FileNotFoundException { final AspectModel aspectModel = new AspectModelLoader().loadNamespacePackage( new FileInputStream( archivePath.toString() ) ); assertThat( aspectModel.namespaces() ).hasSize( 2 ); - assertThat( aspectModel.namespaces().get( 0 ).getName() ).contains( "urn:samm:org.eclipse.examples:1.1.0" ); - assertThat( aspectModel.namespaces().get( 1 ).getName() ).contains( "urn:samm:org.eclipse.examples:1.0.0" ); + assertThat( aspectModel.namespaces() ).map( HasDescription::getName ).contains( "urn:samm:org.eclipse.examples:1.1.0" ); + assertThat( aspectModel.namespaces() ).map( HasDescription::getName ).contains( "urn:samm:org.eclipse.examples:1.0.0" ); final List aspectsNames = List.of( "Movement2", "Movement3", "Movement4", "Movement", "SimpleAspect" ); @@ -118,8 +119,8 @@ void testLoadAspectModelFromZipArchiveAspectModelsRoot() throws FileNotFoundExce final AspectModel aspectModel = new AspectModelLoader().loadNamespacePackage( new FileInputStream( archivePath.toString() ) ); assertThat( aspectModel.namespaces() ).hasSize( 2 ); - assertThat( aspectModel.namespaces().get( 0 ).getName() ).contains( "urn:samm:org.eclipse.examples:1.1.0" ); - assertThat( aspectModel.namespaces().get( 1 ).getName() ).contains( "urn:samm:org.eclipse.examples:1.0.0" ); + assertThat( aspectModel.namespaces() ).map( HasDescription::getName ).contains( "urn:samm:org.eclipse.examples:1.1.0" ); + assertThat( aspectModel.namespaces() ).map( HasDescription::getName ).contains( "urn:samm:org.eclipse.examples:1.0.0" ); final List aspectsNames = List.of( "Movement2", "Movement3", "Movement", "SimpleAspect" ); @@ -136,8 +137,8 @@ void testLoadAspectModelFromZipArchiveAspectModelsSubfolder() throws FileNotFoun final AspectModel aspectModel = new AspectModelLoader().loadNamespacePackage( new FileInputStream( archivePath.toString() ) ); assertThat( aspectModel.namespaces() ).hasSize( 2 ); - assertThat( aspectModel.namespaces().get( 0 ).getName() ).contains( "urn:samm:org.eclipse.examples:1.1.0" ); - assertThat( aspectModel.namespaces().get( 1 ).getName() ).contains( "urn:samm:org.eclipse.examples:1.0.0" ); + assertThat( aspectModel.namespaces() ).map( HasDescription::getName ).contains( "urn:samm:org.eclipse.examples:1.1.0" ); + assertThat( aspectModel.namespaces() ).map( HasDescription::getName ).contains( "urn:samm:org.eclipse.examples:1.0.0" ); final List aspectsNames = List.of( "Movement2", "Movement3", "Movement", "SimpleAspect" ); @@ -176,6 +177,17 @@ void testLoadAspectModelFromZipArchiveWithSharedProperty() throws FileNotFoundEx } ); } + @Test + void testMergeAspectModels() { + final AspectModel a1 = TestResources.load( TestAspect.ASPECT ); + final AspectModel a2 = TestResources.load( TestAspect.ASPECT_WITH_PROPERTY ); + assertThat( a1.aspects() ).hasSize( 1 ); + assertThat( a2.aspects() ).hasSize( 1 ); + final AspectModel merged = new AspectModelLoader().merge( a1, a2 ); + assertThat( merged.aspects() ).hasSize( 2 ); + assertThat( merged.elements().size() ).isEqualTo( a1.elements().size() + a2.elements().size() ); + } + /** * Returns the File object for a test model file */ diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/test/TestResources.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/test/TestResources.java index a39e7ae16..c7a9078bc 100644 --- a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/test/TestResources.java +++ b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/test/TestResources.java @@ -14,6 +14,8 @@ package org.eclipse.esmf.test; import java.io.InputStream; +import java.net.URI; +import java.util.Optional; import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader; import org.eclipse.esmf.aspectmodel.resolver.ClasspathStrategy; @@ -24,11 +26,11 @@ public class TestResources { public static AspectModel load( final TestAspect model ) { final KnownVersion metaModelVersion = KnownVersion.getLatest(); - final String path = String.format( "valid/%s/%s/%s.ttl", model.getUrn().getNamespace(), model.getUrn().getVersion(), + final String path = String.format( "valid/%s/%s/%s.ttl", model.getUrn().getNamespaceMainPart(), model.getUrn().getVersion(), model.getName() ); final InputStream inputStream = TestResources.class.getClassLoader().getResourceAsStream( path ); final ResolutionStrategy testModelsResolutionStrategy = new ClasspathStrategy( "valid/" + metaModelVersion.toString().toLowerCase() ); - return new AspectModelLoader( testModelsResolutionStrategy ).load( inputStream ); + return new AspectModelLoader( testModelsResolutionStrategy ).load( inputStream, Optional.of( URI.create( "testmodel:" + path ) ) ); } } diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/AspectModelHelper.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/AspectModelHelper.java index 5a07d0b65..d17e1b41c 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/AspectModelHelper.java +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/AspectModelHelper.java @@ -105,7 +105,7 @@ public int increment( final int number ) { private String namespaceAnchorPart( final ModelElement modelElement ) { return Optional.ofNullable( modelElement ) .map( ModelElement::urn ) - .map( urn -> urn.getNamespace().replace( ".", "-" ) ).orElse( "" ); + .map( urn -> urn.getNamespaceMainPart().replace( ".", "-" ) ).orElse( "" ); } public String buildAnchor( final ModelElement modelElement, final ModelElement parentElement, final String suffix ) { diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/asyncapi/AspectModelAsyncApiGenerator.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/asyncapi/AspectModelAsyncApiGenerator.java index ba84f8230..c8368f362 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/asyncapi/AspectModelAsyncApiGenerator.java +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/asyncapi/AspectModelAsyncApiGenerator.java @@ -207,11 +207,11 @@ private void setChannelNodeMeta( final ObjectNode channelNode, final Aspect aspe channelNode.put( "address", StringUtils.isNotBlank( config.channelAddress() ) ? config.channelAddress() : - String.format( "/%s/%s/%s", aspectModelUrn.getNamespace(), aspectModelUrn.getVersion(), aspect.getName() ) ); + String.format( "/%s/%s/%s", aspectModelUrn.getNamespaceMainPart(), aspectModelUrn.getVersion(), aspect.getName() ) ); channelNode.put( DESCRIPTION_FIELD, "This channel for updating " + aspect.getName() + " Aspect." ); final ObjectNode parametersNode = FACTORY.objectNode(); - parametersNode.put( "namespace", aspectModelUrn.getNamespace() ); + parametersNode.put( "namespace", aspectModelUrn.getNamespaceMainPart() ); parametersNode.put( "version", aspectModelUrn.getVersion() ); parametersNode.put( "aspect-name", aspect.getName() ); diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/openapi/AspectModelOpenApiGenerator.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/openapi/AspectModelOpenApiGenerator.java index 73b417258..d2f512930 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/openapi/AspectModelOpenApiGenerator.java +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/openapi/AspectModelOpenApiGenerator.java @@ -130,7 +130,7 @@ public OpenApiSchemaArtifact apply( final Aspect aspect, final OpenApiSchemaGene final ObjectNode rootNode = getRootJsonNode( config.generateCommentForSeeAttributes() ); final String apiVersion = getApiVersion( aspect, config.useSemanticVersion() ); - ((ObjectNode) rootNode.get( "info" )) + ( (ObjectNode) rootNode.get( "info" ) ) .put( "title", aspect.getPreferredName( config.locale() ) ) .put( "version", apiVersion ) .put( AbstractGenerator.SAMM_EXTENSION, aspect.urn().toString() ); @@ -243,7 +243,7 @@ private String getApiVersion( final Aspect aspect, final boolean useSemanticVers private void setResponseBodies( final Aspect aspect, final ObjectNode jsonNode, final boolean includePaging ) { final ObjectNode componentsResponseNode = (ObjectNode) jsonNode.get( FIELD_COMPONENTS ).get( FIELD_RESPONSES ); final ObjectNode referenceNode = FACTORY.objectNode() - .put( REF, COMPONENTS_SCHEMAS + (includePaging ? FIELD_PAGING_SCHEMA : aspect.getName()) ); + .put( REF, COMPONENTS_SCHEMAS + ( includePaging ? FIELD_PAGING_SCHEMA : aspect.getName() ) ); final ObjectNode contentNode = getApplicationNode( referenceNode ); componentsResponseNode.set( aspect.getName(), contentNode ); contentNode.put( FIELD_DESCRIPTION, "The request was successful." ); @@ -497,7 +497,7 @@ private ObjectNode getRequestEndpointsUpdate( final Aspect aspect, final ObjectN final boolean isPut ) { final ObjectNode objectNode = FACTORY.objectNode(); objectNode.set( "tags", FACTORY.arrayNode().add( aspect.getName() ) ); - objectNode.put( FIELD_OPERATION_ID, (isPut ? FIELD_PUT : FIELD_PATCH) + aspect.getName() ); + objectNode.put( FIELD_OPERATION_ID, ( isPut ? FIELD_PUT : FIELD_PATCH ) + aspect.getName() ); objectNode.set( FIELD_PARAMETERS, getRequiredParameters( parameterNode, isEmpty( resourcePath ) ) ); objectNode.set( FIELD_REQUEST_BODY, FACTORY.objectNode().put( REF, COMPONENTS_REQUESTS + aspect.getName() ) ); objectNode.set( FIELD_RESPONSES, getResponsesForGet( aspect ) ); @@ -516,8 +516,8 @@ private void setErrorResponses( final ObjectNode responses ) { final ObjectNode unauthorized = FACTORY.objectNode().put( REF, COMPONENTS_RESPONSES + UNAUTHORIZED ); final ObjectNode forbidden = FACTORY.objectNode().put( REF, COMPONENTS_RESPONSES + FORBIDDEN ); final ObjectNode notFoundError = FACTORY.objectNode().put( REF, COMPONENTS_RESPONSES + NOT_FOUND_ERROR ); - responses.set( "401", clientError ); - responses.set( "402", unauthorized ); + responses.set( "400", clientError ); + responses.set( "401", unauthorized ); responses.set( "403", forbidden ); responses.set( "404", notFoundError ); } @@ -617,6 +617,8 @@ private static ObjectNode mergeObjectNode( final ObjectNode node, final ObjectNo node.set( key, mergeObjectNode( (ObjectNode) resultValue, (ObjectNode) value ) ); } else if ( resultValue.isArray() && value.isArray() ) { node.set( key, mergeArrayNode( (ArrayNode) resultValue, (ArrayNode) value ) ); + } else { + node.set( key, value ); } } else { node.set( key, value ); diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/openapi/OpenApiSchemaGenerationConfig.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/openapi/OpenApiSchemaGenerationConfig.java index 3df6774f1..39863277b 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/openapi/OpenApiSchemaGenerationConfig.java +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/openapi/OpenApiSchemaGenerationConfig.java @@ -16,7 +16,6 @@ import static org.eclipse.esmf.aspectmodel.generator.openapi.AspectModelOpenApiGenerator.ObjectNodeExtension.getter; import java.util.Locale; -import java.util.Optional; import org.eclipse.esmf.aspectmodel.generator.GenerationConfig; @@ -67,21 +66,20 @@ public record OpenApiSchemaGenerationConfig( } public ObjectNode queriesTemplate() { - return Optional.ofNullable( template ) - .map( getter( "paths" ) ) - .map( getter( QUERIES_TEMPLATE_PATH ) ) - .orElse( null ); + if ( template == null ) { + return null; + } + + final ObjectNode objectNode = getter( "paths" ).apply( template ); + return getter( QUERIES_TEMPLATE_PATH ).apply( objectNode ); } public ObjectNode documentTemplate() { - return Optional.ofNullable( template ) - .map( ObjectNode::deepCopy ) - .map( doc -> { - Optional.of( doc ).map( getter( "paths" ) ).ifPresent( - paths -> paths.remove( QUERIES_TEMPLATE_PATH ) - ); - return doc; - } ) - .orElse( null ); + if ( template == null ) { + return null; + } + final ObjectNode objectNode = template.deepCopy(); + getter( "paths" ).apply( objectNode ).remove( QUERIES_TEMPLATE_PATH ); + return objectNode; } } diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/zip/AspectModelNamespacePackageCreator.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/zip/AspectModelNamespacePackageCreator.java index 021cc91ec..0cad13fe6 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/zip/AspectModelNamespacePackageCreator.java +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/zip/AspectModelNamespacePackageCreator.java @@ -44,10 +44,10 @@ public void accept( final AspectModel aspectModel, final OutputStream outputStre private static void addFileToArchive( final AspectModelFile file, final ZipOutputStream zos, final String rootPath ) throws IOException { - final String aspectString = AspectSerializer.INSTANCE.apply( file.aspect() ); + final String aspectString = AspectSerializer.INSTANCE.aspectToString( file.aspect() ); final String fileName = String.format( "%s/%s/%s/%s.ttl", !rootPath.isBlank() ? String.format( "%s/%s", rootPath, BASE_ARCHIVE_FORMAT_PATH ) : BASE_ARCHIVE_FORMAT_PATH, - file.aspect().urn().getNamespace(), + file.aspect().urn().getNamespaceMainPart(), file.aspect().urn().getVersion(), file.aspect().getName() ); final ZipEntry zipEntry = new ZipEntry( fileName ); diff --git a/core/esmf-aspect-model-document-generators/src/main/resources/openapi/OpenApiRootJson.json b/core/esmf-aspect-model-document-generators/src/main/resources/openapi/OpenApiRootJson.json index 1d29d6ab5..0dc85d684 100644 --- a/core/esmf-aspect-model-document-generators/src/main/resources/openapi/OpenApiRootJson.json +++ b/core/esmf-aspect-model-document-generators/src/main/resources/openapi/OpenApiRootJson.json @@ -52,13 +52,13 @@ "description": "The requesting user or client is not authenticated." }, "Forbidden": { - "description": "The requesting user or client is not authorized to access resources for the given tenant." + "description": "The requesting user or client is not authorized to access resources." }, "NotFoundError": { - "description": "The requested Twin has not been found." + "description": "The requested resource has not been found." }, "ClientError": { - "description": "Payload or user input is invalid. See error details in the payload for more.", + "description": "Payload or user input is invalid. See error details in the payload for more information.", "content": { "application/json": { "schema": { diff --git a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/openapi/AspectModelOpenApiGeneratorTest.java b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/openapi/AspectModelOpenApiGeneratorTest.java index c898731d9..45f945434 100644 --- a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/openapi/AspectModelOpenApiGeneratorTest.java +++ b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/openapi/AspectModelOpenApiGeneratorTest.java @@ -254,6 +254,28 @@ void testValidParameter() throws IOException { } ); } + @Test + void testValidTemplate() throws IOException { + final Aspect aspect = TestResources.load( TestAspect.ASPECT_WITHOUT_SEE_ATTRIBUTE ).aspect(); + final OpenApiSchemaGenerationConfig config = OpenApiSchemaGenerationConfigBuilder.builder() + .useSemanticVersion( true ) + .baseUrl( TEST_BASE_URL ) + .resourcePath( TEST_RESOURCE_PATH ) + .template( getTemplateParameter() ) + .build(); + final JsonNode json = apiJsonGenerator.apply( aspect, config ).getContent(); + final SwaggerParseResult result = new OpenAPIParser().readContents( json.toString(), null, null ); + assertThat( result.getMessages() ).isEmpty(); + + final OpenAPI openApi = result.getOpenAPI(); + assertThat( openApi.getPaths().values().stream().findFirst().get().getGet().getResponses().get( "400" ).get$ref() ) + .isEqualTo( "./core-api.yaml#/components/responses/ClientError" ); + assertThat( openApi.getPaths().values().stream().findFirst().get().getGet().getResponses().get( "401" ).get$ref() ) + .isEqualTo( "./core-api.yaml#/components/responses/Unauthorized" ); + assertThat( openApi.getPaths().values().stream().findFirst().get().getGet().getResponses().get( "403" ).get$ref() ) + .isEqualTo( "./core-api.yaml#/components/responses/Forbidden" ); + } + @Test void testInValidParameterName() throws IOException { final ListAppender logAppender = new ListAppender<>(); @@ -495,11 +517,11 @@ void testAspectWithOperationWithSeeAttribute() { final SwaggerParseResult result = new OpenAPIParser().readContents( json.toString(), null, null ); final OpenAPI openApi = result.getOpenAPI(); assertThat( - ((Schema) openApi.getComponents().getSchemas().get( "testOperation" ).getAllOf() - .get( 1 )).getProperties() ).doesNotContainKey( + ( (Schema) openApi.getComponents().getSchemas().get( "testOperation" ).getAllOf() + .get( 1 ) ).getProperties() ).doesNotContainKey( "params" ); - assertThat( ((Schema) openApi.getComponents().getSchemas().get( "testOperationTwo" ).getAllOf() - .get( 1 )).getProperties() ).doesNotContainKey( "params" ); + assertThat( ( (Schema) openApi.getComponents().getSchemas().get( "testOperationTwo" ).getAllOf() + .get( 1 ) ).getProperties() ).doesNotContainKey( "params" ); } @Test @@ -635,8 +657,8 @@ void testAspectWithCommentForSeeAttributes() { assertThat( openApi.getSpecVersion() ).isEqualTo( SpecVersion.V31 ); assertThat( openApi.getComponents().getSchemas().get( "AspectWithCollection" ).get$comment() ).isEqualTo( "See: http://example.com/" ); - assertThat( ((Schema) openApi.getComponents().getSchemas().get( "AspectWithCollection" ).getProperties() - .get( "testProperty" )).get$comment() ) + assertThat( ( (Schema) openApi.getComponents().getSchemas().get( "AspectWithCollection" ).getProperties() + .get( "testProperty" ) ).get$comment() ) .isEqualTo( "See: http://example.com/, http://example.com/me" ); assertThat( openApi.getComponents().getSchemas().get( "TestCollection" ).get$comment() ) .isEqualTo( "See: http://example.com/" ); @@ -766,4 +788,10 @@ private ObjectNode getTestParameter() throws IOException { final String inputString = IOUtils.toString( inputStream, StandardCharsets.UTF_8 ); return (ObjectNode) OBJECT_MAPPER.readTree( inputString ); } + + private ObjectNode getTemplateParameter() throws IOException { + final InputStream inputStream = getClass().getResourceAsStream( "/openapi/open-api-error-template.yaml" ); + final String inputString = IOUtils.toString( inputStream, StandardCharsets.UTF_8 ); + return (ObjectNode) new YAMLMapper().readTree( inputString ); + } } diff --git a/core/esmf-aspect-model-document-generators/src/test/resources/openapi/open-api-error-template.yaml b/core/esmf-aspect-model-document-generators/src/test/resources/openapi/open-api-error-template.yaml new file mode 100644 index 000000000..e1f91e666 --- /dev/null +++ b/core/esmf-aspect-model-document-generators/src/test/resources/openapi/open-api-error-template.yaml @@ -0,0 +1,10 @@ +paths: + __DEFAULT_QUERIES_TEMPLATE__: + get: + responses: + '400': + $ref: "core-api.yaml#/components/responses/ClientError" + '401': + $ref: "core-api.yaml#/components/responses/Unauthorized" + '403': + $ref: "core-api.yaml#/components/responses/Forbidden" \ No newline at end of file diff --git a/core/esmf-aspect-model-jackson/src/test/java/org/eclipse/esmf/aspectmodel/jackson/AspectModelJacksonModuleTest.java b/core/esmf-aspect-model-jackson/src/test/java/org/eclipse/esmf/aspectmodel/jackson/AspectModelJacksonModuleTest.java index 57a2931f3..3bd851c97 100644 --- a/core/esmf-aspect-model-jackson/src/test/java/org/eclipse/esmf/aspectmodel/jackson/AspectModelJacksonModuleTest.java +++ b/core/esmf-aspect-model-jackson/src/test/java/org/eclipse/esmf/aspectmodel/jackson/AspectModelJacksonModuleTest.java @@ -250,7 +250,7 @@ private Object generateInstance( final Tuple2 modelNameAndPa private String loadJsonPayload( final TestAspect model, final String payloadName ) throws IOException { final AspectModelUrn modelUrn = model.getUrn(); final URL jsonUrl = getClass().getResource( - String.format( "/%s/%s/%s.json", modelUrn.getNamespace(), modelUrn.getVersion(), payloadName ) ); + String.format( "/%s/%s/%s.json", modelUrn.getNamespaceMainPart(), modelUrn.getVersion(), payloadName ) ); return Resources.toString( jsonUrl, StandardCharsets.UTF_8 ); } diff --git a/core/esmf-aspect-model-java-generator/src/test/java/org/eclipse/esmf/aspectmodel/java/AspectModelJavaGeneratorTest.java b/core/esmf-aspect-model-java-generator/src/test/java/org/eclipse/esmf/aspectmodel/java/AspectModelJavaGeneratorTest.java index b57c8441e..a958d1448 100644 --- a/core/esmf-aspect-model-java-generator/src/test/java/org/eclipse/esmf/aspectmodel/java/AspectModelJavaGeneratorTest.java +++ b/core/esmf-aspect-model-java-generator/src/test/java/org/eclipse/esmf/aspectmodel/java/AspectModelJavaGeneratorTest.java @@ -80,7 +80,7 @@ private Collection getGenerators( final TestAspect testAspect, fi .enableJacksonAnnotations( enableJacksonAnnotations ) .executeLibraryMacros( executeLibraryMacros ) .templateLibFile( templateLibPath ) - .packageName( aspect.urn().getNamespace() ) + .packageName( aspect.urn().getNamespaceMainPart() ) .build(); return List.of( new AspectModelJavaGenerator( aspect, config ) ); } @@ -93,7 +93,7 @@ private Collection getGenerators( final AspectModel aspectModel ) final JavaCodeGenerationConfig config = JavaCodeGenerationConfigBuilder.builder() .enableJacksonAnnotations( true ) .executeLibraryMacros( false ) - .packageName( aspectModel.aspect().urn().getNamespace() ) + .packageName( aspectModel.aspect().urn().getNamespaceMainPart() ) .build(); return List.of( new AspectModelJavaGenerator( aspectModel.aspect(), config ) ); } diff --git a/core/esmf-aspect-model-java-generator/src/test/java/org/eclipse/esmf/aspectmodel/java/AspectModelJavaUtilTest.java b/core/esmf-aspect-model-java-generator/src/test/java/org/eclipse/esmf/aspectmodel/java/AspectModelJavaUtilTest.java index e557ac3da..f3b87d58a 100644 --- a/core/esmf-aspect-model-java-generator/src/test/java/org/eclipse/esmf/aspectmodel/java/AspectModelJavaUtilTest.java +++ b/core/esmf-aspect-model-java-generator/src/test/java/org/eclipse/esmf/aspectmodel/java/AspectModelJavaUtilTest.java @@ -22,12 +22,12 @@ import org.eclipse.esmf.metamodel.Value; import org.eclipse.esmf.test.shared.arbitraries.PropertyBasedTest; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import net.jqwik.api.ForAll; import net.jqwik.api.Property; import net.jqwik.api.Tuple; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public class AspectModelJavaUtilTest extends PropertyBasedTest { private boolean isValidJavaIdentifier( final String value ) { diff --git a/core/esmf-aspect-model-java-generator/src/test/java/org/eclipse/esmf/aspectmodel/java/StaticMetaModelGeneratorTest.java b/core/esmf-aspect-model-java-generator/src/test/java/org/eclipse/esmf/aspectmodel/java/StaticMetaModelGeneratorTest.java index cc51958ad..3970dd547 100644 --- a/core/esmf-aspect-model-java-generator/src/test/java/org/eclipse/esmf/aspectmodel/java/StaticMetaModelGeneratorTest.java +++ b/core/esmf-aspect-model-java-generator/src/test/java/org/eclipse/esmf/aspectmodel/java/StaticMetaModelGeneratorTest.java @@ -46,7 +46,7 @@ Collection getGenerators( final TestAspect testAspect, final bool .enableJacksonAnnotations( false ) .executeLibraryMacros( executeLibraryMacros ) .templateLibFile( templateLibFile ) - .packageName( aspect.urn().getNamespace() ) + .packageName( aspect.urn().getNamespaceMainPart() ) .build(); final JavaGenerator pojoGenerator = new AspectModelJavaGenerator( aspect, config ); final JavaGenerator staticGenerator = new StaticMetaModelJavaGenerator( aspect, config ); @@ -59,7 +59,7 @@ Collection getGenerators( final TestAspect testAspect ) { final JavaCodeGenerationConfig config = JavaCodeGenerationConfigBuilder.builder() .enableJacksonAnnotations( false ) .executeLibraryMacros( false ) - .packageName( aspect.urn().getNamespace() ) + .packageName( aspect.urn().getNamespaceMainPart() ) .build(); final JavaGenerator pojoGenerator = new AspectModelJavaGenerator( aspect, config ); final JavaGenerator staticGenerator = new StaticMetaModelJavaGenerator( aspect, config ); @@ -72,7 +72,7 @@ Collection getGenerators( final TestSharedAspect testAspect ) { final JavaCodeGenerationConfig config = JavaCodeGenerationConfigBuilder.builder() .enableJacksonAnnotations( false ) .executeLibraryMacros( false ) - .packageName( aspect.urn().getNamespace() ) + .packageName( aspect.urn().getNamespaceMainPart() ) .build(); final JavaGenerator pojoGenerator = new AspectModelJavaGenerator( aspect, config ); final JavaGenerator staticGenerator = new StaticMetaModelJavaGenerator( aspect, config ); diff --git a/core/esmf-aspect-model-serializer/src/main/java/org/eclipse/esmf/aspectmodel/serializer/AspectSerializer.java b/core/esmf-aspect-model-serializer/src/main/java/org/eclipse/esmf/aspectmodel/serializer/AspectSerializer.java index a5da06ce0..3eafa1a5f 100644 --- a/core/esmf-aspect-model-serializer/src/main/java/org/eclipse/esmf/aspectmodel/serializer/AspectSerializer.java +++ b/core/esmf-aspect-model-serializer/src/main/java/org/eclipse/esmf/aspectmodel/serializer/AspectSerializer.java @@ -13,39 +13,154 @@ package org.eclipse.esmf.aspectmodel.serializer; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.function.Function; +import org.eclipse.esmf.aspectmodel.AspectModelFile; +import org.eclipse.esmf.aspectmodel.RdfUtil; +import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader; +import org.eclipse.esmf.aspectmodel.resolver.modelfile.RawAspectModelFileBuilder; import org.eclipse.esmf.metamodel.Aspect; +import org.eclipse.esmf.metamodel.AspectModel; import org.eclipse.esmf.metamodel.vocabulary.RdfNamespace; +import org.eclipse.esmf.metamodel.vocabulary.SimpleRdfNamespace; import org.apache.jena.rdf.model.Model; /** - * Convenience function to serialize an Aspect into a String + * Functions to write Aspect Models and Aspect Model files to Strings or their respective source locations */ -public class AspectSerializer implements Function { +public class AspectSerializer { public static final AspectSerializer INSTANCE = new AspectSerializer(); + private final Map> protocolHandlers = new HashMap<>(); - @Override + protected AspectSerializer() { + registerProtocolHandler( "file", this::createFileOutputStream ); + } + + private OutputStream createFileOutputStream( final URI outputUri ) { + try { + return new FileOutputStream( new File( outputUri ) ); + } catch ( final FileNotFoundException exception ) { + throw new SerializationException( exception ); + } + } + + /** + * Serializes an Aspect and the elements in the same file + * + * @param aspect the Aspect + * @return the String representation in RDF/Turtle + * @deprecated Use {link {@link #aspectToString(Aspect)}} instead + */ + @Deprecated( forRemoval = true ) public String apply( final Aspect aspect ) { - final RdfNamespace aspectNamespace = new RdfNamespace() { - @Override - public String getShortForm() { - return ""; - } - - @Override - public String getUri() { - return aspect.urn().getUrnPrefix(); - } - }; - final RdfModelCreatorVisitor rdfModelCreatorVisitor = new RdfModelCreatorVisitor( aspectNamespace ); + return aspectToString( aspect ); + } + + /** + * Serializes all files of an Aspect Model to their respective source locations. + * Attention: This method does not check validity of paths or existance of files and will overwrite without further checks. + * + * @param aspectModel the Aspect Model + */ + public void write( final AspectModel aspectModel ) { + aspectModel.files().forEach( this::write ); + } + + /** + * Returns a URL for the Aspect Model file if it can be determined. + * + * @param aspectModelFile the input Aspect Model file + * @return the Aspect Model file's location as URL + * @throws SerializationException if the file has no source location or the source location URI is no URL + */ + public URL aspectModelFileUrl( final AspectModelFile aspectModelFile ) { + if ( aspectModelFile.sourceLocation().isEmpty() ) { + throw new SerializationException( "Aspect Model file has no source location" ); + } + + final URI uri = aspectModelFile.sourceLocation().get(); + final URL url; + try { + url = uri.toURL(); + } catch ( final MalformedURLException exception ) { + throw new SerializationException( "Aspect Model file can only be written to locations given by URLs" ); + } + + return url; + } + + /** + * Writes the content of an Aspect Model file to its defined source location + * + * @param aspectModelFile the Aspect Model file + */ + public void write( final AspectModelFile aspectModelFile ) { + final URL url = aspectModelFileUrl( aspectModelFile ); + final Function protocolHandler = protocolHandlers.get( url.getProtocol() ); + if ( protocolHandler == null ) { + throw new SerializationException( "Don't know how to write " + url.getProtocol() + " URLs: " + url ); + } + + final String content = aspectModelFileToString( aspectModelFile ); + try ( final OutputStream out = protocolHandler.apply( aspectModelFile.sourceLocation().get() ) ) { + out.write( content.getBytes( StandardCharsets.UTF_8 ) ); + } catch ( final IOException exception ) { + throw new SerializationException( exception ); + } + } + + public void registerProtocolHandler( final String protocol, final Function protocolHandler ) { + protocolHandlers.put( protocol, protocolHandler ); + } + + /** + * Serializes an Aspect and the elements in the same file + * + * @param aspect the Aspect + * @return the String representation in RDF/Turtle + */ + public String aspectToString( final Aspect aspect ) { + if ( aspect.getSourceFile() != null ) { + return aspectModelFileToString( aspect.getSourceFile() ); + } + + // The Aspect has no source file, it was probably created programmatically. + // Construct a virtual AspectModelFile with an RDF representation that be serialized. + + final RdfNamespace namespace = new SimpleRdfNamespace( "", aspect.urn().getUrnPrefix() ); + final Model rdfModel = new RdfModelCreatorVisitor( namespace ).visitAspect( aspect, null ).model(); + RdfUtil.cleanPrefixes( rdfModel ); + final AspectModel aspectModel = new AspectModelLoader().loadAspectModelFiles( + List.of( RawAspectModelFileBuilder.builder().sourceModel( rdfModel ).build() ) ); + final AspectModelFile newSourceFile = aspectModel.aspect().getSourceFile(); + return aspectModelFileToString( newSourceFile ); + } + + /** + * Serializes an Aspect Model file + * + * @param aspectModelFile the Aspect Model file + * @return the String representation in RDF/Turtle + */ + public String aspectModelFileToString( final AspectModelFile aspectModelFile ) { final StringWriter stringWriter = new StringWriter(); try ( final PrintWriter printWriter = new PrintWriter( stringWriter ) ) { - final Model model = aspect.accept( rdfModelCreatorVisitor, null ).model(); - final PrettyPrinter prettyPrinter = new PrettyPrinter( model, aspect.urn(), printWriter ); + final PrettyPrinter prettyPrinter = new PrettyPrinter( aspectModelFile, printWriter ); prettyPrinter.print(); } return stringWriter.toString(); diff --git a/core/esmf-aspect-model-serializer/src/main/java/org/eclipse/esmf/aspectmodel/serializer/PrettyPrinter.java b/core/esmf-aspect-model-serializer/src/main/java/org/eclipse/esmf/aspectmodel/serializer/PrettyPrinter.java index a8cec3e9e..dc037c568 100644 --- a/core/esmf-aspect-model-serializer/src/main/java/org/eclipse/esmf/aspectmodel/serializer/PrettyPrinter.java +++ b/core/esmf-aspect-model-serializer/src/main/java/org/eclipse/esmf/aspectmodel/serializer/PrettyPrinter.java @@ -19,7 +19,6 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Comparator; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -30,8 +29,8 @@ import java.util.stream.Collectors; import org.eclipse.esmf.aspectmodel.AspectModelFile; -import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn; -import org.eclipse.esmf.metamodel.vocabulary.RdfNamespace; +import org.eclipse.esmf.aspectmodel.RdfUtil; +import org.eclipse.esmf.metamodel.ModelElement; import org.eclipse.esmf.metamodel.vocabulary.SammNs; import com.google.common.collect.ImmutableList; @@ -48,6 +47,7 @@ import org.apache.jena.graph.Node_URI; import org.apache.jena.graph.Node_Variable; import org.apache.jena.graph.Triple; +import org.apache.jena.graph.UriNode; import org.apache.jena.graph.impl.LiteralLabel; import org.apache.jena.rdf.model.Literal; import org.apache.jena.rdf.model.Model; @@ -58,53 +58,31 @@ import org.apache.jena.rdf.model.Resource; import org.apache.jena.rdf.model.ResourceFactory; import org.apache.jena.rdf.model.Statement; +import org.apache.jena.rdf.model.StmtIterator; import org.apache.jena.rdf.model.impl.Util; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.XSD; /** - * Serializes an {@link AspectModelFile} to RDF/Turtle while following the formatting rules for Aspect models. - * The model is expected to contain exactly one Aspect. + * Serializes an {@link AspectModelFile} to RDF/Turtle while following the formatting rules for Aspect Models. + * Initialize with {@link #PrettyPrinter(AspectModelFile, PrintWriter)}, then run {@link #print()}. */ public class PrettyPrinter { private static final String INDENT = " "; private static final String TRIPLE_QUOTE = "\"\"\""; private static final String LINE_BREAK = "\n"; + private final Comparator elementDefinitionOrder; private final Comparator propertyOrder; private final Comparator> prefixOrder; private final Set processedResources = new HashSet<>(); private final Queue resourceQueue = new ArrayDeque<>(); + private final AspectModelFile modelFile; private final Model model; - private final AspectModelUrn rootElementUrn; private final PrintWriter writer; - private final Map prefixMap; - private final List headerComment; private final PrintVisitor printVisitor; - private PrettyPrinter( final Model model, final AspectModelUrn rootElementUrn, final PrintWriter writer, - final List headerComment ) { - this.writer = writer; - this.rootElementUrn = rootElementUrn; - this.model = ModelFactory.createDefaultModel(); - this.model.add( model ); - this.headerComment = List.of(); - - prefixMap = new HashMap<>( RdfNamespace.createPrefixMap() ); - prefixMap.putAll( model.getNsPrefixMap() ); - prefixMap.put( "", rootElementUrn.getUrnPrefix() ); - this.model.setNsPrefixes( prefixMap ); - - propertyOrder = createPredefinedPropertyOrder(); - prefixOrder = createPredefinedPrefixOrder(); - printVisitor = new PrintVisitor( this.model ); - } - - public PrettyPrinter( final Model model, final AspectModelUrn rootElementUrn, final PrintWriter writer ) { - this( model, rootElementUrn, writer, List.of() ); - } - /** * Creates a new Pretty Printer for a given Aspect Model File. * @@ -112,7 +90,38 @@ public PrettyPrinter( final Model model, final AspectModelUrn rootElementUrn, fi * @param writer the writer to write to */ public PrettyPrinter( final AspectModelFile modelFile, final PrintWriter writer ) { - this( modelFile.sourceModel(), modelFile.aspect().urn(), writer, modelFile.headerComment() ); + this.modelFile = modelFile; + this.writer = writer; + model = ModelFactory.createDefaultModel(); + model.add( modelFile.sourceModel() ); + RdfUtil.cleanPrefixes( model ); + + elementDefinitionOrder = createElementDefinitionOrder(); + propertyOrder = createPredefinedPropertyOrder(); + prefixOrder = createPredefinedPrefixOrder(); + printVisitor = new PrintVisitor( model ); + } + + private Comparator createElementDefinitionOrder() { + return Comparator.comparingInt( element -> { + final Resource resource = element.getSourceFile().sourceModel().createResource( element.urn().toString() ); + final StmtIterator iterator = resource.getModel().listStatements( resource, RDF.type, (RDFNode) null ); + if ( iterator.hasNext() ) { + final Statement statement = iterator.next(); + if ( statement.getSubject().asNode() instanceof final UriNode uriNode ) { + return uriNode.getToken().line(); + } else { + // This happens when the model was not loaded using the esmf-sdk customized RDF parser, e.g. + // for programmatically created models. + // Fall back to inherent RDF order (i.e. no order). At least try to keep samm:Aspect on the top. + if ( statement.getObject().isURIResource() && statement.getResource().equals( SammNs.SAMM.Aspect() ) ) { + return 0; + } + return 1; + } + } + return Integer.MAX_VALUE; + } ); } private Comparator createPredefinedPropertyOrder() { @@ -177,21 +186,19 @@ private Comparator> createPredefinedPrefixOrder() { .thenComparing( Map.Entry::getKey ); } - private Properties loadProperties( final String filename ) { + private void showMilestoneBanner() { final Properties properties = new Properties(); - final InputStream propertiesResource = PrettyPrinter.class.getClassLoader().getResourceAsStream( filename ); + final InputStream propertiesResource = PrettyPrinter.class.getClassLoader().getResourceAsStream( "pom.properties" ); + if ( propertiesResource == null ) { + return; + } try { properties.load( propertiesResource ); } catch ( final IOException exception ) { - throw new RuntimeException( "Failed to load Properties: " + filename ); + return; } - return properties; - } - private void showMilestoneBanner() { - // property file generated by properties-maven-plugin at build time - final Properties applicationProperties = loadProperties( "pom.properties" ); - final String version = applicationProperties.get( "aspect-meta-model-version" ).toString(); + final String version = properties.get( "aspect-meta-model-version" ).toString(); if ( version.contains( "-M" ) ) { writer.println( "# This model was created using SAMM version " + version + " and is not intended for productive usage." ); writer.println(); @@ -202,21 +209,26 @@ private void showMilestoneBanner() { * Print to the PrintWriter given in the constructor. This method does not close the PrintWriter. */ public void print() { - for ( final String line : headerComment ) { - writer.println( line ); + for ( final String line : modelFile.headerComment() ) { + writer.println( "# " + line ); } - if ( headerComment.size() > 1 ) { + if ( modelFile.headerComment().size() > 1 ) { writer.println(); } showMilestoneBanner(); - prefixMap.entrySet().stream().sorted( prefixOrder ) + model.getNsPrefixMap().entrySet().stream().sorted( prefixOrder ) .forEach( entry -> writer.format( "@prefix %s: <%s> .%n", entry.getKey(), entry.getValue() ) ); writer.println(); - final Resource rootElementResource = ResourceFactory.createResource( rootElementUrn.toString() ); - resourceQueue.add( rootElementResource ); + modelFile.elements().stream() + .filter( element -> !element.isAnonymous() ) + .sorted( elementDefinitionOrder ) + .map( ModelElement::urn ) + .map( urn -> ResourceFactory.createResource( urn.toString() ) ) + .forEach( resourceQueue::add ); + while ( !resourceQueue.isEmpty() ) { final Resource resource = resourceQueue.poll(); writer.print( processElement( resource, 0 ) ); @@ -246,8 +258,28 @@ private String serializeList( final Resource list, final int indentationLevel ) return "( )"; } - return list.as( RDFList.class ).asJavaList().stream().map( listNode -> serialize( listNode, indentationLevel ) ) - .collect( Collectors.joining( " ", "( ", " )" ) ); + final List listContent = list.as( RDFList.class ).asJavaList(); + final long anonymousNodesInList = listContent.stream().filter( RDFNode::isAnon ).count(); + if ( listContent.size() <= 3 && anonymousNodesInList <= 1 ) { + return listContent.stream() + .map( listNode -> serialize( listNode, indentationLevel ) ) + .collect( Collectors.joining( " ", "( ", " )" ) ); + } + final StringBuilder builder = new StringBuilder(); + builder.append( "(\n" ); + int i = 0; + for ( final RDFNode listNode : listContent ) { + builder.append( INDENT.repeat( indentationLevel + 2 ) ); + builder.append( serialize( listNode, indentationLevel + 2 ) ); + i++; + if ( i < listContent.size() ) { + builder.append( "\n" ); + } + } + builder.append( "\n" ); + builder.append( INDENT.repeat( indentationLevel + 1 ) ); + builder.append( ")" ); + return builder.toString(); } private String serialize( final RDFNode rdfNode, final int indentationLevel ) { @@ -300,9 +332,9 @@ private void escapeStringAndAppendToBuilder( final String input, final StringBui int index = 0; while ( index < chars.length ) { final boolean indexAtUnicodeEscapeSequence = chars[index] == '\\' - && (index + 1) < chars.length + && ( index + 1 ) < chars.length && chars[index + 1] == 'u' - && (index + 5) <= (chars.length - 1); + && ( index + 5 ) <= ( chars.length - 1 ); if ( indexAtUnicodeEscapeSequence ) { final long codepoint = Long.parseLong( new String( chars, index + 2, 4 ), 16 ); builder.append( (char) codepoint ); @@ -357,7 +389,7 @@ private String serializeResource( final RDFNode rdfNode, final int indentationLe return print( resource ); } - if ( (resource.isURIResource() && resource.getURI().equals( RDF.nil.getURI() )) + if ( ( resource.isURIResource() && resource.getURI().equals( RDF.nil.getURI() ) ) || statements( resource, RDF.first, null ).iterator().hasNext() ) { return serializeList( resource, indentationLevel ); } @@ -401,9 +433,9 @@ private String processElement( final Resource element, final int indentationLeve .map( statement -> String.format( "%s%s %s", INDENT.repeat( indentationLevel + 1 ), serialize( statement.getPredicate(), indentationLevel ), serialize( statement.getObject(), indentationLevel ) ) ) - .collect( Collectors.joining( String.format( " ;%n" ), "", (element.isAnon() + .collect( Collectors.joining( String.format( " ;%n" ), "", ( element.isAnon() ? String.format( " %n%s]", INDENT.repeat( indentationLevel ) ) - : "") ) ); + : "" ) ) ); if ( body.isEmpty() ) { return String.format( "%s .%n%n", firstLine ); } else { @@ -414,7 +446,7 @@ private String processElement( final Resource element, final int indentationLeve if ( body.endsWith( "]" ) && indentationLevel >= 1 ) { return firstPart; } - return String.format( (indentationLevel >= 1 ? "%s ;%n" : "%s .%n%n"), firstPart ); + return String.format( ( indentationLevel >= 1 ? "%s ;%n" : "%s .%n%n" ), firstPart ); } } @@ -426,7 +458,6 @@ private String print( final RDFNode node ) { } record PrintVisitor( Model model ) implements NodeVisitor { - @Override public Object visitAny( final Node_ANY it ) { return "*"; @@ -445,7 +476,7 @@ public Object visitLiteral( final Node_Literal it, final LiteralLabel lit ) { lf = lf.replace( singleQuote, "\\'" ); } // RDF 1.1 : Print xsd:string without ^^xsd:string - return singleQuote + lf + singleQuote + (Util.isSimpleString( it ) ? "" : "^^" + it.getLiteralDatatypeURI()); + return singleQuote + lf + singleQuote + ( Util.isSimpleString( it ) ? "" : "^^" + it.getLiteralDatatypeURI() ); } @Override diff --git a/core/esmf-aspect-model-serializer/src/main/java/org/eclipse/esmf/aspectmodel/serializer/RdfModelCreatorVisitor.java b/core/esmf-aspect-model-serializer/src/main/java/org/eclipse/esmf/aspectmodel/serializer/RdfModelCreatorVisitor.java index d61b6c9a6..f1e07a842 100644 --- a/core/esmf-aspect-model-serializer/src/main/java/org/eclipse/esmf/aspectmodel/serializer/RdfModelCreatorVisitor.java +++ b/core/esmf-aspect-model-serializer/src/main/java/org/eclipse/esmf/aspectmodel/serializer/RdfModelCreatorVisitor.java @@ -144,7 +144,7 @@ private Resource getElementResource( final ModelElement element ) { private Model serializeDescriptions( final Resource elementResource, final ModelElement element ) { final Model model = ModelFactory.createDefaultModel(); - element.getSee().forEach( seeValue -> model.add( elementResource, SammNs.SAMM.see(), ResourceFactory.createResource( seeValue ) ) ); + element.getSee().forEach( seeValue -> model.add( elementResource, SammNs.SAMM.see(), createResource( seeValue ) ) ); element.getPreferredNames().stream().map( this::serializeLocalizedString ).forEach( preferredName -> model.add( elementResource, SammNs.SAMM.preferredName(), preferredName ) ); element.getDescriptions().stream().map( this::serializeLocalizedString ).forEach( description -> diff --git a/core/esmf-aspect-model-serializer/src/main/java/org/eclipse/esmf/aspectmodel/serializer/SerializationException.java b/core/esmf-aspect-model-serializer/src/main/java/org/eclipse/esmf/aspectmodel/serializer/SerializationException.java new file mode 100644 index 000000000..9d44b32e0 --- /dev/null +++ b/core/esmf-aspect-model-serializer/src/main/java/org/eclipse/esmf/aspectmodel/serializer/SerializationException.java @@ -0,0 +1,29 @@ +/* + * 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.serializer; + +import java.io.Serial; + +public class SerializationException extends RuntimeException { + @Serial + private static final long serialVersionUID = 8685799345891779111L; + + public SerializationException( final String message ) { + super( message ); + } + + public SerializationException( final Throwable cause ) { + super( cause ); + } +} diff --git a/core/esmf-aspect-model-serializer/src/test/java/org/eclipse/esmf/aspectmodel/serializer/AspectSerializerTest.java b/core/esmf-aspect-model-serializer/src/test/java/org/eclipse/esmf/aspectmodel/serializer/AspectSerializerTest.java new file mode 100644 index 000000000..8ae6f3a1f --- /dev/null +++ b/core/esmf-aspect-model-serializer/src/test/java/org/eclipse/esmf/aspectmodel/serializer/AspectSerializerTest.java @@ -0,0 +1,161 @@ +/* + * 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.serializer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.esmf.aspectmodel.RdfUtil.createModel; +import static org.eclipse.esmf.aspectmodel.serializer.RdfComparison.modelToString; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.Optional; +import java.util.stream.Stream; + +import org.eclipse.esmf.aspectmodel.AspectModelFile; +import org.eclipse.esmf.aspectmodel.edit.AspectChangeManager; +import org.eclipse.esmf.aspectmodel.edit.ChangeGroup; +import org.eclipse.esmf.aspectmodel.edit.change.AddAspectModelFile; +import org.eclipse.esmf.aspectmodel.edit.change.MoveRenameAspectModelFile; +import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader; +import org.eclipse.esmf.aspectmodel.resolver.modelfile.RawAspectModelFileBuilder; +import org.eclipse.esmf.aspectmodel.resolver.services.TurtleLoader; +import org.eclipse.esmf.metamodel.AspectModel; +import org.eclipse.esmf.test.TestAspect; +import org.eclipse.esmf.test.TestResources; + +import io.vavr.control.Try; +import org.apache.jena.rdf.model.Model; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +public class AspectSerializerTest { + Path outputDirectory = null; + + @BeforeEach + void beforeEach() throws IOException { + outputDirectory = Files.createTempDirectory( "junit" ); + } + + @AfterEach + void afterEach() { + if ( outputDirectory != null ) { + final File outputDir = outputDirectory.toFile(); + if ( outputDir.exists() && outputDir.isDirectory() ) { + // Recursively delete temporary directory + try ( final Stream paths = Files.walk( outputDirectory ) ) { + paths.sorted( Comparator.reverseOrder() ) + .map( Path::toFile ) + .forEach( file -> { + if ( !file.delete() ) { + System.err.println( "Could not delete file " + file ); + } + } ); + } catch ( final IOException e ) { + throw new RuntimeException( e ); + } + } + } + } + + @ParameterizedTest + @EnumSource( value = TestAspect.class, + mode = EnumSource.Mode.EXCLUDE, + names = { "MODEL_WITH_BLANK_AND_ADDITIONAL_NODES" } ) + void testSerializeAspectModelFile( final TestAspect testAspect ) { + final AspectModel aspectModel = TestResources.load( testAspect ); + for ( final AspectModelFile file : aspectModel.files() ) { + final Model originalModel = file.sourceModel(); + + final String modelString = AspectSerializer.INSTANCE.aspectModelFileToString( file ); + final Try tryModel = TurtleLoader.loadTurtle( modelString ); + final Model serializedModel = tryModel.getOrElseThrow( () -> new RuntimeException( tryModel.getCause() ) ); + + serializedModel.clearNsPrefixMap(); + originalModel.getNsPrefixMap().forEach( serializedModel::setNsPrefix ); + + final String serializedModelString = modelToString( serializedModel ); + final String originalModelString = modelToString( originalModel ); + assertThat( serializedModelString ).isEqualToIgnoringWhitespace( originalModelString ); + } + } + + @Test + void testWriteAspectModelFileToFileSystem() { + final AspectModel aspectModel = TestResources.load( TestAspect.ASPECT ); + + // Change the loaded test model's source location to a file system location + final AspectChangeManager changeContext = new AspectChangeManager( aspectModel ); + final Path filePath = outputDirectory.resolve( "Aspect.ttl" ); + changeContext.applyChange( new MoveRenameAspectModelFile( aspectModel.files().iterator().next(), filePath.toUri() ) ); + + // Serialize the model file content to the file system + AspectSerializer.INSTANCE.write( aspectModel.files().iterator().next() ); + + final File writtenFile = filePath.toFile(); + assertThat( writtenFile ).content().contains( ":Aspect a samm:Aspect" ); + } + + @Test + void testWriteAspectModelToFileSystem() { + // Construct Aspect Model with two files from scratch + final AspectModel aspectModel = new AspectModelLoader().emptyModel(); + final Path file1Path = outputDirectory.resolve( "Aspect1.ttl" ); + final AspectModelFile file1 = RawAspectModelFileBuilder.builder() + .sourceLocation( Optional.of( file1Path.toUri() ) ) + .sourceModel( createModel( """ + @prefix samm: . + @prefix xsd: . + @prefix : . + + :Aspect1 a samm:Aspect ; + samm:description "This is a test description"@en ; + samm:properties ( ) ; + samm:operations ( ) . + """ + ) ) + .build(); + final Path file2Path = outputDirectory.resolve( "Aspect2.ttl" ); + final AspectModelFile file2 = RawAspectModelFileBuilder.builder() + .sourceLocation( Optional.of( file2Path.toUri() ) ) + .sourceModel( createModel( """ + @prefix samm: . + @prefix xsd: . + @prefix : . + + :Aspect2 a samm:Aspect ; + samm:description "This is a test description"@en ; + samm:properties ( ) ; + samm:operations ( ) . + """ + ) ) + .build(); + final AspectChangeManager changeContext = new AspectChangeManager( aspectModel ); + changeContext.applyChange( new ChangeGroup( + new AddAspectModelFile( file1 ), + new AddAspectModelFile( file2 ) + ) ); + + // Serialize the model to the file system + AspectSerializer.INSTANCE.write( aspectModel ); + + assertThat( file1Path.toFile() ).content().contains( ":Aspect1 a samm:Aspect" ); + assertThat( file2Path.toFile() ).content().contains( ":Aspect2 a samm:Aspect" ); + } +} diff --git a/core/esmf-aspect-model-serializer/src/test/java/org/eclipse/esmf/aspectmodel/serializer/PrettyPrinterTest.java b/core/esmf-aspect-model-serializer/src/test/java/org/eclipse/esmf/aspectmodel/serializer/PrettyPrinterTest.java index 60ea03e03..4e8113561 100644 --- a/core/esmf-aspect-model-serializer/src/test/java/org/eclipse/esmf/aspectmodel/serializer/PrettyPrinterTest.java +++ b/core/esmf-aspect-model-serializer/src/test/java/org/eclipse/esmf/aspectmodel/serializer/PrettyPrinterTest.java @@ -20,34 +20,46 @@ import java.io.InputStream; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.stream.Stream; +import org.eclipse.esmf.aspectmodel.AspectModelFile; import org.eclipse.esmf.aspectmodel.resolver.services.TurtleLoader; import org.eclipse.esmf.metamodel.AspectModel; import org.eclipse.esmf.test.TestAspect; +import org.eclipse.esmf.test.TestModel; +import org.eclipse.esmf.test.TestProperty; import org.eclipse.esmf.test.TestResources; import org.apache.jena.rdf.model.Model; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public class PrettyPrinterTest { @ParameterizedTest - @EnumSource( value = TestAspect.class, mode = EnumSource.Mode.EXCLUDE, names = { - // contains blank nodes which are not referenced from an aspect and therefore not pretty-printed - "MODEL_WITH_BLANK_AND_ADDITIONAL_NODES" - } ) - public void testPrettyPrinter( final TestAspect testAspect ) { - final AspectModel aspectModel = TestResources.load( testAspect ); - final Model originalModel = aspectModel.files().iterator().next().sourceModel(); + @MethodSource( value = "testModels" ) + void testPrettyPrinter( final TestModel testModel ) { + final AspectModel aspectModel = TestResources.load( testModel ); + final AspectModelFile originalFile = aspectModel.files().iterator().next(); final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); final PrintWriter writer = new PrintWriter( buffer, false, StandardCharsets.UTF_8 ); - new PrettyPrinter( originalModel, testAspect.getUrn(), writer ).print(); + new PrettyPrinter( originalFile, writer ).print(); writer.flush(); final InputStream bufferInput = new ByteArrayInputStream( buffer.toByteArray() ); - final Model prettyPrintedModel = TurtleLoader.loadTurtle( buffer.toString( StandardCharsets.UTF_8 ) ).get(); + final String formattedModel = buffer.toString( StandardCharsets.UTF_8 ); + final Model prettyPrintedModel = TurtleLoader.loadTurtle( formattedModel ).get(); - assertThat( RdfComparison.hash( originalModel ).equals( RdfComparison.hash( prettyPrintedModel ) ) ).isTrue(); + assertThat( RdfComparison.hash( originalFile.sourceModel() ).equals( RdfComparison.hash( prettyPrintedModel ) ) ).isTrue(); + } + + static Stream testModels() { + return Stream.concat( Arrays.stream( TestAspect.values() ), Arrays.stream( TestProperty.values() ) + ).filter( testModel -> + // contains blank nodes which are not referenced from an aspect and therefore not pretty-printed + testModel != TestAspect.MODEL_WITH_BLANK_AND_ADDITIONAL_NODES ) + .map( Arguments::arguments ); } } diff --git a/core/esmf-aspect-model-serializer/src/test/java/org/eclipse/esmf/aspectmodel/serializer/RdfComparison.java b/core/esmf-aspect-model-serializer/src/test/java/org/eclipse/esmf/aspectmodel/serializer/RdfComparison.java index 75f004567..7e6586581 100644 --- a/core/esmf-aspect-model-serializer/src/test/java/org/eclipse/esmf/aspectmodel/serializer/RdfComparison.java +++ b/core/esmf-aspect-model-serializer/src/test/java/org/eclipse/esmf/aspectmodel/serializer/RdfComparison.java @@ -13,9 +13,12 @@ package org.eclipse.esmf.aspectmodel.serializer; +import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; +import org.eclipse.esmf.test.TestModel; + import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.RDFList; import org.apache.jena.rdf.model.RDFNode; @@ -58,4 +61,31 @@ private static String hashAnonymousResource( final Resource resource ) { .sorted() .collect( Collectors.joining() ); } + + static String modelToString( final Model model ) { + return Arrays.stream( TestModel.modelToString( model ) + .replaceAll( ";", "" ) + .replaceAll( "\\.", "" ) + .replaceAll( " +", "" ) + .split( "\\n" ) ) + .filter( line -> !line.contains( "samm-c:values" ) ) + .filter( line -> !line.contains( "samm:see" ) ) + .map( RdfComparison::sortLineWithRdfListOrLangString ) + .sorted() + .collect( Collectors.joining() ) + .replaceAll( " +", " " ); + } + + /** + * In some test models, lines with RDF lists appear, e.g.: + * :property ( "foo" "bar" ) + * However, for some serialized models, the order of elements is non-deterministic since the underlying collection is a Set. + * In order to check that the line is present in the two models, we simply tokenize and sort both lines, so we can compare them. + */ + static String sortLineWithRdfListOrLangString( final String line ) { + if ( line.contains( " ( " ) || line.contains( "@" ) ) { + return Arrays.stream( line.split( "[ ,\"]" ) ).sorted().collect( Collectors.joining() ); + } + return line; + } } diff --git a/core/esmf-aspect-model-serializer/src/test/java/org/eclipse/esmf/aspectmodel/serializer/RdfModelCreatorVisitorTest.java b/core/esmf-aspect-model-serializer/src/test/java/org/eclipse/esmf/aspectmodel/serializer/RdfModelCreatorVisitorTest.java index 01667f490..23446ec57 100644 --- a/core/esmf-aspect-model-serializer/src/test/java/org/eclipse/esmf/aspectmodel/serializer/RdfModelCreatorVisitorTest.java +++ b/core/esmf-aspect-model-serializer/src/test/java/org/eclipse/esmf/aspectmodel/serializer/RdfModelCreatorVisitorTest.java @@ -14,6 +14,7 @@ package org.eclipse.esmf.aspectmodel.serializer; import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.esmf.aspectmodel.serializer.RdfComparison.modelToString; import java.util.Arrays; import java.util.HashMap; @@ -23,6 +24,7 @@ import org.eclipse.esmf.metamodel.Aspect; import org.eclipse.esmf.metamodel.AspectModel; import org.eclipse.esmf.metamodel.vocabulary.RdfNamespace; +import org.eclipse.esmf.metamodel.vocabulary.SimpleRdfNamespace; import org.eclipse.esmf.samm.KnownVersion; import org.eclipse.esmf.test.TestAspect; import org.eclipse.esmf.test.TestModel; @@ -58,24 +60,15 @@ public class RdfModelCreatorVisitorTest { "ASPECT_WITH_UMLAUT_DESCRIPTION", "MODEL_WITH_BROKEN_CYCLES", "MODEL_WITH_BLANK_AND_ADDITIONAL_NODES", - "ASPECT_WITH_TIME_SERIES" + "ASPECT_WITH_TIME_SERIES", + "ASPECT_WITH_NAMESPACE_DESCRIPTION" } ) public void testRdfModelCreatorVisitor( final TestAspect testAspect ) { final KnownVersion knownVersion = KnownVersion.getLatest(); final AspectModel aspectModel = TestResources.load( testAspect ); final Aspect aspect = aspectModel.aspect(); - final RdfNamespace namespace = new RdfNamespace() { - @Override - public String getShortForm() { - return ""; - } - - @Override - public String getUri() { - return aspect.urn().getUrnPrefix(); - } - }; + final RdfNamespace namespace = new SimpleRdfNamespace( "", aspect.urn().getUrnPrefix() ); final RdfModelCreatorVisitor visitor = new RdfModelCreatorVisitor( namespace ); final Model serializedModel = visitor.visitAspect( aspect, null ).model(); @@ -99,30 +92,5 @@ public String getUri() { assertThat( serializedModelString ).isEqualToIgnoringWhitespace( originalModelString ); } - private String modelToString( final Model model ) { - return Arrays.stream( TestModel.modelToString( model ) - .replaceAll( ";", "" ) - .replaceAll( "\\.", "" ) - .replaceAll( " +", "" ) - .split( "\\n" ) ) - .filter( line -> !line.contains( "samm-c:values" ) ) - .filter( line -> !line.contains( "samm:see" ) ) - .map( this::sortLineWithRdfListOrLangString ) - .sorted() - .collect( Collectors.joining() ) - .replaceAll( " +", " " ); - } - /** - * In some test models, lines with RDF lists appear, e.g.: - * :property ( "foo" "bar" ) - * However, for some serialized models, the order of elements is non-deterministic since the underlying collection is a Set. - * In order to check that the line is present in the two models, we simply tokenize and sort both lines, so we can compare them. - */ - private String sortLineWithRdfListOrLangString( final String line ) { - if ( line.contains( " ( " ) || line.contains( "@" ) ) { - return Arrays.stream( line.split( "[ ,\"]" ) ).sorted().collect( Collectors.joining() ); - } - return line; - } } diff --git a/core/esmf-aspect-model-urn/src/main/java/org/eclipse/esmf/aspectmodel/urn/AspectModelUrn.java b/core/esmf-aspect-model-urn/src/main/java/org/eclipse/esmf/aspectmodel/urn/AspectModelUrn.java index 6c108bd0e..a0eedea6e 100644 --- a/core/esmf-aspect-model-urn/src/main/java/org/eclipse/esmf/aspectmodel/urn/AspectModelUrn.java +++ b/core/esmf-aspect-model-urn/src/main/java/org/eclipse/esmf/aspectmodel/urn/AspectModelUrn.java @@ -70,18 +70,18 @@ public class AspectModelUrn implements Comparable { private final String name; private final String version; - private final String namespace; + private final String namespaceMainPart; private final ElementType elementType; private final boolean isSammUrn; @JsonValue private final URI urn; - private AspectModelUrn( final URI urn, final String name, final String namespace, final ElementType elementType, + private AspectModelUrn( final URI urn, final String name, final String namespaceMainPart, final ElementType elementType, final String version, final boolean isSammUrn ) { this.urn = urn; this.name = name; - this.namespace = namespace; + this.namespaceMainPart = namespaceMainPart; this.elementType = elementType; this.version = version; this.isSammUrn = isSammUrn; @@ -114,7 +114,19 @@ public static AspectModelUrn fromUrn( final String urn ) { public static AspectModelUrn fromUrn( final URI urn ) { checkNotEmpty( urn ); - final List urnParts = ImmutableList.copyOf( urn.toString().split( "[:|#]" ) ); + final List urnParts; + if ( urn.toString().contains( "#" ) ) { + urnParts = ImmutableList.copyOf( urn.toString().split( "[:|#]" ) ); + } else { + final String[] parts = urn.toString().split( ":" ); + final ImmutableList.Builder builder = ImmutableList.builder(); + for ( final String part : parts ) { + builder.add( part ); + } + builder.add( "" ); + urnParts = builder.build(); + } + final int numberOfUrnParts = urnParts.size(); checkUrn( numberOfUrnParts >= 5, UrnSyntaxException.URN_IS_MISSING_SECTIONS_MESSAGE ); @@ -264,7 +276,7 @@ private static String getName( final boolean isSammUrn, final List urnPa } final String modelElementName = urnParts.get( ASPECT_NAME_INDEX ); - checkElementName( modelElementName, "aspect" ); + checkElementName( modelElementName, "model element" ); return modelElementName; } @@ -284,6 +296,9 @@ private static boolean isSammUrn( final URI urn, final List urnParts, } private static void checkElementName( final String modelElementName, final String elementTypeForErrorMessage ) { + if ( modelElementName.isEmpty() ) { + return; + } checkUrn( modelElementName.matches( MODEL_ELEMENT_NAME_REGEX ), UrnSyntaxException.URN_INVALID_ELEMENT_NAME_MESSAGE, elementTypeForErrorMessage, MODEL_ELEMENT_NAME_REGEX, modelElementName ); @@ -329,19 +344,40 @@ public String getVersion() { * Returns the namespace part of the URN, e.g. com.example.foo * * @return the namespace part of the URN + * @deprecated Use {@link #getNamespaceMainPart()} instead */ + @Deprecated( forRemoval = true ) public String getNamespace() { - return namespace; + return namespaceMainPart; + } + + /** + * Returns the namespace part of the URN, e.g. com.example.foo + * + * @return the namespace part of the URN + */ + public String getNamespaceMainPart() { + return namespaceMainPart; + } + + /** + * Returns the namespace identifier, i.e. the part of the URN before the # symbol + * e.g. urn:samm:com.foo.example:1.0.0 + * + * @return the prefix part of the URN + */ + public String getNamespaceIdentifier() { + return urn.toString().split( "#" )[0]; } /** - * Returns prefix part of the URN, i.e. the part up to and including the # but not including the local name, + * Returns the RDF prefix part of the URN, i.e. the part up to and including the # but not including the local name, * e.g. urn:samm:com.foo.example:1.0.0# * * @return the prefix part of the URN */ public String getUrnPrefix() { - return urn.toString().split( "#" )[0] + "#"; + return getNamespaceIdentifier() + "#"; } public ElementType getElementType() { diff --git a/core/esmf-aspect-model-urn/src/test/java/org/eclipse/esmf/aspectmodel/urn/AspectModelUrnTest.java b/core/esmf-aspect-model-urn/src/test/java/org/eclipse/esmf/aspectmodel/urn/AspectModelUrnTest.java index 5a2649c45..2226dd9e1 100644 --- a/core/esmf-aspect-model-urn/src/test/java/org/eclipse/esmf/aspectmodel/urn/AspectModelUrnTest.java +++ b/core/esmf-aspect-model-urn/src/test/java/org/eclipse/esmf/aspectmodel/urn/AspectModelUrnTest.java @@ -40,32 +40,14 @@ void createFromValidUrn() throws URISyntaxException { assertModelElementUrn( aspectModelUrn, "E2", "org.eclipse.esmf.test" ); } - @Test - void createFromValidLegacyUrn() throws URISyntaxException { - URI validUrn = new URI( baseUri + "aspect-model:Errors:1.0.0" ); - AspectModelUrn aspectModelUrn = AspectModelUrn.fromUrn( validUrn ); - - assertAspectModelUrn( aspectModelUrn, "Errors", "org.eclipse.esmf.test" ); - - validUrn = new URI( baseUri + "aspect-model:E2:1.0.0" ); - aspectModelUrn = AspectModelUrn.fromUrn( validUrn ); - - assertAspectModelUrn( aspectModelUrn, "E2", "org.eclipse.esmf.test" ); - - // Check that URNs using the schema of BAMM (which at the end of 2022 was renamed to SAMM) are still valid - final URI validBammUrn = new URI( "urn:bamm:com.example:1.0.0#MyAspect" ); - final AspectModelUrn bammUrn = AspectModelUrn.fromUrn( validBammUrn ); - assertModelElementUrn( bammUrn, "MyAspect", "com.example" ); - } - @Test void createFromValidUrnMaxLength() throws URISyntaxException { final String namespace = Strings.repeat( "x", 62 ); final URI validUrn = new URI( - "urn:samm:" + namespace + ".test:aspect-model:Errors:1.0.0" ); + "urn:samm:" + namespace + ".test:1.0.0#Errors" ); final AspectModelUrn aspectModelUrn = AspectModelUrn.fromUrn( validUrn ); - assertAspectModelUrn( aspectModelUrn, "Errors", namespace + ".test" ); + assertModelElementUrn( aspectModelUrn, "Errors", namespace + ".test" ); } @Test @@ -122,15 +104,7 @@ void createFromUrnInvalidNamespace() throws URISyntaxException { } @Test - void createFromUrnInvalidAspectName() { - assertThatExceptionOfType( UrnSyntaxException.class ) - .isThrownBy( () -> AspectModelUrn.fromUrn( new URI( baseUri + "aspect-model:123Error:1.0.0" ) ) ) - .withMessage( "The aspect name must match \\p{Alpha}\\p{Alnum}*: 123Error" ); - - assertThatExceptionOfType( UrnSyntaxException.class ) - .isThrownBy( () -> AspectModelUrn.fromUrn( new URI( baseUri + "aspect-model:Error?s:1.0.0" ) ) ) - .withMessage( "The aspect name must match \\p{Alpha}\\p{Alnum}*: Error?s" ); - + void createFromUrnInvalidAspectName() throws URISyntaxException { assertThatExceptionOfType( UrnSyntaxException.class ) .isThrownBy( () -> AspectModelUrn.fromUrn( new URI( sammBaseUri + "meta-model:1.0.0#Aspe?ct" ) ) ) .withMessage( "The meta model element name must match \\p{Alpha}\\p{Alnum}*: Aspe?ct" ); @@ -139,25 +113,13 @@ void createFromUrnInvalidAspectName() { .isThrownBy( () -> AspectModelUrn.fromUrn( new URI( sammBaseUri + "characteristic:1.0.0#Eit?her" ) ) ) .withMessage( "The characteristic name must match \\p{Alpha}\\p{Alnum}*: Eit?her" ); - assertThatExceptionOfType( UrnSyntaxException.class ) - .isThrownBy( () -> AspectModelUrn.fromUrn( new URI( baseUri + "characteristic:Eit?her:1.0.0" ) ) ) - .withMessage( "The characteristic name must match \\p{Alpha}\\p{Alnum}*: Eit?her" ); - assertThatExceptionOfType( UrnSyntaxException.class ) .isThrownBy( () -> AspectModelUrn.fromUrn( new URI( sammBaseUri + "entity:1.0.0#Time?Series" ) ) ) .withMessage( "The entity name must match \\p{Alpha}\\p{Alnum}*: Time?Series" ); - assertThatExceptionOfType( UrnSyntaxException.class ) - .isThrownBy( () -> AspectModelUrn.fromUrn( new URI( baseUri + "entity:Time?Series:1.0.0" ) ) ) - .withMessage( "The entity name must match \\p{Alpha}\\p{Alnum}*: Time?Series" ); - assertThatExceptionOfType( UrnSyntaxException.class ) .isThrownBy( () -> AspectModelUrn.fromUrn( new URI( baseUri + "unit:1.0.0#lit?re" ) ) ) .withMessage( "The unit name must match \\p{Alpha}\\p{Alnum}*: lit?re" ); - - assertThatExceptionOfType( UrnSyntaxException.class ) - .isThrownBy( () -> AspectModelUrn.fromUrn( new URI( baseUri + "unit:lit?re:1.0.0" ) ) ) - .withMessage( "The unit name must match \\p{Alpha}\\p{Alnum}*: lit?re" ); } @Test @@ -245,12 +207,7 @@ private void assertModelElementUrn( final AspectModelUrn aspectModelUrn, final S final String nameSpace ) { assertThat( aspectModelUrn.getName() ).isEqualTo( name ); assertThat( aspectModelUrn.getVersion() ).isEqualTo( "1.0.0" ); - assertThat( aspectModelUrn.getNamespace() ).isEqualTo( nameSpace ); - } - - private void assertAspectModelUrn( final AspectModelUrn aspectModelUrn, final String name, final String nameSpace ) { - assertModelElementUrn( aspectModelUrn, name, nameSpace ); - assertThat( aspectModelUrn.getElementType() ).isEqualTo( ElementType.ASPECT_MODEL ); + assertThat( aspectModelUrn.getNamespaceMainPart() ).isEqualTo( nameSpace ); } @Test @@ -260,7 +217,7 @@ void createUrnForModelElement() throws URISyntaxException { assertThat( elementUrn.getName() ).isEqualTo( "property" ); assertThat( elementUrn.getVersion() ).isEqualTo( "1.0.0" ); - assertThat( elementUrn.getNamespace() ).isEqualTo( "org.eclipse.esmf.test" ); + assertThat( elementUrn.getNamespaceMainPart() ).isEqualTo( "org.eclipse.esmf.test" ); assertThat( elementUrn.getElementType() ).isEqualTo( ElementType.ASPECT_MODEL_ELEMENT ); assertThat( elementUrn.isSammUrn() ).isFalse(); } @@ -272,21 +229,21 @@ void createUrnForSammCharacteristic() throws URISyntaxException { assertThat( metaModelElementUrn.getName() ).isEqualTo( "Either" ); assertThat( metaModelElementUrn.getVersion() ).isEqualTo( "1.0.0" ); - assertThat( metaModelElementUrn.getNamespace() ).isEqualTo( "org.eclipse.esmf.samm" ); + assertThat( metaModelElementUrn.getNamespaceMainPart() ).isEqualTo( "org.eclipse.esmf.samm" ); assertThat( metaModelElementUrn.getElementType() ).isEqualTo( ElementType.CHARACTERISTIC ); assertThat( metaModelElementUrn.isSammUrn() ).isTrue(); } @Test void createUrnForCharacteristic() throws URISyntaxException { - final URI validUrn = new URI( baseUri + "characteristic:TestCharacteristic:1.0.0" ); + final URI validUrn = new URI( baseUri + "characteristic:1.0.0#TestCharacteristic" ); final AspectModelUrn elementUrn = AspectModelUrn.fromUrn( validUrn ); assertThat( elementUrn.getName() ).isEqualTo( "TestCharacteristic" ); assertThat( elementUrn.getVersion() ).isEqualTo( "1.0.0" ); - assertThat( elementUrn.getNamespace() ).isEqualTo( "org.eclipse.esmf.test" ); + assertThat( elementUrn.getNamespaceMainPart() ).isEqualTo( "org.eclipse.esmf.test" ); assertThat( elementUrn.getElementType() ).isEqualTo( ElementType.CHARACTERISTIC ); - assertThat( elementUrn.isSammUrn() ).isFalse(); + assertThat( elementUrn.isSammUrn() ).isTrue(); } @Test @@ -296,7 +253,7 @@ void createUrnForCharacteristicModelElement() throws URISyntaxException { assertThat( elementUrn.getName() ).isEqualTo( "RightCharacteristic" ); assertThat( elementUrn.getVersion() ).isEqualTo( "1.0.0" ); - assertThat( elementUrn.getNamespace() ).isEqualTo( "org.eclipse.esmf.test" ); + assertThat( elementUrn.getNamespaceMainPart() ).isEqualTo( "org.eclipse.esmf.test" ); assertThat( elementUrn.getElementType() ).isEqualTo( ElementType.CHARACTERISTIC_MODEL_ELEMENT ); assertThat( elementUrn.isSammUrn() ).isFalse(); } @@ -308,7 +265,7 @@ void createUrnForSammEntity() throws URISyntaxException { assertThat( metaModelElementUrn.getName() ).isEqualTo( "TimeSeriesEntity" ); assertThat( metaModelElementUrn.getVersion() ).isEqualTo( "1.0.0" ); - assertThat( metaModelElementUrn.getNamespace() ).isEqualTo( "org.eclipse.esmf.samm" ); + assertThat( metaModelElementUrn.getNamespaceMainPart() ).isEqualTo( "org.eclipse.esmf.samm" ); assertThat( metaModelElementUrn.getElementType() ).isEqualTo( ElementType.ENTITY ); assertThat( metaModelElementUrn.isSammUrn() ).isTrue(); } @@ -320,21 +277,21 @@ void createUrnForSammEntityProperty() throws URISyntaxException { assertThat( metaModelElementUrn.getName() ).isEqualTo( "value" ); assertThat( metaModelElementUrn.getVersion() ).isEqualTo( "1.0.0" ); - assertThat( metaModelElementUrn.getNamespace() ).isEqualTo( "org.eclipse.esmf.samm" ); + assertThat( metaModelElementUrn.getNamespaceMainPart() ).isEqualTo( "org.eclipse.esmf.samm" ); assertThat( metaModelElementUrn.getElementType() ).isEqualTo( ElementType.ENTITY ); assertThat( metaModelElementUrn.isSammUrn() ).isTrue(); } @Test void createUrnForEntity() throws URISyntaxException { - final URI validUrn = new URI( baseUri + "entity:TestEntity:1.0.0" ); + final URI validUrn = new URI( baseUri + "entity:1.0.0#TestEntity" ); final AspectModelUrn elementUrn = AspectModelUrn.fromUrn( validUrn ); assertThat( elementUrn.getName() ).isEqualTo( "TestEntity" ); assertThat( elementUrn.getVersion() ).isEqualTo( "1.0.0" ); - assertThat( elementUrn.getNamespace() ).isEqualTo( "org.eclipse.esmf.test" ); + assertThat( elementUrn.getNamespaceMainPart() ).isEqualTo( "org.eclipse.esmf.test" ); assertThat( elementUrn.getElementType() ).isEqualTo( ElementType.ENTITY ); - assertThat( elementUrn.isSammUrn() ).isFalse(); + assertThat( elementUrn.isSammUrn() ).isTrue(); } @Test @@ -344,7 +301,7 @@ void createUrnForEntityProperty() throws URISyntaxException { assertThat( elementUrn.getName() ).isEqualTo( "property" ); assertThat( elementUrn.getVersion() ).isEqualTo( "1.0.0" ); - assertThat( elementUrn.getNamespace() ).isEqualTo( "org.eclipse.esmf.test" ); + assertThat( elementUrn.getNamespaceMainPart() ).isEqualTo( "org.eclipse.esmf.test" ); assertThat( elementUrn.getElementType() ).isEqualTo( ElementType.ENTITY_MODEL_ELEMENT ); assertThat( elementUrn.isSammUrn() ).isFalse(); } @@ -356,7 +313,7 @@ void createUrnForSammUnit() throws URISyntaxException { assertThat( metaModelElementUrn.getName() ).isEqualTo( "litre" ); assertThat( metaModelElementUrn.getVersion() ).isEqualTo( "1.0.0" ); - assertThat( metaModelElementUrn.getNamespace() ).isEqualTo( "org.eclipse.esmf.samm" ); + assertThat( metaModelElementUrn.getNamespaceMainPart() ).isEqualTo( "org.eclipse.esmf.samm" ); assertThat( metaModelElementUrn.getElementType() ).isEqualTo( ElementType.UNIT ); assertThat( metaModelElementUrn.isSammUrn() ).isTrue(); } @@ -368,7 +325,7 @@ void createUrnForUnit() throws URISyntaxException { assertThat( elementUrn.getName() ).isEqualTo( "litre" ); assertThat( elementUrn.getVersion() ).isEqualTo( "1.0.0" ); - assertThat( elementUrn.getNamespace() ).isEqualTo( "org.eclipse.esmf.test" ); + assertThat( elementUrn.getNamespaceMainPart() ).isEqualTo( "org.eclipse.esmf.test" ); assertThat( elementUrn.getElementType() ).isEqualTo( ElementType.UNIT ); assertThat( elementUrn.isSammUrn() ).isFalse(); } @@ -377,12 +334,12 @@ void createUrnForUnit() throws URISyntaxException { void validNamespaceTest() throws URISyntaxException { final URI validNamespaceUrnUnderscore = new URI( "urn:samm:org.eclipse.esmf_test:0.0.1#TestAspect" ); final AspectModelUrn elementUrnWithUnderscore = AspectModelUrn.fromUrn( validNamespaceUrnUnderscore ); - assertThat( elementUrnWithUnderscore.getNamespace() ).isNotEmpty(); - assertThat( elementUrnWithUnderscore.getNamespace() ).isEqualTo( "org.eclipse.esmf_test" ); + assertThat( elementUrnWithUnderscore.getNamespaceMainPart() ).isNotEmpty(); + assertThat( elementUrnWithUnderscore.getNamespaceMainPart() ).isEqualTo( "org.eclipse.esmf_test" ); final URI invalidNamespaceUrnDash = new URI( "urn:samm:org.eclipse.esmf-test:0.0.1#TestAspect" ); final AspectModelUrn elementUrnWithDash = AspectModelUrn.fromUrn( invalidNamespaceUrnDash ); - assertThat( elementUrnWithDash.getNamespace() ).isNotEmpty(); - assertThat( elementUrnWithDash.getNamespace() ).isEqualTo( "org.eclipse.esmf-test" ); + assertThat( elementUrnWithDash.getNamespaceMainPart() ).isNotEmpty(); + assertThat( elementUrnWithDash.getNamespaceMainPart() ).isEqualTo( "org.eclipse.esmf-test" ); } } diff --git a/core/esmf-aspect-model-validator/src/test/java/org/eclipse/esmf/aspectmodel/shacl/RustLikeFormatterTest.java b/core/esmf-aspect-model-validator/src/test/java/org/eclipse/esmf/aspectmodel/shacl/RustLikeFormatterTest.java index d045429c2..4bc3dd9db 100644 --- a/core/esmf-aspect-model-validator/src/test/java/org/eclipse/esmf/aspectmodel/shacl/RustLikeFormatterTest.java +++ b/core/esmf-aspect-model-validator/src/test/java/org/eclipse/esmf/aspectmodel/shacl/RustLikeFormatterTest.java @@ -14,20 +14,11 @@ package org.eclipse.esmf.aspectmodel.shacl; import static org.assertj.core.api.Assertions.assertThat; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -import org.eclipse.esmf.aspectmodel.resolver.parser.ReaderRiotTurtle; +import static org.eclipse.esmf.aspectmodel.RdfUtil.createModel; import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.RDFNode; import org.apache.jena.rdf.model.ResourceFactory; -import org.apache.jena.riot.Lang; -import org.apache.jena.riot.RDFLanguages; -import org.apache.jena.riot.RDFParserRegistry; import org.junit.jupiter.api.Test; public class RustLikeFormatterTest { @@ -38,7 +29,7 @@ public class RustLikeFormatterTest { @Test void testMiddleStatement() { - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; @@ -54,7 +45,7 @@ void testMiddleStatement() { @Test void testLastStatement() { - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; @@ -70,7 +61,7 @@ void testLastStatement() { @Test void testMultipleStatementsSameLine() { - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; @@ -85,7 +76,7 @@ void testMultipleStatementsSameLine() { @Test void testMultiSubjectSameLine() { - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :property 1 . :Bar a :TestClass ; :property 2 . @@ -99,7 +90,7 @@ void testMultiSubjectSameLine() { @Test void testAnonymousNodes() { - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; @@ -114,7 +105,7 @@ void testAnonymousNodes() { @Test void testMultilineAnonymousNode() { - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; @@ -131,7 +122,7 @@ void testMultilineAnonymousNode() { @Test void testMultilineAnonymousNodeMiddlePart() { - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; @@ -148,7 +139,7 @@ void testMultilineAnonymousNodeMiddlePart() { @Test void testEmptyList() { - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; @@ -163,7 +154,7 @@ void testEmptyList() { @Test void testList() { - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; @@ -178,7 +169,7 @@ void testList() { @Test void testMultilineListStarted() { - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; @@ -194,7 +185,7 @@ void testMultilineListStarted() { @Test void testMultilineListFinished() { - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; @@ -210,7 +201,7 @@ void testMultilineListFinished() { @Test void testListWithAnonymousNodes() { - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; @@ -225,7 +216,7 @@ void testListWithAnonymousNodes() { @Test void testDenseFormatting() { - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass;:property 1.:Bar a :TestClass;:property 2. @@ -242,12 +233,4 @@ private void assertCorrectFormatting( final String messageText, final String exp final String reconstructedLine = lineWithSourceText.substring( lineWithSourceText.indexOf( '|' ) + 1 ); assertThat( expectedLine ).isEqualTo( reconstructedLine.trim() ); } - - private Model model( final String ttlRepresentation ) { - final Model model = ModelFactory.createDefaultModel(); - final InputStream in = new ByteArrayInputStream( ttlRepresentation.getBytes( StandardCharsets.UTF_8 ) ); - RDFParserRegistry.registerLangTriples( Lang.TURTLE, ReaderRiotTurtle.factory ); - model.read( in, "", RDFLanguages.strLangTurtle ); - return model; - } } diff --git a/core/esmf-aspect-model-validator/src/test/java/org/eclipse/esmf/aspectmodel/shacl/ShaclValidatorTest.java b/core/esmf-aspect-model-validator/src/test/java/org/eclipse/esmf/aspectmodel/shacl/ShaclValidatorTest.java index 005f844cc..838a8e51b 100644 --- a/core/esmf-aspect-model-validator/src/test/java/org/eclipse/esmf/aspectmodel/shacl/ShaclValidatorTest.java +++ b/core/esmf-aspect-model-validator/src/test/java/org/eclipse/esmf/aspectmodel/shacl/ShaclValidatorTest.java @@ -14,14 +14,11 @@ package org.eclipse.esmf.aspectmodel.shacl; import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.esmf.aspectmodel.RdfUtil.createModel; -import java.io.ByteArrayInputStream; -import java.io.InputStream; import java.math.BigInteger; -import java.nio.charset.StandardCharsets; import java.util.List; -import org.eclipse.esmf.aspectmodel.resolver.parser.ReaderRiotTurtle; import org.eclipse.esmf.aspectmodel.shacl.constraint.DatatypeConstraint; import org.eclipse.esmf.aspectmodel.shacl.constraint.MinCountConstraint; import org.eclipse.esmf.aspectmodel.shacl.constraint.NodeKindConstraint; @@ -60,13 +57,9 @@ import org.apache.jena.graph.Node_URI; import org.apache.jena.rdf.model.Literal; import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.Property; import org.apache.jena.rdf.model.Resource; import org.apache.jena.rdf.model.ResourceFactory; -import org.apache.jena.riot.Lang; -import org.apache.jena.riot.RDFLanguages; -import org.apache.jena.riot.RDFParserRegistry; import org.apache.jena.vocabulary.XSD; import org.junit.jupiter.api.Test; @@ -80,7 +73,7 @@ public class ShaclValidatorTest { @Test public void testLoadingCustomShape() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix rdf: . @prefix sh: . @prefix xsd: . @@ -122,7 +115,7 @@ public void testLoadingCustomShape() { @Test public void testClassConstraintEvaluation() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix xsd: . @prefix : . @@ -138,7 +131,7 @@ public void testClassConstraintEvaluation() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . @prefix xsd: . @prefix rdfs: . @@ -179,7 +172,7 @@ public void testClassConstraintEvaluation() { @Test public void testDatatypeConstraintEvaluation() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix xsd: . @prefix : . @@ -195,7 +188,7 @@ public void testDatatypeConstraintEvaluation() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . @prefix xsd: . :Foo a :TestClass ; @@ -228,7 +221,7 @@ public void testDatatypeConstraintEvaluation() { @Test public void testNodeKindConstraintEvaluation() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix xsd: . @prefix : . @@ -244,7 +237,7 @@ public void testNodeKindConstraintEvaluation() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . @prefix xsd: . :Foo a :TestClass ; @@ -276,7 +269,7 @@ public void testNodeKindConstraintEvaluation() { @Test public void testMinCountConstraintEvaluation() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -291,7 +284,7 @@ public void testMinCountConstraintEvaluation() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass . """ ); @@ -319,7 +312,7 @@ public void testMinCountConstraintEvaluation() { @Test public void testMaxCountEvaluation() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -334,7 +327,7 @@ public void testMaxCountEvaluation() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty "foo" ; @@ -364,7 +357,7 @@ public void testMaxCountEvaluation() { @Test public void testMinExclusiveConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -379,7 +372,7 @@ public void testMinExclusiveConstraint() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty 42 . @@ -410,7 +403,7 @@ public void testMinExclusiveConstraint() { @Test public void testMinInclusiveConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -425,7 +418,7 @@ public void testMinInclusiveConstraint() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty 41 . @@ -457,7 +450,7 @@ public void testMinInclusiveConstraint() { @Test public void testMaxExclusiveConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -472,7 +465,7 @@ public void testMaxExclusiveConstraint() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty 42 . @@ -503,7 +496,7 @@ public void testMaxExclusiveConstraint() { @Test public void testMaxInclusiveConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -518,7 +511,7 @@ public void testMaxInclusiveConstraint() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty 43 . @@ -550,7 +543,7 @@ public void testMaxInclusiveConstraint() { @Test public void testMinLengthConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -565,7 +558,7 @@ public void testMinLengthConstraint() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty "abc" . @@ -597,7 +590,7 @@ public void testMinLengthConstraint() { @Test public void testMaxLengthConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -612,7 +605,7 @@ public void testMaxLengthConstraint() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty "abcabc" . @@ -644,7 +637,7 @@ public void testMaxLengthConstraint() { @Test public void testPatternConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -660,7 +653,7 @@ public void testPatternConstraint() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty "y" . @@ -695,7 +688,7 @@ public void testPatternConstraint() { @Test public void testAllowedLanguageConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -710,7 +703,7 @@ public void testAllowedLanguageConstraint() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty "non valide"@fr . @@ -745,7 +738,7 @@ public void testAllowedLanguageConstraint() { @Test public void testEqualsConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -760,7 +753,7 @@ public void testEqualsConstraint() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty "some value" ; @@ -798,7 +791,7 @@ public void testEqualsConstraint() { @Test public void testDisjointConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -813,7 +806,7 @@ public void testDisjointConstraint() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty "some value" ; @@ -849,7 +842,7 @@ public void testDisjointConstraint() { @Test public void testLessThanConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -864,7 +857,7 @@ public void testLessThanConstraint() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty 10 ; @@ -901,7 +894,7 @@ public void testLessThanConstraint() { @Test public void testLessThanOrEqualsConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -916,7 +909,7 @@ public void testLessThanOrEqualsConstraint() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty 10 ; @@ -954,7 +947,7 @@ public void testLessThanOrEqualsConstraint() { @Test public void testUniqueLangConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -969,7 +962,7 @@ public void testUniqueLangConstraint() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty "hello"@en ; @@ -1004,7 +997,7 @@ public void testUniqueLangConstraint() { @Test public void testHasValueConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -1019,7 +1012,7 @@ public void testHasValueConstraint() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty "hello" . @@ -1053,7 +1046,7 @@ public void testHasValueConstraint() { @Test public void testAllowedValuesEvaluation() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -1068,7 +1061,7 @@ public void testAllowedValuesEvaluation() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty "baz" . @@ -1101,7 +1094,7 @@ public void testAllowedValuesEvaluation() { @Test public void testNodeConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -1123,7 +1116,7 @@ public void testNodeConstraint() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty :Bar . @@ -1157,7 +1150,7 @@ public void testNodeConstraint() { @Test public void testMultipleNodeConstraints() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -1187,7 +1180,7 @@ public void testMultipleNodeConstraints() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :myProperty :element . @@ -1214,7 +1207,7 @@ public void testMultipleNodeConstraints() { @Test public void testClosedConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix rdf: . @prefix sh: . @prefix : . @@ -1231,7 +1224,7 @@ public void testClosedConstraint() { sh:ignoredProperties ( rdf:type ) . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty "bar" ; @@ -1264,7 +1257,7 @@ public void testClosedConstraint() { @Test public void testSparqlConstraintEvaluation() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix xsd: . @prefix : . @@ -1301,7 +1294,7 @@ public void testSparqlConstraintEvaluation() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . @prefix xsd: . :Foo a :TestClass ; @@ -1333,7 +1326,7 @@ public void testSparqlConstraintEvaluation() { @Test public void testBooleanJsConstraintEvaluation() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix xsd: . @prefix : . @@ -1360,7 +1353,7 @@ public void testBooleanJsConstraintEvaluation() { """.replace( "$RESOURCE_URL", getClass().getClassLoader() .getResource( "JsConstraintTest.js" ).toString() ) ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . @prefix xsd: . :Foo a :TestClass ; @@ -1389,7 +1382,7 @@ public void testBooleanJsConstraintEvaluation() { @Test public void testMessageObjectJsConstraintEvaluation() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix xsd: . @prefix : . @@ -1416,7 +1409,7 @@ public void testMessageObjectJsConstraintEvaluation() { """.replace( "$RESOURCE_URL", getClass().getClassLoader() .getResource( "JsConstraintTest.js" ).toString() ) ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . @prefix xsd: . :Foo a :TestClass ; @@ -1449,7 +1442,7 @@ public void testMessageObjectJsConstraintEvaluation() { @Test public void testSequencePath() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -1464,7 +1457,7 @@ public void testSequencePath() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :prop1 [ @@ -1497,7 +1490,7 @@ public void testSequencePath() { @Test public void testAlternativePath() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -1512,7 +1505,7 @@ public void testAlternativePath() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :prop1 42 ; @@ -1544,7 +1537,7 @@ public void testAlternativePath() { @Test public void testInversePath() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -1559,7 +1552,7 @@ public void testInversePath() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty :Bar ; @@ -1591,7 +1584,7 @@ public void testInversePath() { @Test public void testZeroOrMorePath() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -1606,7 +1599,7 @@ public void testZeroOrMorePath() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty [ @@ -1641,7 +1634,7 @@ public void testZeroOrMorePath() { @Test public void testOneOrMorePath() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -1656,7 +1649,7 @@ public void testOneOrMorePath() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty [ @@ -1691,7 +1684,7 @@ public void testOneOrMorePath() { @Test public void testZeroOrOnePath() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -1706,7 +1699,7 @@ public void testZeroOrOnePath() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty [ @@ -1739,7 +1732,7 @@ public void testZeroOrOnePath() { @Test public void testNotConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix : . @@ -1756,7 +1749,7 @@ public void testNotConstraint() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty [ @@ -1790,7 +1783,7 @@ public void testNotConstraint() { @Test public void testAndConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix xsd: . @prefix : . @@ -1809,7 +1802,7 @@ public void testAndConstraint() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty [ @@ -1827,7 +1820,7 @@ public void testAndConstraint() { final NodeKindViolation violation = (NodeKindViolation) finding; assertThat( violation.context().parentContext().map( EvaluationContext::element ) ).contains( element ); assertThat( violation.context().parentContext().get().shape().attributes().uri() ).hasValue( namespace + "MyShape" ); - assertThat( ((PredicatePath) violation.context().parentContext().get().propertyShape().get().path()).predicate().getURI() ) + assertThat( ( (PredicatePath) violation.context().parentContext().get().propertyShape().get().path() ).predicate().getURI() ) .endsWith( "testProperty" ); assertThat( violation.context().parentElementName() ).isEqualTo( ":Foo" ); assertThat( violation.allowedNodeKind() ).isEqualTo( Shape.NodeKind.IRI ); @@ -1843,7 +1836,7 @@ public void testAndConstraint() { @Test public void testOrConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix xsd: . @prefix : . @@ -1862,7 +1855,7 @@ public void testOrConstraint() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty 42 . @@ -1880,7 +1873,7 @@ public void testOrConstraint() { assertThat( violation.context().propertyName() ).isEqualTo( ":testProperty" ); assertThat( violation.context().elementName() ).isEqualTo( ":Foo" ); assertThat( violation ).isInstanceOf( OrViolation.class ); - assertThat( ((OrViolation) violation).violations() ) + assertThat( ( (OrViolation) violation ).violations() ) .hasSize( 2 ) .anySatisfy( subviolation -> assertThat( subviolation ).isInstanceOfSatisfying( InvalidValueViolation.class, invalidValueViolation -> { @@ -1901,7 +1894,7 @@ public void testOrConstraint() { @Test public void testXoneConstraintInPropertyShape() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix xsd: . @prefix : . @@ -1919,7 +1912,7 @@ public void testXoneConstraintInPropertyShape() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty 42 . @@ -1953,7 +1946,7 @@ public void testXoneConstraintInPropertyShape() { @Test void testXoneConstraintInPropertyShapeWithNoSubViolations() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix xsd: . @prefix : . @@ -1980,7 +1973,7 @@ void testXoneConstraintInPropertyShapeWithNoSubViolations() { sh:hasValue 42 . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty 42 . @@ -2005,7 +1998,7 @@ void testXoneConstraintInPropertyShapeWithNoSubViolations() { @Test void testXoneConstraintInNodeShapeExpectSuccess() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix xsd: . @prefix : . @@ -2031,7 +2024,7 @@ void testXoneConstraintInNodeShapeExpectSuccess() { sh:minCount 1 . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :foo 1 . @@ -2046,7 +2039,7 @@ void testXoneConstraintInNodeShapeExpectSuccess() { @Test void testXoneConstraintInNodeShapeExpectFailure() { - final Model shapesModel = model( + final Model shapesModel = createModel( """ @prefix sh: . @prefix xsd: . @@ -2074,7 +2067,7 @@ void testXoneConstraintInNodeShapeExpectFailure() { """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty 42 . @@ -2113,7 +2106,7 @@ void testXoneConstraintInNodeShapeExpectFailure() { @Test void testSparqlTargetWithGenericConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix xsd: . @prefix : . @@ -2144,7 +2137,7 @@ void testSparqlTargetWithGenericConstraint() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . :Foo a :TestClass ; :testProperty "abc" . @@ -2161,7 +2154,7 @@ void testSparqlTargetWithGenericConstraint() { @Test void testSparqlTargetWithShapeSparqlConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix xsd: . @prefix : . @@ -2206,7 +2199,7 @@ void testSparqlTargetWithShapeSparqlConstraint() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . @prefix xsd: . :Foo a :TestClass ; @@ -2227,7 +2220,7 @@ void testSparqlTargetWithShapeSparqlConstraint() { @Test void testSparqlTargetWithPropertySparqlConstraint() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix xsd: . @prefix : . @@ -2272,7 +2265,7 @@ void testSparqlTargetWithPropertySparqlConstraint() { """ ); // important detail: ':testProperty' is missing on ':Foo', the SPARQLConstraint must run anyway - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . @prefix xsd: . :Foo a :TestClass. @@ -2292,7 +2285,7 @@ void testSparqlTargetWithPropertySparqlConstraint() { @Test void testMultiElementValidation() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix xsd: . @prefix : . @@ -2337,7 +2330,7 @@ void testMultiElementValidation() { ] . """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . @prefix xsd: . :Foo a :TestClass ; @@ -2359,7 +2352,7 @@ void testMultiElementValidation() { @Test void testTargetObjectsOf() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix xsd: . @prefix rdf: . @@ -2376,7 +2369,7 @@ void testTargetObjectsOf() { ] ; """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . @prefix xsd: . :Foo a :TestClass ; @@ -2398,7 +2391,7 @@ void testTargetObjectsOf() { @Test void testNodeTargets() { - final Model shapesModel = model( """ + final Model shapesModel = createModel( """ @prefix sh: . @prefix xsd: . @prefix rdf: . @@ -2415,7 +2408,7 @@ void testNodeTargets() { ] ; """ ); - final Model dataModel = model( """ + final Model dataModel = createModel( """ @prefix : . @prefix xsd: . :Foo a :TestClass ; @@ -2433,12 +2426,4 @@ void testNodeTargets() { assertThat( violations.size() ).isEqualTo( 1 ); assertThat( violations.get( 0 ) ).isInstanceOf( DatatypeViolation.class ); } - - private Model model( final String ttlRepresentation ) { - final Model model = ModelFactory.createDefaultModel(); - final InputStream in = new ByteArrayInputStream( ttlRepresentation.getBytes( StandardCharsets.UTF_8 ) ); - RDFParserRegistry.registerLangTriples( Lang.TURTLE, ReaderRiotTurtle.factory ); - model.read( in, "", RDFLanguages.strLangTurtle ); - return model; - } } diff --git a/core/esmf-test-aspect-models/src/main/java/org/eclipse/esmf/test/TestAspect.java b/core/esmf-test-aspect-models/src/main/java/org/eclipse/esmf/test/TestAspect.java index b0a9b1898..2e2fce12b 100644 --- a/core/esmf-test-aspect-models/src/main/java/org/eclipse/esmf/test/TestAspect.java +++ b/core/esmf-test-aspect-models/src/main/java/org/eclipse/esmf/test/TestAspect.java @@ -124,6 +124,7 @@ public enum TestAspect implements TestModel { ASPECT_WITH_MULTIPLE_ENUMERATIONS_ON_MULTIPLE_LEVELS, ASPECT_WITH_MULTIPLE_SEE_ATTRIBUTES, ASPECT_WITH_MULTI_LANGUAGE_TEXT, + ASPECT_WITH_NAMESPACE_DESCRIPTION, ASPECT_WITH_NESTED_ENTITY, ASPECT_WITH_NESTED_ENTITY_LIST, ASPECT_WITH_NESTED_ENTITY_ENUMERATION_WITH_NOT_IN_PAYLOAD, diff --git a/core/esmf-test-aspect-models/src/main/resources/valid/org.eclipse.esmf.test/1.0.0/AspectWithNamespaceDescription.ttl b/core/esmf-test-aspect-models/src/main/resources/valid/org.eclipse.esmf.test/1.0.0/AspectWithNamespaceDescription.ttl new file mode 100644 index 000000000..ece86f4a0 --- /dev/null +++ b/core/esmf-test-aspect-models/src/main/resources/valid/org.eclipse.esmf.test/1.0.0/AspectWithNamespaceDescription.ttl @@ -0,0 +1,36 @@ +# 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 + +@prefix : . +@prefix samm: . +@prefix samm-c: . +@prefix xsd: . +@prefix unit: . + + a samm:Namespace ; + samm:preferredName "Test namespace"@en ; + samm:description "Test of the namespace pseudo element"@en ; + samm:see . + +:AspectWithProperty a samm:Aspect ; + samm:preferredName "Test Aspect"@en ; + samm:description "This is a test description"@en ; + samm:see ; + samm:properties ( :testProperty ) ; + samm:operations ( ) . + +:testProperty a samm:Property ; + samm:preferredName "Test Property"@en ; + samm:description "This is a test property."@en ; + samm:see ; + samm:see ; + samm:exampleValue "Example Value" ; + samm:characteristic samm-c:Text . diff --git a/core/esmf-test-aspect-models/src/main/resources/valid/org.eclipse.esmf.test/1.0.0/ModelWithBlankAndAdditionalNodes.ttl b/core/esmf-test-aspect-models/src/main/resources/valid/org.eclipse.esmf.test/1.0.0/ModelWithBlankAndAdditionalNodes.ttl index da222ce7c..512a914df 100644 --- a/core/esmf-test-aspect-models/src/main/resources/valid/org.eclipse.esmf.test/1.0.0/ModelWithBlankAndAdditionalNodes.ttl +++ b/core/esmf-test-aspect-models/src/main/resources/valid/org.eclipse.esmf.test/1.0.0/ModelWithBlankAndAdditionalNodes.ttl @@ -12,7 +12,7 @@ @prefix : . @prefix samm: . @prefix samm-c: . -@prefix aux: . +@prefix aux: . @prefix xsd: . [ diff --git a/core/esmf-test-resources/src/main/java/org/eclipse/esmf/test/TestResources.java b/core/esmf-test-resources/src/main/java/org/eclipse/esmf/test/TestResources.java index b1bcec113..7f7435570 100644 --- a/core/esmf-test-resources/src/main/java/org/eclipse/esmf/test/TestResources.java +++ b/core/esmf-test-resources/src/main/java/org/eclipse/esmf/test/TestResources.java @@ -14,6 +14,8 @@ package org.eclipse.esmf.test; import java.io.InputStream; +import java.net.URI; +import java.util.Optional; import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader; import org.eclipse.esmf.aspectmodel.resolver.ClasspathStrategy; @@ -28,7 +30,7 @@ public class TestResources { public static AspectModel load( final InvalidTestAspect model ) { - final String path = String.format( "invalid/%s/%s/%s.ttl", model.getUrn().getNamespace(), model.getUrn().getVersion(), + final String path = String.format( "invalid/%s/%s/%s.ttl", model.getUrn().getNamespaceMainPart(), model.getUrn().getVersion(), model.getName() ); final InputStream inputStream = TestResources.class.getClassLoader().getResourceAsStream( path ); final ResolutionStrategy testModelsResolutionStrategy = new ClasspathStrategy( @@ -37,11 +39,11 @@ public static AspectModel load( final InvalidTestAspect model ) { } public static AspectModel load( final TestModel model ) { - final String path = String.format( "valid/%s/%s/%s.ttl", model.getUrn().getNamespace(), model.getUrn().getVersion(), + final String path = String.format( "valid/%s/%s/%s.ttl", model.getUrn().getNamespaceMainPart(), model.getUrn().getVersion(), model.getName() ); final InputStream inputStream = TestResources.class.getClassLoader().getResourceAsStream( path ); final ResolutionStrategy testModelsResolutionStrategy = new ClasspathStrategy( "valid" ); - return new AspectModelLoader( testModelsResolutionStrategy ).load( inputStream ); + return new AspectModelLoader( testModelsResolutionStrategy ).load( inputStream, Optional.of( URI.create( "testmodel:" + path ) ) ); } public static Try loadPayload( final TestModel model ) { diff --git a/documentation/developer-guide/modules/tooling-guide/examples/EditAspectModel.java b/documentation/developer-guide/modules/tooling-guide/examples/EditAspectModel.java new file mode 100644 index 000000000..3f9e625f0 --- /dev/null +++ b/documentation/developer-guide/modules/tooling-guide/examples/EditAspectModel.java @@ -0,0 +1,100 @@ +/* + * 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 examples; + +// tag::imports[] +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Optional; + +import org.eclipse.esmf.aspectmodel.edit.AspectChangeManager; +import org.eclipse.esmf.aspectmodel.edit.AspectChangeManagerConfig; +import org.eclipse.esmf.aspectmodel.edit.AspectChangeManagerConfigBuilder; +import org.eclipse.esmf.aspectmodel.edit.Change; +import org.eclipse.esmf.aspectmodel.edit.ChangeGroup; +import org.eclipse.esmf.aspectmodel.edit.ChangeReport; +import org.eclipse.esmf.aspectmodel.edit.ChangeReportFormatter; +import org.eclipse.esmf.aspectmodel.edit.change.MoveElementToNewFile; +import org.eclipse.esmf.aspectmodel.edit.change.RenameElement; +import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader; +import org.eclipse.esmf.metamodel.AspectModel; +// end::imports[] + +import org.junit.jupiter.api.Test; + +public class EditAspectModel { + @Test + void testEditAspectModel() { + // tag::editModel[] + final AspectModel aspectModel = new AspectModelLoader().load( + // a File, InputStream or AspectModelUrn + // end::editModel[] + new File( "aspect-models/org.eclipse.esmf.examples.movement/1.0.0/Movement.ttl" ) + // tag::editModel[] + ); + + // All changes to an Aspect Model are done using an AspectChangeManager. + // Optionally, you can pass a config object as first constructor argument: + final AspectChangeManagerConfig config = AspectChangeManagerConfigBuilder.builder() + .detailedChangeReport( true ) + .defaultFileHeader( List.of( "Generated on " + + new SimpleDateFormat( "dd-MM-yyyy" ).format( new Date() ) ) ) + .build(); + final AspectChangeManager changeManager = new AspectChangeManager( config, aspectModel ); + + // You can create a single change, or you can combine multiple changes into a group. + // For all possible refactoring operations, see classes implementing the Change interface. + final Change refactorModel = new ChangeGroup( List.of( + // Rename an element. This works with with samm:Aspect and all other model elements. + new RenameElement( aspectModel.aspect(), "MyAspect" ), + // Move an element to a new Aspect Model file in the same namespace. + new MoveElementToNewFile( + // The element to move. + aspectModel.aspect().getProperties().get( 0 ), + // If you intend writing the model to the file system, set the location + // for the newly created file here. + Optional.empty() ) + ) ); + + // Apply the changes and get a report that summerizes the changes. + final ChangeReport changeReport = changeManager.applyChange( refactorModel ); + + // If you want to display the change report, you can serialize it to a string: + ChangeReportFormatter.INSTANCE.apply( changeReport, config ); + + // Alternatively, you can also get views on collections containing modified/ + // added/removed files for the last applied change. + changeManager.createdFiles().forEach( file -> System.out.println( "Created: " + file ) ); + changeManager.modifiedFiles().forEach( file -> System.out.println( "Modified: " + file ) ); + changeManager.removedFiles().forEach( file -> System.out.println( "Removed: " + file ) ); + + // At this point, you could use the AspectChangeManager's undo() method to revert + // the last change (refactorModel in this case); afterwards you can also redo(). + // This functionality is mainly interesting when refactoring the Aspect Model + // interactively. + + // If you want to write the model changes to the file system, use the AspectSerializer. + // Each AspectModelFile will be written to its respective source location. + // Alternatively, the AspectSerializer also provides a method to write an AspectModelFile + // into a String. + // end::editModel[] + /* + // tag::editModel[] + AspectSerializer.INSTANCE.write( aspectModel ); + // end::editModel[] + */ + } +} diff --git a/documentation/developer-guide/modules/tooling-guide/examples/LoadAspectModelRdf.java b/documentation/developer-guide/modules/tooling-guide/examples/LoadAspectModelRdf.java index b66f75138..d3b79b34d 100644 --- a/documentation/developer-guide/modules/tooling-guide/examples/LoadAspectModelRdf.java +++ b/documentation/developer-guide/modules/tooling-guide/examples/LoadAspectModelRdf.java @@ -33,19 +33,19 @@ public class LoadAspectModelRdf { @Test public void loadAndResolveFromFile() { - // tag::loadModel[] + // tag::loadAndResolveFromFile[] final AspectModel aspectModel = new AspectModelLoader().load( // a File, InputStream or AspectModelUrn - // end::generate[] + // end::loadAndResolveFromFile[] new File( "aspect-models/org.eclipse.esmf.examples.movement/1.0.0/Movement.ttl" ) - // tag::loadModel[] + // tag::loadAndResolveFromFile[] ); // Do something with the Aspect Model on the RDF level. // Example: List the URNs of all samm:Entitys aspectModel.mergedModel().listStatements( null, RDF.type, SammNs.SAMM.Entity() ) .forEachRemaining( statement -> System.out.println( statement.getSubject().getURI() ) ); - // end::loadModel[] + // end::loadAndResolveFromFile[] } @Test diff --git a/documentation/developer-guide/modules/tooling-guide/examples/ParseAspectModelUrn.java b/documentation/developer-guide/modules/tooling-guide/examples/ParseAspectModelUrn.java index f5aa62922..ec04f6639 100644 --- a/documentation/developer-guide/modules/tooling-guide/examples/ParseAspectModelUrn.java +++ b/documentation/developer-guide/modules/tooling-guide/examples/ParseAspectModelUrn.java @@ -22,7 +22,7 @@ public class ParseAspectModelUrn { public void parseAspectModelUrn() { // tag::parseAspectModelUrn[] final AspectModelUrn urn = AspectModelUrn.fromUrn( "urn:samm:com.example:1.0.0#Example" ); - final String namespace = urn.getNamespace(); // com.example + final String namespace = urn.getNamespaceMainPart(); // com.example final String name = urn.getName(); // Example final String version = urn.getVersion(); // 1.0.0 final String urnPrefix = urn.getUrnPrefix(); // urn:samm:com.example:1.0.0# diff --git a/documentation/developer-guide/modules/tooling-guide/examples/SerializeAspectModel.java b/documentation/developer-guide/modules/tooling-guide/examples/SerializeAspectModel.java index 41c46e530..812a3584c 100644 --- a/documentation/developer-guide/modules/tooling-guide/examples/SerializeAspectModel.java +++ b/documentation/developer-guide/modules/tooling-guide/examples/SerializeAspectModel.java @@ -31,14 +31,14 @@ public void serializeAspectModel() { // tag::serialize[] // AspectModel as returned by the AspectModelLoader final AspectModel aspectModel = // ... - // end::generate[] + // end::serialize[] new AspectModelLoader().load( new File( "aspect-models/org.eclipse.esmf.examples.movement/1.0.0/Movement.ttl" ) ); // tag::serialize[] // A String that contains the pretty printed Aspect Model final String aspectString = - AspectSerializer.INSTANCE.apply( aspectModel.aspect() ); + AspectSerializer.INSTANCE.aspectToString( aspectModel.aspect() ); // end::serialize[] assertThat( aspectString ).contains( ":Movement a samm:Aspect" ); assertThat( aspectString ).contains( ":isMoving a samm:Property" ); diff --git a/documentation/developer-guide/modules/tooling-guide/pages/java-aspect-tooling.adoc b/documentation/developer-guide/modules/tooling-guide/pages/java-aspect-tooling.adoc index f2f192fd4..9adede2a4 100644 --- a/documentation/developer-guide/modules/tooling-guide/pages/java-aspect-tooling.adoc +++ b/documentation/developer-guide/modules/tooling-guide/pages/java-aspect-tooling.adoc @@ -712,6 +712,32 @@ Example: include::example$sample-file-header.vm[] ---- +[[modifying-and-creating-aspect-models]] +== Modifying and creating Aspect Models + +You can use the `AspectChangeManager` to modify an Aspect Model. Each modifying operation performed +on an Aspect Model is called a _change_. Instances of classes that implement the `Change` interface +can be passed to the `AspectChangeManager` `applyChange()` method. Available `Change`​s +include renaming Aspect Model files or Model elements, adding and removing Aspect Model files and +moving Aspect Model elements to other or new files in the same or another namespace. + +++++ +
+Show used imports +++++ +[source,java,indent=0,subs="+macros,+quotes"] +---- +include::example$EditAspectModel.java[tags=imports] +---- +++++ +
+++++ + +[source,java,indent=0,subs="+macros,+quotes"] +---- +include::example$EditAspectModel.java[tags=editModel] +---- + [[accessing-samm-programmatically]] == Accessing the SAMM programmatically diff --git a/documentation/developer-guide/modules/tooling-guide/pages/samm-cli.adoc b/documentation/developer-guide/modules/tooling-guide/pages/samm-cli.adoc index e83a5ea9f..d93ff77b5 100644 --- a/documentation/developer-guide/modules/tooling-guide/pages/samm-cli.adoc +++ b/documentation/developer-guide/modules/tooling-guide/pages/samm-cli.adoc @@ -48,21 +48,24 @@ alias samm='java -jar /location/to/samm-cli-{esmf-sdk-version}.jar' Successful execution of a command is signaled by returning 0. In case of a logical or other internal error the error code 1 is being returned. Missing or wrong command parameters result in error code 2 being returned. -To get help for a certain subcommand, add 'help' before the subcommand name or add `--help` to the end, e.g., `samm help aspect` -or `samm aspect --help`. +To get an overview of all commands and subcommands, use `samm help`. +To get help for a certain subcommand, use `help` before the subcommand name, e.g., `samm help aspect`, `samm help aspect validate` or `samm help aas to aspect`. Each subcommand can have its own set of options which allow the user to further fine-tune the execution of the command. The available options and their meaning can also be seen in the help text of the individual subcommands. +=== List of commands [width="100%",options="header",cols="20,50,30"] |=== | Command | Description/Options | Examples | help | Get overview of all commands | `samm help` -| help | Get help for a specific subcommand | `samm help aspect` -| aspect help | Get help for `aspect` subcommands | `samm aspect help validate` +.3+| [[help]] help | Get help for a list of subcommands | `samm help aspect` + | | `samm help aspect to svg` + | | `samm help aspect validate` .2+| [[aspect-validate]] aspect validate | Validate Aspect Model | `samm aspect AspectModel.ttl validate` | _--custom-resolver_ : use an external resolver for the resolution of the model elements | `samm aspect AspectModel.ttl validate --custom-resolver myresolver.sh` -.2+| [[aspect-prettyprint]] aspect prettyprint | Pretty-print Aspect Model | `samm aspect AspectModel.ttl prettyprint` +.3+| [[aspect-prettyprint]] aspect prettyprint | Pretty-print Aspect Model | `samm aspect AspectModel.ttl prettyprint` | _--output, -o_ : the output will be saved to the given file | `samm aspect AspectModel.ttl prettyprint -o c:\Results\PrettyPrinted.ttl` + | _--overwrite, -w_ : Overwrite the input file | `samm aspect AspectModel.ttl prettyprint -w` .5+| [[aspect-to-html]] aspect to html | Generate HTML documentation for an Aspect Model | `samm aspect AspectModel.ttl to html` | _--output, -o_ : the output will be saved to the given file | `samm aspect AspectModel.ttl to html -o c:\Model.html` | _--css, -c_ : CSS file with custom styles to be included in the generated HTML @@ -169,6 +172,20 @@ The available options and their meaning can also be seen in the help text of the | _--custom-resolver_ : use an external resolver for the resolution of the model elements | | _--aspect-data, -a_ : path to a JSON file containing aspect data corresponding to the Aspect Model | +.5+| [[aspect-edit-move]] aspect edit move [] | Move a model element definition from its + current place to another existing or new file in the same or another namespace. | `samm aspect AspectModel.ttl edit move + MyAspect otherFile.ttl` or `samm aspect + AspectModel.ttl edit move MyAspect + someFileInOtherNamespace.ttl + urn:samm:org.eclipse.example.myns:1.0.0` + | _--dry-run_ : Don't write changes to the file system, but print a report of changes + that would be performed. | + | _--details_ : When used with `--dry-run`, include details about model content + changes in the report . | + | _--copy-file-header_ : When a model element is moved to a new file, copy the file + header from the source file to the new file | + | _--force_ : When a new file is to be created but it already exists in the file system, + the operation will be cancelled, unless `--force` is used. | .3+| [[aas-to-aspect]] aas to aspect | Translate Asset Administration Shell (AAS) Submodel Templates to Aspect Models | `samm aas AssetAdminShell.aasx to aspect` | _--output-directory, -d_ : output directory to write files to (default: @@ -364,7 +381,7 @@ For the generated OpenAPI specification, the following mapping would apply: "openapi" : "3.0.3", "info" : { "title" : "TestAspect", // <2> <3> - "version" : "v2" // <1> + "version" : "v1" // <1> } } ---- @@ -379,10 +396,10 @@ The version information as described above is also used in the URL definitions o ---- { "servers" : [ { - "url" : "http://example.com/api/v2", // <1> + "url" : "http://example.com/api/v1", // <1> "variables" : { "api-version" : { - "default" : "v2" // <1> + "default" : "v1" // <1> } } } ] @@ -766,7 +783,7 @@ For the generated AsyncAPI specification, the following mapping would apply: "asyncapi" : "3.0.0", "info" : { "title" : "My Movement Aspect MQTT API", // <2> - "version" : "v2", // <1> + "version" : "v1", // <1> "description" : "Aspect for movement information" // <3> } } diff --git a/tools/esmf-aspect-model-maven-plugin/src/main/java/org/eclipse/esmf/aspectmodel/CodeGenerationMojo.java b/tools/esmf-aspect-model-maven-plugin/src/main/java/org/eclipse/esmf/aspectmodel/CodeGenerationMojo.java index 348e1c313..1e3625935 100644 --- a/tools/esmf-aspect-model-maven-plugin/src/main/java/org/eclipse/esmf/aspectmodel/CodeGenerationMojo.java +++ b/tools/esmf-aspect-model-maven-plugin/src/main/java/org/eclipse/esmf/aspectmodel/CodeGenerationMojo.java @@ -55,12 +55,12 @@ protected void validateParameters( final File templateLibFile ) throws MojoExecu protected String determinePackageName( final Aspect aspect ) { if ( packageName == null || packageName.isEmpty() ) { - return aspect.urn().getNamespace(); + return aspect.urn().getNamespaceMainPart(); } final AspectModelUrn urn = aspect.urn(); final VersionNumber versionNumber = VersionNumber.parse( urn.getVersion() ); - final String interpolated = packageName.replace( "{{namespace}}", urn.getNamespace() ) + final String interpolated = packageName.replace( "{{namespace}}", urn.getNamespaceMainPart() ) .replace( "{{majorVersion}}", "" + versionNumber.getMajor() ) .replace( "{{minorVersion}}", "" + versionNumber.getMinor() ) .replace( "{{microVersion}}", "" + versionNumber.getMicro() ); diff --git a/tools/esmf-aspect-model-maven-plugin/src/main/java/org/eclipse/esmf/aspectmodel/GenerateAspectFromAas.java b/tools/esmf-aspect-model-maven-plugin/src/main/java/org/eclipse/esmf/aspectmodel/GenerateAspectFromAas.java index 4ad0ce2a3..0cce3f64d 100644 --- a/tools/esmf-aspect-model-maven-plugin/src/main/java/org/eclipse/esmf/aspectmodel/GenerateAspectFromAas.java +++ b/tools/esmf-aspect-model-maven-plugin/src/main/java/org/eclipse/esmf/aspectmodel/GenerateAspectFromAas.java @@ -39,7 +39,7 @@ public void executeGeneration() throws MojoExecutionException, MojoFailureExcept for ( final Aspect aspect : generator.generateAspects() ) { try ( final FileOutputStream outputStreamForFile = new FileOutputStream( getOutputFile( aspect ) ) ) { - outputStreamForFile.write( AspectSerializer.INSTANCE.apply( aspect ).getBytes() ); + outputStreamForFile.write( AspectSerializer.INSTANCE.aspectToString( aspect ).getBytes() ); } catch ( final IOException exception ) { throw new MojoExecutionException( "Could not write file", exception ); } @@ -49,7 +49,7 @@ public void executeGeneration() throws MojoExecutionException, MojoFailureExcept private File getOutputFile( final Aspect aspect ) throws MojoExecutionException { final AspectModelUrn urn = aspect.urn(); - final Path outputPath = Path.of( outputDirectory, urn.getNamespace(), urn.getVersion() ); + final Path outputPath = Path.of( outputDirectory, urn.getNamespaceMainPart(), urn.getVersion() ); try { Files.createDirectories( outputPath ); } catch ( final IOException exception ) { diff --git a/tools/esmf-aspect-model-maven-plugin/src/main/java/org/eclipse/esmf/aspectmodel/PrettyPrint.java b/tools/esmf-aspect-model-maven-plugin/src/main/java/org/eclipse/esmf/aspectmodel/PrettyPrint.java index 624bb3d56..a800ed7c9 100644 --- a/tools/esmf-aspect-model-maven-plugin/src/main/java/org/eclipse/esmf/aspectmodel/PrettyPrint.java +++ b/tools/esmf-aspect-model-maven-plugin/src/main/java/org/eclipse/esmf/aspectmodel/PrettyPrint.java @@ -38,7 +38,7 @@ public void executeGeneration() throws MojoExecutionException, MojoFailureExcept for ( final AspectModel aspectModel : loadModels() ) { final Aspect aspect = aspectModel.aspect(); - final String formatted = AspectSerializer.INSTANCE.apply( aspect ); + final String formatted = AspectSerializer.INSTANCE.aspectToString( aspect ); final String aspectModelFileName = String.format( "%s.ttl", aspect.getName() ); try ( final FileOutputStream streamForFile = getOutputStreamForFile( aspectModelFileName, outputDirectory ); final PrintWriter writer = new PrintWriter( streamForFile ) ) { diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/AbstractCommand.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/AbstractCommand.java index 50a600624..7a451d947 100644 --- a/tools/samm-cli/src/main/java/org/eclipse/esmf/AbstractCommand.java +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/AbstractCommand.java @@ -46,7 +46,7 @@ import org.apache.commons.io.FilenameUtils; public abstract class AbstractCommand implements Runnable { - private Path modelsRootForFile( final File file ) { + protected Path modelsRootForFile( final File file ) { return file.toPath().getParent().getParent().getParent(); } @@ -54,13 +54,16 @@ protected AspectModel loadAspectModelOrFail( final String modelFileName, final E return loadAspectModelOrFail( modelFileName, resolverConfig, false ); } - protected AspectModel loadAspectModelOrFail( final String modelFileName, final ExternalResolverMixin resolverConfig, - final boolean details ) { + protected File getInputFile( final String modelFileName ) { final File inputFile = new File( modelFileName ); - final File absoluteFile = inputFile.isAbsolute() + return inputFile.isAbsolute() ? inputFile : Path.of( System.getProperty( "user.dir" ) ).resolve( inputFile.toPath() ).toFile().getAbsoluteFile(); + } + protected AspectModel loadAspectModelOrFail( final File modelFile, final ExternalResolverMixin resolverConfig, + final boolean details ) { + final File absoluteFile = modelFile.getAbsoluteFile(); final ResolutionStrategy resolveFromWorkspace = new FileSystemStrategy( modelsRootForFile( absoluteFile ) ); final ResolutionStrategy resolveFromCurrentDirectory = AspectModelLoader.DEFAULT_STRATEGY.get(); final ResolutionStrategy resolutionStrategy = resolverConfig.commandLine.isBlank() @@ -83,6 +86,11 @@ protected AspectModel loadAspectModelOrFail( final String modelFileName, final E return validModelOrViolations.get(); } + protected AspectModel loadAspectModelOrFail( final String modelFileName, final ExternalResolverMixin resolverConfig, + final boolean details ) { + return loadAspectModelOrFail( getInputFile( modelFileName ), resolverConfig, details ); + } + protected Aspect loadAspectOrFail( final String modelFileName, final ExternalResolverMixin resolverConfig ) { final File inputFile = new File( modelFileName ); final AspectModel aspectModel = loadAspectModelOrFail( modelFileName, resolverConfig ); diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/LoggingMixin.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/LoggingMixin.java index 35920bbcf..ac5283f3d 100644 --- a/tools/samm-cli/src/main/java/org/eclipse/esmf/LoggingMixin.java +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/LoggingMixin.java @@ -32,7 +32,7 @@ public class LoggingMixin { private boolean[] verbosity = new boolean[0]; private static LoggingMixin getTopLevelCommandLoggingMixin( final CommandLine.Model.CommandSpec commandSpec ) { - return ((SammCli) commandSpec.root().userObject()).loggingMixin; + return ( (SammCli) commandSpec.root().userObject() ).loggingMixin; } public static int executionStrategy( final CommandLine.ParseResult parseResult ) { diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/SammCli.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/SammCli.java index ae16fce4d..b97d3f61b 100644 --- a/tools/samm-cli/src/main/java/org/eclipse/esmf/SammCli.java +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/SammCli.java @@ -12,13 +12,26 @@ */ package org.eclipse.esmf; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST; + import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Properties; +import java.util.stream.IntStream; import org.eclipse.esmf.aas.AasCommand; +import org.eclipse.esmf.aas.AasToCommand; +import org.eclipse.esmf.aas.to.AasToAspectCommand; import org.eclipse.esmf.aspect.AspectCommand; +import org.eclipse.esmf.aspect.AspectEditCommand; +import org.eclipse.esmf.aspect.AspectPrettyPrintCommand; +import org.eclipse.esmf.aspect.AspectToCommand; +import org.eclipse.esmf.aspect.to.AspectToSvgCommand; import org.eclipse.esmf.substitution.IsWindows; import org.fusesource.jansi.AnsiConsole; @@ -31,7 +44,13 @@ descriptionHeading = "%n@|bold Description|@:%n%n", parameterListHeading = "%n@|bold Parameters|@:%n", optionListHeading = "%n@|bold Options|@:%n", - footer = "%nRun @|bold " + SammCli.COMMAND_NAME + " help |@ to display its help." + footer = "%nRun @|bold " + SammCli.COMMAND_NAME + " help |@ to display its help, e.g.:%n" + + " @|bold " + SammCli.COMMAND_NAME + " help " + + AspectCommand.COMMAND_NAME + " " + AspectToCommand.COMMAND_NAME + " " + AspectToSvgCommand.COMMAND_NAME + "|@%n" + + "or @|bold " + SammCli.COMMAND_NAME + " help " + + AspectCommand.COMMAND_NAME + " " + AspectPrettyPrintCommand.COMMAND_NAME + "|@%n" + + "or @|bold " + SammCli.COMMAND_NAME + " help " + + AasCommand.COMMAND_NAME + " " + AasToCommand.COMMAND_NAME + " " + AasToAspectCommand.COMMAND_NAME + "|@%n" + "%nDocumentation: https://eclipse-esmf.github.io/esmf-documentation/index.html" ) @SuppressWarnings( "squid:S1147" ) // System.exit is really required here, this is a CLI tool @@ -40,18 +59,53 @@ public class SammCli extends AbstractCommand { private final CommandLine commandLine; + static class CustomCommandListRenderer implements CommandLine.IHelpSectionRenderer { + @Override + public String render( final CommandLine.Help help ) { + final CommandLine.Model.CommandSpec spec = help.commandSpec(); + if ( spec.subcommands().isEmpty() ) { + return ""; + } + + final CommandLine.Help.TextTable textTable = CommandLine.Help.TextTable.forColumns( help.colorScheme(), + new CommandLine.Help.Column( 15, 2, CommandLine.Help.Column.Overflow.SPAN ), + new CommandLine.Help.Column( spec.usageMessage().width() - 15, 2, CommandLine.Help.Column.Overflow.WRAP ) ); + textTable.setAdjustLineBreaksForWideCJKCharacters( spec.usageMessage().adjustLineBreaksForWideCJKCharacters() ); + spec.subcommands().values().forEach( subcommand -> addHierarchy( subcommand, textTable, "" ) ); + return textTable.toString(); + } + + private void addHierarchy( final CommandLine cmd, final CommandLine.Help.TextTable textTable, final String indent ) { + final String names = cmd.getCommandSpec().names().toString(); + final String formattedNames = names.substring( 1, names.length() - 1 ); + final String description = description( cmd.getCommandSpec().usageMessage() ); + textTable.addRowValues( indent + formattedNames, description ); + cmd.getSubcommands().values().forEach( sub -> addHierarchy( sub, textTable, indent + " " ) ); + } + + private String description( final CommandLine.Model.UsageMessageSpec usageMessage ) { + if ( usageMessage.header().length > 0 ) { + return usageMessage.header()[0]; + } + if ( usageMessage.description().length > 0 ) { + return usageMessage.description()[0]; + } + return ""; + } + } + public SammCli() { final CommandLine initialCommandLine = new CommandLine( this ) .addSubcommand( new AspectCommand() ) .addSubcommand( new AasCommand() ) .setCaseInsensitiveEnumValuesAllowed( true ) .setExecutionStrategy( LoggingMixin::executionStrategy ); + initialCommandLine.getHelpSectionMap().put( SECTION_KEY_COMMAND_LIST, new CustomCommandListRenderer() ); final CommandLine.IExecutionExceptionHandler defaultExecutionExceptionHandler = initialCommandLine.getExecutionExceptionHandler(); commandLine = initialCommandLine.setExecutionExceptionHandler( new CommandLine.IExecutionExceptionHandler() { @Override public int handleExecutionException( final Exception exception, final CommandLine commandLine, - final CommandLine.ParseResult parseResult ) - throws Exception { + final CommandLine.ParseResult parseResult ) throws Exception { if ( exception.getClass().getName() .equals( String.format( "%s.MainClassProcessLauncher$SystemExitCaptured", SammCli.class.getPackageName() ) ) ) { // If the exception we encounter is a SystemExitCaptured, this is part of the security manager in the test suite that @@ -131,13 +185,58 @@ public static void main( final String[] argv ) { } final SammCli command = new SammCli(); - final int exitCode = command.commandLine.execute( argv ); + final String[] adjustedArgv = adjustCommandLineArguments( argv ); + + final int exitCode = command.commandLine.execute( adjustedArgv ); if ( !disableColor ) { AnsiConsole.systemUninstall(); } System.exit( exitCode ); } + /** + * Explicitly allow 'samm help command subcommand...' also if the subcommand is 'to' (e.g., aspect to, aas to) and + * usually receives a mandatory input file as its first parameter, e.g.: + * What a user wants to enter: "help aspect to sql" + * What we need to provide to picocli: "aspect _ to help sql" + * + * @param argv the original command line arguments + * @return the adjusted command line arguments + */ + private static String[] adjustCommandLineArguments( final String[] argv ) { + final String[] adjustedArgv; + final List argvList = Arrays.asList( argv ); + final int helpAspectToIndex = Collections.indexOfSubList( argvList, + List.of( "help", AspectCommand.COMMAND_NAME, AspectToCommand.COMMAND_NAME ) ); + final int helpAasToIndex = Collections.indexOfSubList( argvList, + List.of( "help", AasCommand.COMMAND_NAME, AasToCommand.COMMAND_NAME ) ); + final int helpAspectEditIndex = Collections.indexOfSubList( argvList, + List.of( "help", AspectCommand.COMMAND_NAME, AspectEditCommand.COMMAND_NAME ) ); + final int helpAspectSomeIndex = Collections.indexOfSubList( argvList, List.of( "help", AspectCommand.COMMAND_NAME ) ); + final int helpAasSomeIndex = Collections.indexOfSubList( argvList, List.of( "help", AasCommand.COMMAND_NAME ) ); + if ( helpAspectToIndex != -1 || helpAasToIndex != -1 || helpAspectEditIndex != -1 ) { + final int index = IntStream.of( helpAspectToIndex, helpAasToIndex, helpAspectEditIndex ).max().getAsInt(); + final List customArgv = new ArrayList<>( argvList.subList( 0, index ) ); + customArgv.add( argvList.get( index + 1 ) ); + customArgv.add( "_" ); + customArgv.add( argvList.get( index + 2 ) ); + customArgv.add( "help" ); + customArgv.addAll( argvList.subList( index + 3, argvList.size() ) ); + adjustedArgv = customArgv.toArray( new String[] {} ); + } else if ( helpAspectSomeIndex != -1 || helpAasSomeIndex != -1 ) { + // "help aspect prettyprint" -> "aspect help prettyprint" + final int index = Integer.max( helpAspectSomeIndex, helpAasSomeIndex ); + final List customArgv = new ArrayList<>( argvList.subList( 0, index ) ); + customArgv.add( argvList.get( index + 1 ) ); + customArgv.add( "help" ); + customArgv.addAll( argvList.subList( index + 2, argvList.size() ) ); + adjustedArgv = customArgv.toArray( new String[] {} ); + } else { + adjustedArgv = argv; + } + return adjustedArgv; + } + protected String format( final String string ) { return commandLine.getColorScheme().ansi().string( string ); } @@ -166,6 +265,8 @@ public void run() { System.exit( 0 ); } System.out.println( commandLine.getHelp().fullSynopsis() ); - System.out.println( format( "Run @|bold " + commandLine.getCommandName() + " help|@ for help." ) ); + System.out.println( format( "Run @|bold " + commandLine.getCommandName() + " help|@ for help, e.g.:" ) ); + System.out.println( format( " @|bold " + commandLine.getCommandName() + " help " + + AspectCommand.COMMAND_NAME + " " + AspectToCommand.COMMAND_NAME + " " + AspectToSvgCommand.COMMAND_NAME + "|@" ) ); } } 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 ba6a2070a..43872e08d 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 @@ -69,7 +69,7 @@ private void generateAspects( final AasToAspectModelGenerator generator ) { .toList(); for ( final Aspect aspect : filteredAspects ) { - final String aspectString = AspectSerializer.INSTANCE.apply( aspect ); + final String aspectString = AspectSerializer.INSTANCE.aspectToString( aspect ); final File targetFile = modelsRoot.determineAspectModelFile( aspect.urn() ); LOG.info( "Writing {}", targetFile.getAbsolutePath() ); final File directory = targetFile.getParentFile(); diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectCommand.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectCommand.java index 13cb0e67e..e4d54cb8a 100644 --- a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectCommand.java +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectCommand.java @@ -25,13 +25,13 @@ CommandLine.HelpCommand.class, AspectToCommand.class, AspectPrettyPrintCommand.class, - AspectValidateCommand.class + AspectValidateCommand.class, + AspectEditCommand.class }, headerHeading = "@|bold Usage|@:%n%n", descriptionHeading = "%n@|bold Description|@:%n%n", parameterListHeading = "%n@|bold Parameters|@:%n", - optionListHeading = "%n@|bold Options|@:%n", - mixinStandardHelpOptions = true + optionListHeading = "%n@|bold Options|@:%n" ) public class AspectCommand extends AbstractCommand { public static final String COMMAND_NAME = "aspect"; diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectEditCommand.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectEditCommand.java new file mode 100644 index 000000000..4c666a1fc --- /dev/null +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectEditCommand.java @@ -0,0 +1,47 @@ +/* + * 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.aspect; + +import org.eclipse.esmf.AbstractCommand; +import org.eclipse.esmf.LoggingMixin; +import org.eclipse.esmf.aspect.edit.AspectEditMoveCommand; +import org.eclipse.esmf.exception.SubCommandException; + +import picocli.CommandLine; + +@CommandLine.Command( name = AspectEditCommand.COMMAND_NAME, + description = "Edit (refactor) an Aspect Model", + subcommands = { + CommandLine.HelpCommand.class, + AspectEditMoveCommand.class + }, + headerHeading = "@|bold Usage|@:%n%n", + descriptionHeading = "%n@|bold Description|@:%n%n", + parameterListHeading = "%n@|bold Parameters|@:%n", + optionListHeading = "%n@|bold Options|@:%n" +) +public class AspectEditCommand extends AbstractCommand { + public static final String COMMAND_NAME = "edit"; + + @CommandLine.Mixin + private LoggingMixin loggingMixin; + + @CommandLine.ParentCommand + public AspectCommand parentCommand; + + @Override + public void run() { + throw new SubCommandException( COMMAND_NAME ); + } +} diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectPrettyPrintCommand.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectPrettyPrintCommand.java index 4f08f6f06..6d5294020 100644 --- a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectPrettyPrintCommand.java +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectPrettyPrintCommand.java @@ -14,16 +14,20 @@ package org.eclipse.esmf.aspect; import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.OutputStream; import java.io.PrintWriter; +import java.net.URI; import org.eclipse.esmf.AbstractCommand; import org.eclipse.esmf.ExternalResolverMixin; import org.eclipse.esmf.LoggingMixin; +import org.eclipse.esmf.aspectmodel.AspectModelFile; import org.eclipse.esmf.aspectmodel.serializer.PrettyPrinter; import org.eclipse.esmf.exception.CommandException; import org.eclipse.esmf.metamodel.AspectModel; -import org.apache.commons.io.FilenameUtils; import picocli.CommandLine; @CommandLine.Command( name = AspectPrettyPrintCommand.COMMAND_NAME, @@ -31,8 +35,7 @@ headerHeading = "@|bold Usage|@:%n%n", descriptionHeading = "%n@|bold Description|@:%n%n", parameterListHeading = "%n@|bold Parameters|@:%n", - optionListHeading = "%n@|bold Options|@:%n", - mixinStandardHelpOptions = true + optionListHeading = "%n@|bold Options|@:%n" ) public class AspectPrettyPrintCommand extends AbstractCommand { public static final String COMMAND_NAME = "prettyprint"; @@ -44,27 +47,41 @@ public class AspectPrettyPrintCommand extends AbstractCommand { private ExternalResolverMixin customResolver; @CommandLine.Option( names = { "--output", "-o" }, description = "Output file path (default: stdout)" ) - private String outputFilePath = "-"; + String outputFilePath = "-"; + + @CommandLine.Option( names = { "--overwrite", "-w" }, description = "Overwrite the input file" ) + boolean overwrite; @CommandLine.ParentCommand private AspectCommand parentCommand; @Override public void run() { - try ( final PrintWriter printWriter = new PrintWriter( getStreamForFile( outputFilePath ) ) ) { - final File inputFile = new File( parentCommand.getInput() ).getAbsoluteFile(); - final AspectModel aspectModel = loadAspectModelOrFail( parentCommand.getInput(), customResolver ); - aspectModel.files() - .stream() - .filter( file -> file.sourceLocation().map( sourceLocation -> sourceLocation.equals( inputFile.toURI() ) ).orElse( false ) ) - .forEach( sourceModel -> { - if ( sourceModel.aspects().size() != 1 ) { - throw new CommandException( "Can only pretty-print files that contain exactly one aspect" ); - } - new PrettyPrinter( sourceModel, printWriter ).print(); - printWriter.flush(); - printWriter.close(); - } ); + final File inputFile = new File( parentCommand.getInput() ).getAbsoluteFile(); + final AspectModel aspectModel = loadAspectModelOrFail( parentCommand.getInput(), customResolver ); + + for ( final AspectModelFile sourceFile : aspectModel.files() ) { + if ( !sourceFile.sourceLocation().map( uri -> uri.equals( inputFile.toURI() ) ).orElse( false ) ) { + continue; + } + + OutputStream outputStream = null; + if ( overwrite ) { + final URI fileUri = sourceFile.sourceLocation().orElseThrow(); + try { + outputStream = new FileOutputStream( new File( fileUri ) ); + } catch ( final FileNotFoundException exception ) { + throw new CommandException( "Can not write to " + fileUri ); + } + } + if ( outputStream == null ) { + outputStream = getStreamForFile( outputFilePath ); + } + + try ( final PrintWriter printWriter = new PrintWriter( outputStream ) ) { + new PrettyPrinter( sourceFile, printWriter ).print(); + printWriter.flush(); + } } } } diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectToCommand.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectToCommand.java index 6bcc10462..57aac58d0 100644 --- a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectToCommand.java +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectToCommand.java @@ -29,7 +29,8 @@ import picocli.CommandLine; -@CommandLine.Command( name = AspectToCommand.COMMAND_NAME, description = "Transforms an Aspect Model into another format", +@CommandLine.Command( name = AspectToCommand.COMMAND_NAME, + description = "Transforms an Aspect Model into another format", subcommands = { CommandLine.HelpCommand.class, AspectToHtmlCommand.class, @@ -45,8 +46,7 @@ }, descriptionHeading = "%n@|bold Description|@:%n%n", parameterListHeading = "%n@|bold Parameters|@:%n", - optionListHeading = "%n@|bold Options|@:%n", - mixinStandardHelpOptions = true + optionListHeading = "%n@|bold Options|@:%n" ) public class AspectToCommand extends AbstractCommand { public static final String COMMAND_NAME = "to"; diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectValidateCommand.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectValidateCommand.java index 1e8fa37ac..0cdcc964a 100644 --- a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectValidateCommand.java +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectValidateCommand.java @@ -34,8 +34,7 @@ headerHeading = "@|bold Usage|@:%n%n", descriptionHeading = "%n@|bold Description|@:%n%n", parameterListHeading = "%n@|bold Parameters|@:%n", - optionListHeading = "%n@|bold Options|@:%n", - mixinStandardHelpOptions = true + optionListHeading = "%n@|bold Options|@:%n" ) @SuppressWarnings( "UseOfSystemOutOrSystemErr" ) public class AspectValidateCommand extends AbstractCommand { diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/edit/AspectEditMoveCommand.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/edit/AspectEditMoveCommand.java new file mode 100644 index 000000000..18b8a2e96 --- /dev/null +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/edit/AspectEditMoveCommand.java @@ -0,0 +1,355 @@ +/* + * 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.aspect.edit; + +import java.io.File; +import java.net.URI; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.eclipse.esmf.AbstractCommand; +import org.eclipse.esmf.ExternalResolverMixin; +import org.eclipse.esmf.LoggingMixin; +import org.eclipse.esmf.aspect.AspectEditCommand; +import org.eclipse.esmf.aspectmodel.AspectModelFile; +import org.eclipse.esmf.aspectmodel.edit.AspectChangeManager; +import org.eclipse.esmf.aspectmodel.edit.AspectChangeManagerConfig; +import org.eclipse.esmf.aspectmodel.edit.AspectChangeManagerConfigBuilder; +import org.eclipse.esmf.aspectmodel.edit.Change; +import org.eclipse.esmf.aspectmodel.edit.ChangeReport; +import org.eclipse.esmf.aspectmodel.edit.ChangeReportFormatter; +import org.eclipse.esmf.aspectmodel.edit.change.MoveElementToExistingFile; +import org.eclipse.esmf.aspectmodel.edit.change.MoveElementToNewFile; +import org.eclipse.esmf.aspectmodel.edit.change.MoveElementToOtherNamespaceExistingFile; +import org.eclipse.esmf.aspectmodel.edit.change.MoveElementToOtherNamespaceNewFile; +import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader; +import org.eclipse.esmf.aspectmodel.serializer.AspectSerializer; +import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn; +import org.eclipse.esmf.exception.CommandException; +import org.eclipse.esmf.metamodel.AspectModel; +import org.eclipse.esmf.metamodel.ModelElement; +import org.eclipse.esmf.metamodel.Namespace; +import org.eclipse.esmf.metamodel.impl.DefaultNamespace; + +import picocli.CommandLine; + +@SuppressWarnings( "UseOfSystemOutOrSystemErr" ) +@CommandLine.Command( name = AspectEditMoveCommand.COMMAND_NAME, + description = "Move elements to other files or namespaces", + descriptionHeading = "%n@|bold Description|@:%n%n", + parameterListHeading = "%n@|bold Parameters|@:%n", + optionListHeading = "%n@|bold Options|@:%n" +) +public class AspectEditMoveCommand extends AbstractCommand { + public static final String COMMAND_NAME = "move"; + + @CommandLine.ParentCommand + private AspectEditCommand parentCommand; + + @CommandLine.Mixin + private LoggingMixin loggingMixin; + + @CommandLine.Mixin + private ExternalResolverMixin customResolver; + + @CommandLine.Parameters( + paramLabel = "ELEMENT", + description = "Full URN or local name of the model element to move", + arity = "1", + index = "0" + ) + private String elementName; + + @CommandLine.Parameters( + paramLabel = "TARGETFILE", + description = "Target file to move the element to", + arity = "1", + index = "1" + ) + private String targetFile; + + @CommandLine.Parameters( + paramLabel = "TARGETNAMESPACE", + description = "Target namespace to move the element to", + arity = "0..1", + index = "2" + ) + private String targetNamespace; + + @CommandLine.Option( + names = { "--dry-run" }, + description = "Emulate edit operation and print a report of changes that would be performed" + ) + private boolean dryRun; + + @CommandLine.Option( + names = { "--details" }, + description = "When using --dry-run, print details about RDF statements that are added and removed" + ) + private boolean details; + + @CommandLine.Option( + names = { "--copy-file-header" }, + description = "If set, newly created files will contain the same file header (e.g., copyright notice) as the source file of the " + + "move operation" + ) + private boolean copyHeader; + + @CommandLine.Option( + names = { "--force" }, + description = "Force creation/overwriting of existing files" + ) + private boolean force; + + @Override + public void run() { + final String input = parentCommand.parentCommand.getInput(); + + // Move to other/new file in same namespace + if ( targetNamespace == null ) { + final File targetFileRelativeToInput = getInputFile( input ).toPath().getParent().resolve( targetFile ).toFile(); + if ( targetFileRelativeToInput.exists() ) { + moveElementToExistingFile( targetFileRelativeToInput ); + } else { + moveElementToNewFile(); + } + return; + } + + // Move to other/new file in other namespace + final File inputFile = getInputFile( input ); + final AspectModelUrn targetNamespaceUrn = AspectModelUrn.from( targetNamespace ) + .getOrElseThrow( () -> new CommandException( "Target namespace is invalid: " + targetNamespace ) ); + final File targetFileInOtherNamespace = modelsRootForFile( inputFile ) + .resolve( targetNamespaceUrn.getNamespaceMainPart() ) + .resolve( targetNamespaceUrn.getVersion() ) + .resolve( targetFile ) + .toFile(); + if ( targetFileInOtherNamespace.exists() ) { + moveElementToOtherNamespaceExistingFile( targetNamespaceUrn, targetFileInOtherNamespace ); + } else { + moveElementToOtherNamespaceNewFile( targetNamespaceUrn, targetFileInOtherNamespace ); + } + } + + /** + * Supports the case {@code samm aspect Aspect.ttl edit move MyAspect newFile.ttl} + */ + private void moveElementToNewFile() { + final String input = parentCommand.parentCommand.getInput(); + final AspectModel aspectModel = loadAspectModelOrFail( input, customResolver ); + + // Do refactoring + final ModelElement modelElement = determineModelElementToMove( aspectModel ); + if ( targetFile.contains( File.separator ) ) { + throw new CommandException( "The target file name should not contain a path; only a file name." ); + } + final URI targetFileUri = getInputFile( input ).toPath().getParent().resolve( targetFile ).toUri(); + final List headerCommentForNewFile = copyHeader + ? modelElement.getSourceFile().headerComment() + : List.of(); + final Change move = new MoveElementToNewFile( modelElement, headerCommentForNewFile, Optional.of( targetFileUri ) ); + performRefactoring( aspectModel, move ).ifPresent( changeContext -> { + // Check & write changes to file system + checkFilesystemConsistency( changeContext ); + performFileSystemWrite( changeContext ); + } ); + } + + /** + * Supports the case {@code samm aspect Aspect.ttl edit move MyAspect newFile.ttl urn:samm:com.example.othernamespace:1.0.0} + */ + private void moveElementToOtherNamespaceNewFile( final AspectModelUrn targetNamespaceUrn, final File targetFileInNewNamespace ) { + final String input = parentCommand.parentCommand.getInput(); + final AspectModel aspectModel = loadAspectModelOrFail( input, customResolver ); + + // Do refactoring + final ModelElement modelElement = determineModelElementToMove( aspectModel ); + if ( targetFile.contains( File.separator ) ) { + throw new CommandException( "The target file name should not contain a path; only a file name: " + targetFile ); + } + + final Namespace namespace = new DefaultNamespace( targetNamespaceUrn, List.of(), Optional.empty() ); + final List headerCommentForNewFile = copyHeader + ? modelElement.getSourceFile().headerComment() + : List.of(); + + final Change move = new MoveElementToOtherNamespaceNewFile( modelElement, namespace, headerCommentForNewFile, + Optional.of( targetFileInNewNamespace.toURI() ) ); + performRefactoring( aspectModel, move ).ifPresent( changeContext -> { + // Check & write changes to file system + checkFilesystemConsistency( changeContext ); + performFileSystemWrite( changeContext ); + } ); + } + + /** + * Support the case {@code samm aspect Aspect.ttl edit move MyAspect existingFile.ttl} + */ + private void moveElementToExistingFile( final File targetFileRelativeToInput ) { + final AspectModel sourceAspectModel = loadAspectModelOrFail( parentCommand.parentCommand.getInput(), customResolver ); + final AspectModel targetAspectModel = loadAspectModelOrFail( targetFileRelativeToInput, customResolver, false ); + + // Create a consistent in-memory representation of both the source and target models. + // On this Aspect Model we can perform the refactoring operation + final AspectModel aspectModel = new AspectModelLoader().merge( sourceAspectModel, targetAspectModel ); + + // Find the loaded AspectModelFile that corresponds to the input targetFile + final ModelElement modelElement = determineModelElementToMove( aspectModel ); + final AspectModelFile targetAspectModelFile = determineTargetAspectModelFile( aspectModel, modelElement.getSourceFile().namespace() ); + + // Do refactoring + final Change move = new MoveElementToExistingFile( modelElement, targetAspectModelFile ); + performRefactoring( aspectModel, move ).ifPresent( changeContext -> { + // Check & write changes to file system + checkFilesystemConsistency( changeContext ); + performFileSystemWrite( changeContext ); + } ); + } + + /** + * Supports the case {@code samm aspect Aspect.ttl edit move MyAspect existingFile.ttl urn:samm:com.example.othernamespace:1.0.0} + */ + private void moveElementToOtherNamespaceExistingFile( final AspectModelUrn targetNamespaceUrn, final File targetFileInOtherNamespace ) { + final AspectModel sourceAspectModel = loadAspectModelOrFail( parentCommand.parentCommand.getInput(), customResolver ); + final AspectModel targetAspectModel = loadAspectModelOrFail( targetFileInOtherNamespace, customResolver, false ); + + // Create a consistent in-memory representation of both the source and target models. + // On this Aspect Model we can perform the refactoring operation + final AspectModel aspectModel = new AspectModelLoader().merge( sourceAspectModel, targetAspectModel ); + + // Find the loaded AspectModelFile that corresponds to the input targetFile + final Namespace namespace = new DefaultNamespace( targetNamespaceUrn, List.of(), Optional.empty() ); + final AspectModelFile targetAspectModelFile = determineTargetAspectModelFile( aspectModel, namespace ); + + // Do refactoring + final ModelElement modelElement = determineModelElementToMove( aspectModel ); + final Change move = new MoveElementToOtherNamespaceExistingFile( modelElement, targetAspectModelFile, namespace ); + performRefactoring( aspectModel, move ).ifPresent( changeContext -> { + // Check & write changes to file system + checkFilesystemConsistency( changeContext ); + performFileSystemWrite( changeContext ); + } ); + } + + private AspectModelFile determineTargetAspectModelFile( final AspectModel aspectModel, final Namespace targetNamespace ) { + return aspectModel.files().stream() + .filter( file -> file.namespace().urn().equals( targetNamespace.urn() ) + && file.sourceLocation().map( uri -> Paths.get( uri ).toFile().getName().equals( targetFile ) ).orElse( false ) ) + .findFirst() + .orElseThrow( () -> new CommandException( "Could not determine target file" ) ); + } + + private ModelElement determineModelElementToMove( final AspectModel aspectModel ) { + final List potentialElements = aspectModel.elements().stream() + .filter( element -> !element.isAnonymous() ) + .filter( element -> element.urn().toString().endsWith( elementName ) ) + .toList(); + if ( potentialElements.isEmpty() ) { + System.out.println( "Could not find element to move: " + elementName ); + System.exit( 1 ); + } + if ( potentialElements.size() > 1 ) { + System.out.println( "Found more than one element identified by " + elementName + ": " + + potentialElements.stream() + .map( element -> element.urn().toString() ).collect( + Collectors.joining( ", ", "[", "]" ) ) + + "\nPlease use the element's full URN." ); + System.exit( 1 ); + } + return potentialElements.get( 0 ); + } + + private Optional performRefactoring( final AspectModel aspectModel, final Change change ) { + final AspectChangeManagerConfig config = AspectChangeManagerConfigBuilder.builder() + .detailedChangeReport( details ) + .build(); + final AspectChangeManager changeContext = new AspectChangeManager( config, aspectModel ); + final ChangeReport changeReport = changeContext.applyChange( change ); + if ( dryRun ) { + System.out.println( "Changes to be performed" ); + System.out.println( "=======================" ); + System.out.println( ChangeReportFormatter.INSTANCE.apply( changeReport, config ) ); + return Optional.empty(); + } + return Optional.of( changeContext ); + } + + private void performFileSystemWrite( final AspectChangeManager changeContext ) { + changeContext.removedFiles() + .map( fileToRemove -> Paths.get( fileToRemove.sourceLocation().orElseThrow() ).toFile() ) + .filter( file -> !file.delete() ) + .forEach( file -> { + throw new CommandException( "Could not delete file: " + file ); + } ); + changeContext.createdFiles().forEach( fileToCreate -> { + final File file = Paths.get( fileToCreate.sourceLocation().orElseThrow() ).toFile(); + file.getParentFile().mkdirs(); + AspectSerializer.INSTANCE.write( fileToCreate ); + } ); + changeContext.modifiedFiles().forEach( AspectSerializer.INSTANCE::write ); + } + + private void checkFilesystemConsistency( final AspectChangeManager changeContext ) { + final List messages = new ArrayList<>(); + changeContext.removedFiles().map( AspectSerializer.INSTANCE::aspectModelFileUrl ).forEach( url -> { + if ( !url.getProtocol().equals( "file" ) ) { + messages.add( "File should be removed, but it is not identified by a file: URL: " + url ); + } + final File file = new File( URI.create( url.toString() ) ); + if ( !file.exists() ) { + messages.add( "File should be removed, but it does not exist: " + file ); + } + } ); + + changeContext.createdFiles().map( AspectSerializer.INSTANCE::aspectModelFileUrl ).forEach( url -> { + if ( !url.getProtocol().equals( "file" ) ) { + messages.add( "New file should be written, but it is not identified by a file: URL: " + url ); + } + final File file = new File( URI.create( url.toString() ) ); + if ( file.exists() && !force ) { + messages.add( + "New file should be written, but it already exists: " + file + ". Use the --force flag to force overwriting." ); + } + if ( file.exists() && force && !file.canWrite() ) { + messages.add( "New file should be written, but it is not writable:" + file ); + } + } ); + + changeContext.modifiedFiles().map( AspectSerializer.INSTANCE::aspectModelFileUrl ).forEach( url -> { + if ( !url.getProtocol().equals( "file" ) ) { + messages.add( "File should be modified, but it is not identified by a file: URL: " + url ); + } + final File file = new File( URI.create( url.toString() ) ); + if ( !file.exists() ) { + messages.add( "File should be modified, but it does not exist: " + file ); + } + if ( !file.canWrite() ) { + messages.add( "File should be modified, but it is not writable: " + file ); + } + if ( !file.isFile() ) { + messages.add( "File should be modified, but it is not a regular file: " + file ); + } + } ); + + if ( !messages.isEmpty() ) { + System.out.println( "Encountered problems, canceling writing." ); + messages.forEach( message -> System.out.println( "- " + message ) ); + System.exit( 1 ); + } + } +} diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToAasCommand.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToAasCommand.java index 873c2574d..56204146e 100644 --- a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToAasCommand.java +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToAasCommand.java @@ -35,8 +35,8 @@ description = "Generate Asset Administration Shell (AAS) submodel template for an Aspect Model", descriptionHeading = "%n@|bold Description|@:%n%n", parameterListHeading = "%n@|bold Parameters|@:%n", - optionListHeading = "%n@|bold Options|@:%n", - mixinStandardHelpOptions = true ) + optionListHeading = "%n@|bold Options|@:%n" +) public class AspectToAasCommand extends AbstractCommand { public static final String COMMAND_NAME = "aas"; private static final Logger LOG = LoggerFactory.getLogger( AspectToAasCommand.class ); diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToAsyncapiCommand.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToAsyncapiCommand.java index 77c36606b..c20733db7 100644 --- a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToAsyncapiCommand.java +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToAsyncapiCommand.java @@ -46,8 +46,7 @@ description = "Generate AsyncAPI specification for an Aspect Model", descriptionHeading = "%n@|bold Description|@:%n%n", parameterListHeading = "%n@|bold Parameters|@:%n", - optionListHeading = "%n@|bold Options|@:%n", - mixinStandardHelpOptions = true + optionListHeading = "%n@|bold Options|@:%n" ) public class AspectToAsyncapiCommand extends AbstractCommand { diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToHtmlCommand.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToHtmlCommand.java index a879d9506..53e5e7761 100644 --- a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToHtmlCommand.java +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToHtmlCommand.java @@ -33,8 +33,7 @@ description = "Generate HTML documentation for an Aspect Model", descriptionHeading = "%n@|bold Description|@:%n%n", parameterListHeading = "%n@|bold Parameters|@:%n", - optionListHeading = "%n@|bold Options|@:%n", - mixinStandardHelpOptions = true + optionListHeading = "%n@|bold Options|@:%n" ) public class AspectToHtmlCommand extends AbstractCommand { public static final String COMMAND_NAME = "html"; diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToJavaCommand.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToJavaCommand.java index 66053ce63..4cf507171 100644 --- a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToJavaCommand.java +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToJavaCommand.java @@ -34,8 +34,7 @@ description = "Generate Java domain classes for an Aspect Model", descriptionHeading = "%n@|bold Description|@:%n%n", parameterListHeading = "%n@|bold Parameters|@:%n", - optionListHeading = "%n@|bold Options|@:%n", - mixinStandardHelpOptions = true + optionListHeading = "%n@|bold Options|@:%n" ) public class AspectToJavaCommand extends AbstractCommand { public static final String COMMAND_NAME = "java"; @@ -87,7 +86,7 @@ private JavaCodeGenerationConfig buildConfig( final Aspect aspect ) { final File templateLibFile = Path.of( templateLib ).toFile(); final String pkgName = Optional.ofNullable( packageName ) .flatMap( pkg -> pkg.isBlank() ? Optional.empty() : Optional.of( pkg ) ) - .orElseGet( () -> aspect.urn().getNamespace() ); + .orElseGet( () -> aspect.urn().getNamespaceMainPart() ); return JavaCodeGenerationConfigBuilder.builder() .executeLibraryMacros( executeLibraryMacros ) .templateLibFile( templateLibFile ) diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToJsonCommand.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToJsonCommand.java index 753fce7ab..670466d4a 100644 --- a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToJsonCommand.java +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToJsonCommand.java @@ -28,8 +28,7 @@ description = "Generate OpenAPI JSON specification for an Aspect Model", descriptionHeading = "%n@|bold Description|@:%n%n", parameterListHeading = "%n@|bold Parameters|@:%n", - optionListHeading = "%n@|bold Options|@:%n", - mixinStandardHelpOptions = true + optionListHeading = "%n@|bold Options|@:%n" ) public class AspectToJsonCommand extends AbstractCommand { public static final String COMMAND_NAME = "json"; diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToJsonSchemaCommand.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToJsonSchemaCommand.java index 950447546..0c5eb64b7 100644 --- a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToJsonSchemaCommand.java +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToJsonSchemaCommand.java @@ -35,8 +35,7 @@ description = "Generate JSON schema for an Aspect Model", descriptionHeading = "%n@|bold Description|@:%n%n", parameterListHeading = "%n@|bold Parameters|@:%n", - optionListHeading = "%n@|bold Options|@:%n", - mixinStandardHelpOptions = true + optionListHeading = "%n@|bold Options|@:%n" ) public class AspectToJsonSchemaCommand extends AbstractCommand { public static final String COMMAND_NAME = "schema"; diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToOpenapiCommand.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToOpenapiCommand.java index 6481b985e..6fd7a7b0b 100644 --- a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToOpenapiCommand.java +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToOpenapiCommand.java @@ -56,8 +56,7 @@ description = "Generate OpenAPI specification for an Aspect Model", descriptionHeading = "%n@|bold Description|@:%n%n", parameterListHeading = "%n@|bold Parameters|@:%n", - optionListHeading = "%n@|bold Options|@:%n", - mixinStandardHelpOptions = true + optionListHeading = "%n@|bold Options|@:%n" ) public class AspectToOpenapiCommand extends AbstractCommand { public static final String COMMAND_NAME = "openapi"; @@ -214,7 +213,7 @@ private void writeSchemaWithSeparateFiles( final OpenApiSchemaArtifact openApiSp } } - private ObjectNode readFile( String file ) throws CommandException { + private ObjectNode readFile( final String file ) throws CommandException { if ( StringUtils.isBlank( file ) ) { return null; } diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToPngCommand.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToPngCommand.java index 9cd56b296..b7ded2875 100644 --- a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToPngCommand.java +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToPngCommand.java @@ -28,8 +28,7 @@ description = "Generate PNG diagram for an Aspect Model", descriptionHeading = "%n@|bold Description|@:%n%n", parameterListHeading = "%n@|bold Parameters|@:%n", - optionListHeading = "%n@|bold Options|@:%n", - mixinStandardHelpOptions = true + optionListHeading = "%n@|bold Options|@:%n" ) public class AspectToPngCommand extends AbstractCommand { public static final String COMMAND_NAME = "png"; diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToSqlCommand.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToSqlCommand.java index b4adb80b3..eb323768d 100644 --- a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToSqlCommand.java +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToSqlCommand.java @@ -38,8 +38,7 @@ description = "Generate SQL table creation script for an Aspect Model", descriptionHeading = "%n@|bold Description|@:%n%n", parameterListHeading = "%n@|bold Parameters|@:%n", - optionListHeading = "%n@|bold Options|@:%n", - mixinStandardHelpOptions = true + optionListHeading = "%n@|bold Options|@:%n" ) public class AspectToSqlCommand extends AbstractCommand { public static final String COMMAND_NAME = "sql"; diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToSvgCommand.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToSvgCommand.java index 059b796d4..ff0eadbc2 100644 --- a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToSvgCommand.java +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToSvgCommand.java @@ -28,8 +28,7 @@ description = "Generate SVG diagram for an Aspect Model", descriptionHeading = "%n@|bold Description|@:%n%n", parameterListHeading = "%n@|bold Parameters|@:%n", - optionListHeading = "%n@|bold Options|@:%n", - mixinStandardHelpOptions = true + optionListHeading = "%n@|bold Options|@:%n" ) public class AspectToSvgCommand extends AbstractCommand { public static final String COMMAND_NAME = "svg"; diff --git a/tools/samm-cli/src/test/java/org/eclipse/esmf/SammCliTest.java b/tools/samm-cli/src/test/java/org/eclipse/esmf/SammCliTest.java index 2a9e88fc3..baee7e37b 100644 --- a/tools/samm-cli/src/test/java/org/eclipse/esmf/SammCliTest.java +++ b/tools/samm-cli/src/test/java/org/eclipse/esmf/SammCliTest.java @@ -22,6 +22,7 @@ import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; +import java.io.PrintWriter; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -34,11 +35,13 @@ import org.eclipse.esmf.ProcessLauncher.ExecutionResult; import org.eclipse.esmf.aspect.AspectValidateCommand; import org.eclipse.esmf.aspectmodel.shacl.violation.InvalidSyntaxViolation; +import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn; import org.eclipse.esmf.samm.KnownVersion; import org.eclipse.esmf.test.InvalidTestAspect; import org.eclipse.esmf.test.TestAspect; import org.eclipse.esmf.test.TestModel; +import org.apache.commons.io.FileUtils; import org.apache.tika.config.TikaConfig; import org.apache.tika.exception.TikaException; import org.apache.tika.metadata.Metadata; @@ -124,6 +127,22 @@ void testHelp() { assertThat( result.stderr() ).isEmpty(); } + @Test + void testSubCommandHelp() { + final ExecutionResult result = sammCli.runAndExpectSuccess( "--disable-color", "help", "aspect", "prettyprint" ); + assertThat( result.stdout() ).contains( "Usage:" ); + assertThat( result.stdout() ).contains( "--overwrite" ); + assertThat( result.stderr() ).isEmpty(); + } + + @Test + void testSubSubCommandHelp() { + final ExecutionResult result = sammCli.runAndExpectSuccess( "--disable-color", "help", "aspect", "to", "svg" ); + assertThat( result.stdout() ).contains( "Usage:" ); + assertThat( result.stdout() ).contains( "--language" ); + assertThat( result.stderr() ).isEmpty(); + } + @Test void testVerboseOutput() { final ExecutionResult result = sammCli.runAndExpectSuccess( "--disable-color", "aspect", defaultInputFile, "validate", "-vvv" ); @@ -149,6 +168,22 @@ void testAspectPrettyPrintToStdout() { assertThat( result.stderr() ).isEmpty(); } + @Test + void testAspectPrettyPrintOverwrite() throws IOException { + final File targetFile = outputFile( "output.ttl" ); + FileUtils.copyFile( new File( defaultInputFile ), targetFile ); + assertThat( targetFile ).content().contains( "@prefix xsd:" ); + + final ExecutionResult result = sammCli.runAndExpectSuccess( "--disable-color", "aspect", targetFile.getAbsolutePath(), "prettyprint", + "--overwrite" ); + assertThat( result.stdout() ).isEmpty(); + assertThat( result.stderr() ).isEmpty(); + assertThat( targetFile ).exists(); + assertThat( targetFile ).content().contains( "@prefix" ); + // The xsd prefix is not actually used in the file, so it is removed by the pretty printer + assertThat( targetFile ).content().doesNotContain( "@prefix xsd:" ); + } + @Test void testAspectValidateValidModel() { final ExecutionResult result = sammCli.runAndExpectSuccess( "--disable-color", "aspect", defaultInputFile, "validate" ); @@ -351,7 +386,7 @@ void testAasToAspectModel() { assertThat( result.stdout() ).isEmpty(); assertThat( result.stderr() ).isEmpty(); - final File directory = outputDirectory.resolve( testModel.getUrn().getNamespace() ) + final File directory = outputDirectory.resolve( testModel.getUrn().getNamespaceMainPart() ) .resolve( testModel.getUrn().getVersion() ) .toFile(); assertThat( directory ).exists(); @@ -391,7 +426,7 @@ void testAasToAspectModelWithSelectedSubmodels() { assertThat( result.stdout() ).isEmpty(); assertThat( result.stderr() ).isEmpty(); - final File directory = outputDirectory.resolve( testModel.getUrn().getNamespace() ) + final File directory = outputDirectory.resolve( testModel.getUrn().getNamespaceMainPart() ) .resolve( testModel.getUrn().getVersion() ) .toFile(); assertThat( directory ).exists(); @@ -1048,11 +1083,223 @@ void testAspectToSqlWithCustomColumnToStdout() { assertThat( result.stderr() ).isEmpty(); } + @Test + void testAspectEditMoveExistingFile() throws IOException { + // Set up file system structure of writable files + final Path modelLocation = outputDirectory.resolve( testModel.getUrn().getNamespaceMainPart() ) + .resolve( testModel.getUrn().getVersion() ); + modelLocation.toFile().mkdirs(); + final File inputFile = modelLocation.resolve( "AspectWithEntity.ttl" ).toFile(); + FileUtils.copyFile( inputFile( testModel ).getAbsoluteFile(), inputFile ); + final File targetFile = modelLocation.resolve( "target.ttl" ).toFile(); + Files.createFile( targetFile.toPath() ); + try ( final PrintWriter out = new PrintWriter( targetFile ) ) { + out.printf( "@prefix : <%s> .", testModel.getUrn().getUrnPrefix() ); + } + + // Run refactoring + assertThat( inputFile ).content().contains( ":AspectWithEntity" ); + assertThat( targetFile ).content().doesNotContain( ":AspectWithEntity" ); + final ExecutionResult result = sammCli.runAndExpectSuccess( "--disable-color", "aspect", inputFile.getAbsolutePath(), + "edit", "move", "AspectWithEntity", targetFile.getName() ); + assertThat( result.stdout() ).isEmpty(); + assertThat( result.stderr() ).isEmpty(); + assertThat( inputFile ).content().doesNotContain( ":AspectWithEntity" ); + assertThat( targetFile ).content().contains( ":AspectWithEntity" ); + } + + @Test + void testAspectEditMoveExistingFileDryRun() throws IOException { + // Set up file system structure of writable files + final Path modelLocation = outputDirectory.resolve( testModel.getUrn().getNamespaceMainPart() ) + .resolve( testModel.getUrn().getVersion() ); + modelLocation.toFile().mkdirs(); + final File inputFile = modelLocation.resolve( "AspectWithEntity.ttl" ).toFile(); + FileUtils.copyFile( inputFile( testModel ).getAbsoluteFile(), inputFile ); + final File targetFile = modelLocation.resolve( "target.ttl" ).toFile(); + Files.createFile( targetFile.toPath() ); + try ( final PrintWriter out = new PrintWriter( targetFile ) ) { + out.printf( "@prefix : <%s> .", testModel.getUrn().getUrnPrefix() ); + } + + // Run refactoring + final ExecutionResult result = sammCli.runAndExpectSuccess( "--disable-color", "aspect", inputFile.getAbsolutePath(), + "edit", "move", "--dry-run", "--details", "AspectWithEntity", targetFile.getName() ); + assertThat( result.stdout() ).contains( "Changes to be performed" ); + assertThat( result.stdout() ).contains( "Remove definition of urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEntity" ); + assertThat( result.stdout() ).contains( "Add definition of urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEntity" ); + assertThat( result.stderr() ).isEmpty(); + assertThat( inputFile ).content().contains( ":AspectWithEntity" ); + assertThat( targetFile ).content().doesNotContain( ":AspectWithEntity" ); + } + + @Test + void testAspectEditMoveNewFile() throws IOException { + // Set up file system structure of writable files + final Path modelLocation = outputDirectory.resolve( testModel.getUrn().getNamespaceMainPart() ) + .resolve( testModel.getUrn().getVersion() ); + modelLocation.toFile().mkdirs(); + final File inputFile = modelLocation.resolve( "AspectWithEntity.ttl" ).toFile(); + FileUtils.copyFile( inputFile( testModel ).getAbsoluteFile(), inputFile ); + final File targetFile = modelLocation.resolve( "target.ttl" ).toFile(); + + // Run refactoring + assertThat( inputFile ).content().contains( ":AspectWithEntity" ); + assertThat( targetFile ).doesNotExist(); + final ExecutionResult result = sammCli.runAndExpectSuccess( "--disable-color", "aspect", inputFile.getAbsolutePath(), + "edit", "move", "--copy-file-header", "AspectWithEntity", targetFile.getName() ); + assertThat( result.stdout() ).isEmpty(); + assertThat( result.stderr() ).isEmpty(); + assertThat( inputFile ).content().doesNotContain( ":AspectWithEntity" ); + assertThat( targetFile ).exists(); + assertThat( targetFile ).content().contains( ":AspectWithEntity" ); + assertThat( targetFile ).content().contains( "# Copyright" ); + } + + @Test + void testAspectEditMoveNewFileDryRun() throws IOException { + // Set up file system structure of writable files + final Path modelLocation = outputDirectory.resolve( testModel.getUrn().getNamespaceMainPart() ) + .resolve( testModel.getUrn().getVersion() ); + modelLocation.toFile().mkdirs(); + final File inputFile = modelLocation.resolve( "AspectWithEntity.ttl" ).toFile(); + FileUtils.copyFile( inputFile( testModel ).getAbsoluteFile(), inputFile ); + final File targetFile = modelLocation.resolve( "target.ttl" ).toFile(); + + // Run refactoring + assertThat( inputFile ).content().contains( ":AspectWithEntity" ); + assertThat( targetFile ).doesNotExist(); + final ExecutionResult result = sammCli.runAndExpectSuccess( "--disable-color", "aspect", inputFile.getAbsolutePath(), + "edit", "move", "--dry-run", "--details", "AspectWithEntity", targetFile.getName() ); + assertThat( result.stdout() ).contains( "Changes to be performed" ); + assertThat( result.stdout() ).contains( "Remove definition of urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEntity" ); + assertThat( result.stdout() ).contains( "Add definition of urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEntity" ); + assertThat( result.stderr() ).isEmpty(); + assertThat( inputFile ).content().contains( ":AspectWithEntity" ); + assertThat( targetFile ).doesNotExist(); + } + + @Test + void testAspectEditMoveOtherNamespaceExistingFile() throws IOException { + // Set up file system structure of writable files + final Path modelLocationNs1 = outputDirectory.resolve( testModel.getUrn().getNamespaceMainPart() ) + .resolve( testModel.getUrn().getVersion() ); + modelLocationNs1.toFile().mkdirs(); + final File inputFile = modelLocationNs1.resolve( "AspectWithEntity.ttl" ).toFile(); + FileUtils.copyFile( inputFile( testModel ).getAbsoluteFile(), inputFile ); + final String targetNamespace = "urn:samm:org.eclipse.example.newnamespace:1.0.0"; + final AspectModelUrn newNamespace = AspectModelUrn.fromUrn( targetNamespace ); + final Path modelLocationNs2 = outputDirectory.resolve( newNamespace.getNamespaceMainPart() ) + .resolve( newNamespace.getVersion() ); + modelLocationNs2.toFile().mkdirs(); + final File targetFile = modelLocationNs2.resolve( "target.ttl" ).toFile(); + Files.createFile( targetFile.toPath() ); + try ( final PrintWriter out = new PrintWriter( targetFile ) ) { + out.printf( "@prefix : <%s#> .", targetNamespace ); + } + + // Run refactoring + assertThat( inputFile ).content().contains( ":AspectWithEntity" ); + assertThat( targetFile ).content().doesNotContain( ":AspectWithEntity" ); + final ExecutionResult result = sammCli.runAndExpectSuccess( "--disable-color", "aspect", inputFile.getAbsolutePath(), + "edit", "move", "AspectWithEntity", targetFile.getName(), targetNamespace ); + assertThat( result.stdout() ).isEmpty(); + assertThat( result.stderr() ).isEmpty(); + assertThat( inputFile ).content().doesNotContain( ":AspectWithEntity" ); + assertThat( targetFile ).content().contains( ":AspectWithEntity" ); + } + + @Test + void testAspectEditMoveOtherNamespaceExistingFileDryRun() throws IOException { + // Set up file system structure of writable files + final Path modelLocationNs1 = outputDirectory.resolve( testModel.getUrn().getNamespaceMainPart() ) + .resolve( testModel.getUrn().getVersion() ); + modelLocationNs1.toFile().mkdirs(); + final File inputFile = modelLocationNs1.resolve( "AspectWithEntity.ttl" ).toFile(); + FileUtils.copyFile( inputFile( testModel ).getAbsoluteFile(), inputFile ); + final String targetNamespace = "urn:samm:org.eclipse.example.newnamespace:1.0.0"; + final AspectModelUrn newNamespace = AspectModelUrn.fromUrn( targetNamespace ); + final Path modelLocationNs2 = outputDirectory.resolve( newNamespace.getNamespaceMainPart() ) + .resolve( newNamespace.getVersion() ); + modelLocationNs2.toFile().mkdirs(); + final File targetFile = modelLocationNs2.resolve( "target.ttl" ).toFile(); + Files.createFile( targetFile.toPath() ); + try ( final PrintWriter out = new PrintWriter( targetFile ) ) { + out.printf( "@prefix : <%s#> .", targetNamespace ); + } + + // Run refactoring + assertThat( inputFile ).content().contains( ":AspectWithEntity" ); + assertThat( targetFile ).content().doesNotContain( ":AspectWithEntity" ); + final ExecutionResult result = sammCli.runAndExpectSuccess( "--disable-color", "aspect", inputFile.getAbsolutePath(), + "edit", "move", "--dry-run", "--details", "AspectWithEntity", targetFile.getName(), targetNamespace ); + assertThat( result.stdout() ).contains( "Changes to be performed" ); + assertThat( result.stdout() ).contains( "Remove definition of urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEntity" ); + assertThat( result.stdout() ).contains( "Add definition of urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEntity" ); + assertThat( result.stderr() ).isEmpty(); + assertThat( inputFile ).content().contains( ":AspectWithEntity" ); + assertThat( targetFile ).content().doesNotContain( ":AspectWithEntity" ); + } + + @Test + void testAspectEditMoveOtherNamespaceNewFile() throws IOException { + // Set up file system structure of writable files + final Path modelLocationNs1 = outputDirectory.resolve( testModel.getUrn().getNamespaceMainPart() ) + .resolve( testModel.getUrn().getVersion() ); + modelLocationNs1.toFile().mkdirs(); + final File inputFile = modelLocationNs1.resolve( "AspectWithEntity.ttl" ).toFile(); + FileUtils.copyFile( inputFile( testModel ).getAbsoluteFile(), inputFile ); + final String targetNamespace = "urn:samm:org.eclipse.example.newnamespace:1.0.0"; + final AspectModelUrn newNamespace = AspectModelUrn.fromUrn( targetNamespace ); + final Path modelLocationNs2 = outputDirectory.resolve( newNamespace.getNamespaceMainPart() ) + .resolve( newNamespace.getVersion() ); + final File targetFile = modelLocationNs2.resolve( "target.ttl" ).toFile(); + + // Run refactoring + assertThat( inputFile ).content().contains( ":AspectWithEntity" ); + assertThat( targetFile ).doesNotExist(); + final ExecutionResult result = sammCli.runAndExpectSuccess( "--disable-color", "aspect", inputFile.getAbsolutePath(), + "edit", "move", "--copy-file-header", "AspectWithEntity", targetFile.getName(), targetNamespace ); + assertThat( result.stdout() ).isEmpty(); + assertThat( result.stderr() ).isEmpty(); + assertThat( inputFile ).content().doesNotContain( ":AspectWithEntity" ); + assertThat( targetFile ).exists(); + assertThat( targetFile ).content().contains( ":AspectWithEntity" ); + assertThat( targetFile ).content().contains( "# Copyright" ); + } + + @Test + void testAspectEditMoveOtherNamespaceNewFileDryRun() throws IOException { + // Set up file system structure of writable files + final Path modelLocationNs1 = outputDirectory.resolve( testModel.getUrn().getNamespaceMainPart() ) + .resolve( testModel.getUrn().getVersion() ); + modelLocationNs1.toFile().mkdirs(); + final File inputFile = modelLocationNs1.resolve( "AspectWithEntity.ttl" ).toFile(); + FileUtils.copyFile( inputFile( testModel ).getAbsoluteFile(), inputFile ); + final String targetNamespace = "urn:samm:org.eclipse.example.newnamespace:1.0.0"; + final AspectModelUrn newNamespace = AspectModelUrn.fromUrn( targetNamespace ); + final Path modelLocationNs2 = outputDirectory.resolve( newNamespace.getNamespaceMainPart() ) + .resolve( newNamespace.getVersion() ); + final File targetFile = modelLocationNs2.resolve( "target.ttl" ).toFile(); + + // Run refactoring + assertThat( inputFile ).content().contains( ":AspectWithEntity" ); + assertThat( targetFile ).doesNotExist(); + final ExecutionResult result = sammCli.runAndExpectSuccess( "--disable-color", "aspect", inputFile.getAbsolutePath(), + "edit", "move", "--dry-run", "--details", "AspectWithEntity", targetFile.getName(), targetNamespace ); + assertThat( result.stdout() ).contains( "Changes to be performed" ); + assertThat( result.stdout() ).contains( "Remove definition of urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEntity" ); + assertThat( result.stdout() ).contains( "Add definition of urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEntity" ); + assertThat( result.stderr() ).isEmpty(); + assertThat( inputFile ).content().contains( ":AspectWithEntity" ); + assertThat( targetFile ).doesNotExist(); + } + /** * Returns the File object for a test model file */ private File inputFile( final TestModel testModel ) { - final boolean isValid = !(testModel instanceof InvalidTestAspect); + final boolean isValid = !( testModel instanceof InvalidTestAspect ); final String resourcePath = String.format( "%s/../../core/esmf-test-aspect-models/src/main/resources/%s/org.eclipse.esmf.test/1.0.0/%s.ttl", System.getProperty( "user.dir" ), isValid ? "valid" : "invalid", testModel.getName() ); @@ -1103,8 +1350,8 @@ private String resolverCommand() { // are not resolved to the file system but to the jar) try { final String resolverScript = new File( - System.getProperty( "user.dir" ) + "/target/test-classes/model_resolver" + (OS.WINDOWS.isCurrentOs() - ? ".bat" : ".sh") ).getCanonicalPath(); + System.getProperty( "user.dir" ) + "/target/test-classes/model_resolver" + ( OS.WINDOWS.isCurrentOs() + ? ".bat" : ".sh" ) ).getCanonicalPath(); final String modelsRoot = new File( System.getProperty( "user.dir" ) + "/target/classes/valid" ).getCanonicalPath(); final String metaModelVersion = KnownVersion.getLatest().toString().toLowerCase(); return resolverScript + " " + modelsRoot + " " + metaModelVersion;