Skip to content

Commit dc7456d

Browse files
authored
Fix nondeterministic results in ModelService (#88)
1 parent d43938a commit dc7456d

File tree

6 files changed

+54
-219
lines changed

6 files changed

+54
-219
lines changed

aspect-model-editor-service/src/main/java/org/eclipse/esmf/ame/services/ModelService.java

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,11 @@
1616
import java.io.ByteArrayInputStream;
1717
import java.io.File;
1818
import java.io.IOException;
19-
import java.net.URI;
2019
import java.nio.file.Path;
2120
import java.util.ArrayList;
2221
import java.util.List;
2322
import java.util.Map;
2423
import java.util.function.Supplier;
25-
import java.util.stream.Stream;
2624

2725
import org.eclipse.esmf.ame.exceptions.CreateFileException;
2826
import org.eclipse.esmf.ame.exceptions.FileNotFoundException;
@@ -41,6 +39,7 @@
4139
import org.eclipse.esmf.aspectmodel.edit.change.IncreaseVersion;
4240
import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
4341
import org.eclipse.esmf.aspectmodel.resolver.exceptions.ModelResolutionException;
42+
import org.eclipse.esmf.aspectmodel.resolver.fs.StructuredModelsRoot;
4443
import org.eclipse.esmf.aspectmodel.serializer.AspectSerializer;
4544
import org.eclipse.esmf.aspectmodel.shacl.violation.Violation;
4645
import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn;
@@ -72,25 +71,24 @@ public ModelService( final AspectModelValidator aspectModelValidator, final Aspe
7271
this.modelPath = modelPath;
7372
}
7473

75-
public String getModel( final String aspectModelUrn, final String filePath ) {
74+
public String getModel( final AspectModelUrn aspectModelUrn, final String filePath ) {
7675
try {
7776
final AspectModel aspectModel = ( filePath != null ) ?
7877
ModelUtils.loadModelFromFile( modelPath, filePath, aspectModelLoader ) :
7978
loadModelFromUrn( aspectModelUrn );
8079
validateModel( aspectModel );
8180

8281
return aspectModel.files().stream()
83-
.filter( a -> a.elements().stream().anyMatch( e -> e.urn().equals( AspectModelUrn.fromUrn( aspectModelUrn ) ) ) ).findFirst()
82+
.filter( a -> a.elements().stream().anyMatch( e -> e.urn().equals( aspectModelUrn ) ) ).findFirst()
8483
.map( AspectSerializer.INSTANCE::aspectModelFileToString )
8584
.orElseThrow( () -> new FileNotFoundException( "Aspect Model not found" ) );
8685
} catch ( final ModelResolutionException e ) {
8786
throw new FileNotFoundException( e.getMessage(), e );
8887
}
8988
}
9089

91-
private AspectModel loadModelFromUrn( final String aspectModelUrn ) {
92-
final Supplier<AspectModel> aspectModelSupplier = ModelUtils.getAspectModelSupplier( AspectModelUrn.fromUrn( aspectModelUrn ),
93-
aspectModelLoader );
90+
private AspectModel loadModelFromUrn( final AspectModelUrn aspectModelUrn ) {
91+
final Supplier<AspectModel> aspectModelSupplier = ModelUtils.getAspectModelSupplier( aspectModelUrn, aspectModelLoader );
9492
return aspectModelSupplier.get();
9593
}
9694

@@ -101,9 +99,9 @@ private void validateModel( final AspectModel aspectModel ) {
10199
}
102100
}
103101

104-
public void createOrSaveModel( final String turtleData, final String urn, final String fileName, final Path storagePath ) {
102+
public void createOrSaveModel( final String turtleData, final AspectModelUrn aspectModelUrn, final String fileName,
103+
final Path storagePath ) {
105104
try {
106-
final AspectModelUrn aspectModelUrn = AspectModelUrn.fromUrn( urn );
107105
final File newFile = ModelUtils.createFile( aspectModelUrn, fileName, storagePath );
108106

109107
final Supplier<AspectModel> aspectModelSupplier = ModelUtils.getAspectModelSupplier( turtleData, newFile, aspectModelLoader );
@@ -115,13 +113,12 @@ public void createOrSaveModel( final String turtleData, final String urn, final
115113

116114
AspectSerializer.INSTANCE.write( aspectModelSupplier.get().files().getFirst() );
117115
} catch ( final IOException e ) {
118-
throw new CreateFileException( String.format( "Cannot create file %s on workspace", urn ), e );
116+
throw new CreateFileException( String.format( "Cannot create file %s on workspace", aspectModelUrn ), e );
119117
}
120118
}
121119

122-
public void deleteModel( final String aspectModelUrn ) {
123-
final Path filePath = ModelUtils.resolvePathFromUrn( aspectModelUrn, modelPath );
124-
ModelUtils.deleteEmptyFiles( filePath.toFile() );
120+
public void deleteModel( final AspectModelUrn aspectModelUrn ) {
121+
ModelUtils.deleteEmptyFiles( new StructuredModelsRoot( modelPath ).determineAspectModelFile( aspectModelUrn ) );
125122
}
126123

127124
public ViolationReport validateModel( final String turtleData ) {
@@ -153,11 +150,10 @@ public String getFormattedModel( final String turtleData ) {
153150

154151
public Map<String, List<Version>> getAllNamespaces() {
155152
try {
156-
final Stream<URI> uriStream = aspectModelLoader.listContents();
157-
return new ModelGroupingUtils( this.aspectModelLoader, this.modelPath ).groupModelsByNamespaceAndVersion( uriStream );
153+
return new ModelGroupingUtils( aspectModelLoader ).groupModelsByNamespaceAndVersion( aspectModelLoader.listContents() );
158154
} catch ( final UnsupportedVersionException e ) {
159-
LOG.error( "{} There is a loose .ttl file somewhere — remove it along with any other non-standardized files.", sammStructureInfo,
160-
e );
155+
LOG.error( "{} There is a loose .ttl file somewhere — remove it along with any other non-standardized files.",
156+
sammStructureInfo, e );
161157
throw new FileReadException( sammStructureInfo + " Remove all non-standardized files." );
162158
}
163159
}

aspect-model-editor-service/src/main/java/org/eclipse/esmf/ame/services/PackageService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public List<Map<String, String>> validatePackage( final CompletedFileUpload zipF
116116

117117
public Map<String, List<Version>> importPackage( final CompletedFileUpload zipFile ) {
118118
try {
119-
final ModelsRoot modelsRoot = new StructuredModelsRoot( this.modelPath );
119+
final ModelsRoot modelsRoot = new StructuredModelsRoot( modelPath );
120120
final byte[] zipContent = IOUtils.toByteArray( zipFile.getInputStream() );
121121
final NamespacePackage namespacePackage = new NamespacePackage( zipContent, null );
122122

@@ -128,7 +128,7 @@ public Map<String, List<Version>> importPackage( final CompletedFileUpload zipFi
128128

129129
final Stream<URI> savedUris = saveAspectModelFiles( changeManager.aspectModelFiles() );
130130

131-
return new ModelGroupingUtils( this.aspectModelLoader, this.modelPath ).groupModelsByNamespaceAndVersion( savedUris );
131+
return new ModelGroupingUtils( aspectModelLoader ).groupModelsByNamespaceAndVersion( savedUris );
132132
} catch ( final IOException e ) {
133133
throw new ModelResolutionException( "Could not read from input", e );
134134
}

aspect-model-editor-service/src/main/java/org/eclipse/esmf/ame/services/utils/ModelGroupingUtils.java

Lines changed: 18 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -15,41 +15,31 @@
1515

1616
import java.io.File;
1717
import java.net.URI;
18-
import java.nio.file.Path;
19-
import java.nio.file.Paths;
2018
import java.util.Comparator;
19+
import java.util.LinkedHashMap;
2120
import java.util.List;
2221
import java.util.Map;
23-
import java.util.TreeMap;
24-
import java.util.regex.Pattern;
2522
import java.util.stream.Collectors;
2623
import java.util.stream.Stream;
2724

2825
import org.eclipse.esmf.ame.services.models.Model;
2926
import org.eclipse.esmf.ame.services.models.Version;
3027
import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
31-
import org.eclipse.esmf.metamodel.AspectModel;
3228
import org.eclipse.esmf.metamodel.ModelElement;
3329

34-
import io.vavr.Tuple;
35-
3630
/**
3731
* A utility class for grouping model URIs by namespace and version.
3832
*/
3933
public class ModelGroupingUtils {
40-
4134
private final AspectModelLoader aspectModelLoader;
42-
private final Path modelPath;
4335

4436
/**
4537
* Constructs a ModelGrouper with the given base model path.
4638
*
4739
* @param aspectModelLoader the loader for aspect models
48-
* @param modelPath the base path to relativize URIs against
4940
*/
50-
public ModelGroupingUtils( final AspectModelLoader aspectModelLoader, final Path modelPath ) {
41+
public ModelGroupingUtils( final AspectModelLoader aspectModelLoader ) {
5142
this.aspectModelLoader = aspectModelLoader;
52-
this.modelPath = modelPath;
5343
}
5444

5545
/**
@@ -59,136 +49,21 @@ public ModelGroupingUtils( final AspectModelLoader aspectModelLoader, final Path
5949
* @return a map where the keys are namespaces and the values are lists of maps containing versions and their associated models
6050
*/
6151
public Map<String, List<Version>> groupModelsByNamespaceAndVersion( final Stream<URI> uriStream ) {
62-
final List<URI> sortedUris = uriStream
63-
.sorted( Comparator.comparing( uri -> modelPath.relativize( Paths.get( uri ) ).toString() ) )
64-
.toList();
65-
66-
return sortedUris.stream()
67-
.map( this::relativizePath )
68-
.map( relativePath -> {
69-
final AspectModel aspectModel = ModelUtils.loadModelFromFile( modelPath, relativePath, this.aspectModelLoader );
70-
71-
final ModelElement modelElement = !aspectModel.aspects().isEmpty()
72-
? aspectModel.aspect()
73-
: aspectModel.files().stream()
74-
.flatMap( file -> file.elements().stream() )
75-
.findFirst()
76-
.orElse( null );
77-
78-
return Tuple.of( modelElement, splitPath( relativePath ) );
79-
} )
80-
.collect( Collectors.groupingBy(
81-
tuple -> extractNamespace( tuple._2 ),
82-
TreeMap::new,
83-
Collectors.collectingAndThen(
84-
Collectors.groupingBy(
85-
tuple -> extractVersion( tuple._2 ),
86-
Collectors.mapping(
87-
tuple -> {
88-
final String[] parts = tuple._2;
89-
final Path resolvedPath = Paths.get( parts[0], parts[1], parts[2] );
90-
final boolean fileExists = modelPath.resolve( resolvedPath ).toFile().exists();
91-
return createModel( parts, fileExists, tuple._1 );
92-
},
93-
Collectors.toList()
94-
)
95-
),
96-
this::convertAndSortVersionMap
97-
)
98-
) );
99-
}
100-
101-
/**
102-
* Relativizes the given URI against the base model path.
103-
*
104-
* @param uri the URI to relativize
105-
* @return the relativized path as a string
106-
*/
107-
private String relativizePath( final URI uri ) {
108-
return modelPath.relativize( Path.of( uri ) ).toString();
109-
}
110-
111-
/**
112-
* Splits the given path string into parts using the file separator.
113-
*
114-
* @param path the path string to split
115-
* @return an array of path parts
116-
*/
117-
private String[] splitPath( final String path ) {
118-
return path.split( Pattern.quote( File.separator ) );
119-
}
120-
121-
/**
122-
* Extracts the namespace from the given path parts.
123-
*
124-
* @param parts an array of path parts
125-
* @return the namespace (first part of the path)
126-
*/
127-
private String extractNamespace( final String[] parts ) {
128-
return parts[0];
129-
}
130-
131-
/**
132-
* Extracts the version from the given path parts.
133-
*
134-
* @param parts an array of path parts
135-
* @return the version (second part of the path)
136-
*/
137-
private String extractVersion( final String[] parts ) {
138-
return parts[1];
139-
}
140-
141-
/**
142-
* Creates a map representing a model from the given path parts, setting the existing field as specified.
143-
*
144-
* @param parts an array of path parts
145-
* @param modelElement an element of the aspect model
146-
* @param existing whether to set the existing field to true
147-
* @return a map containing the model information
148-
*/
149-
private Model createModel( final String[] parts, final boolean existing, final ModelElement modelElement ) {
150-
return new Model( parts[2], modelElement.urn(), existing );
151-
}
152-
153-
/**
154-
* Converts a version-to-models map into a list of sorted Version objects.
155-
*
156-
* @param versionMap a map where keys are versions and values are lists of Model objects
157-
* @return a list of Version objects sorted by semantic version
158-
*/
159-
private List<Version> convertAndSortVersionMap( final Map<String, List<Model>> versionMap ) {
160-
return versionMap.entrySet().stream().sorted( Map.Entry.comparingByKey( this::compareSemanticVersions ) )
161-
.map( entry -> new Version( entry.getKey(), sortModelsAlphabetically( entry.getValue() ) ) ).collect( Collectors.toList() );
162-
}
163-
164-
/**
165-
* Sorts a list of models alphabetically by their names.
166-
*
167-
* @param models the list of models to sort
168-
* @return a sorted list of models
169-
*/
170-
private List<Model> sortModelsAlphabetically( final List<Model> models ) {
171-
return models.stream().sorted( Comparator.comparing( Model::getModel ) ).collect( Collectors.toList() );
172-
}
173-
174-
/**
175-
* Compares two semantic version strings (e.g., "1.0.0" and "1.0.1").
176-
*
177-
* @param v1 the first version string
178-
* @param v2 the second version string
179-
* @return a negative number if v1 < v2, zero if v1 == v2, or a positive number if v1 > v2
180-
*/
181-
private int compareSemanticVersions( final String v1, final String v2 ) {
182-
final String[] parts1 = v1.split( "\\." );
183-
final String[] parts2 = v2.split( "\\." );
184-
for ( int i = 0; i < Math.max( parts1.length, parts2.length ); i++ ) {
185-
final int part1 = i < parts1.length ? Integer.parseInt( parts1[i] ) : 0;
186-
final int part2 = i < parts2.length ? Integer.parseInt( parts2[i] ) : 0;
187-
final int comparison = Integer.compare( part1, part2 );
188-
if ( comparison != 0 ) {
189-
return comparison;
190-
}
191-
}
192-
return 0;
52+
return aspectModelLoader.load( uriStream.map( File::new ).toList() ).files().stream()
53+
.flatMap( aspectModelFile -> aspectModelFile.aspects().stream().map( ModelElement.class::cast ).findFirst().or( () ->
54+
aspectModelFile.elements().stream().filter( modelElement -> !modelElement.isAnonymous() ).findAny() ).stream() )
55+
.map( modelElement -> new Model( modelElement.getSourceFile().filename().orElse( "unnamed file" ), modelElement.urn(), true ) )
56+
.collect( Collectors.groupingBy( model -> model.getAspectModelUrn().getNamespaceMainPart() ) )
57+
.entrySet().stream().sorted( Map.Entry.comparingByKey() )
58+
.map( modelsByNamespaceEntry -> Map.entry( modelsByNamespaceEntry.getKey(),
59+
modelsByNamespaceEntry.getValue().stream()
60+
.collect( Collectors.groupingBy( model -> model.getAspectModelUrn().getVersion() ) )
61+
.entrySet().stream().sorted( Map.Entry.comparingByKey() )
62+
.map( modelsByVersion -> new Version( modelsByVersion.getKey(),
63+
modelsByVersion.getValue().stream().sorted( Comparator.comparing( Model::getModel ) ).toList() ) )
64+
.toList() ) )
65+
.collect( Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue, ( v1, v2 ) -> {
66+
throw new RuntimeException( String.format( "Duplicate key for values %s and %s", v1, v2 ) );
67+
}, LinkedHashMap::new ) );
19368
}
19469
}

aspect-model-editor-service/src/main/java/org/eclipse/esmf/ame/services/utils/ModelUtils.java

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -192,38 +192,6 @@ public AspectModel get() {
192192
};
193193
}
194194

195-
/**
196-
* Resolves a file path from a given URN and base model path.
197-
* <p>
198-
* The URN is expected to follow the format:
199-
* <pre>urn:namespace:version#fileName</pre>
200-
* This method extracts the namespace, version, and file name from the URN
201-
* and constructs a path relative to the provided model path.
202-
*
203-
* @param urn the URN to be resolved, in the format `urn:namespace:version#fileName`
204-
* @param modelPath the base path where the resolved file path will be constructed
205-
* @return the resolved file path as a {@link Path}
206-
* @throws IllegalArgumentException if the URN format is invalid
207-
*/
208-
public static Path resolvePathFromUrn( final String urn, final Path modelPath ) {
209-
if ( urn == null || !urn.contains( "#" ) || !urn.contains( ":" ) ) {
210-
throw new IllegalArgumentException( "Invalid URN format. Expected format: urn:namespace:version#fileName" );
211-
}
212-
213-
final String[] parts = urn.split( "#", 2 );
214-
final String fileName = parts[1];
215-
final String[] nsAndVer = parts[0].split( ":" );
216-
217-
if ( nsAndVer.length < 3 ) {
218-
throw new IllegalArgumentException( "Invalid URN format. Namespace and version are missing." );
219-
}
220-
221-
final String version = nsAndVer[nsAndVer.length - 1];
222-
final String namespace = nsAndVer[nsAndVer.length - 2];
223-
224-
return modelPath.resolve( namespace ).resolve( version ).resolve( fileName + ".ttl" );
225-
}
226-
227195
public static AspectModel loadModelFromFile( final Path modelPath, final String filePath, final AspectModelLoader aspectModelLoader ) {
228196
final Path path = Paths.get( filePath ).normalize();
229197
final String[] pathParts = StreamSupport.stream( path.spliterator(), false ).map( Path::toString ).toArray( String[]::new );

0 commit comments

Comments
 (0)