From 3d9c9cf781539c2ccd10968da543da1f9d6aafa6 Mon Sep 17 00:00:00 2001 From: tarn Date: Wed, 1 Jul 2026 09:13:47 +0200 Subject: [PATCH 1/8] draft --- ... => MigrationArtifactsRESTController.java} | 70 +++++- .../MigrationContextRESTController.java | 22 +- .../context/SchemaMigrationContext.java | 3 + .../SemanticAssociationChange.java | 2 + .../SemanticAttributeChange.java | 3 + .../SemanticResourceChange.java | 3 + .../compare/TripleChangeAnalyser.java | 115 +++++---- .../SchemaMigrationService.java | 45 +++- .../SetMigrationContextUseCase.java | 12 +- .../GenerateMigrationReportUseCase.java | 24 ++ .../GenerateMigrationScriptUseCase.java | 2 +- .../MarkdownMigrationReportBuilder.java | 231 ++++++++++++++++++ .../artifacts/MigrationReportBuilder.java | 28 +++ .../MigrationScriptBuilder.java | 2 +- .../SparqlMigrationBuilder.java | 2 +- .../SparqlUpdateGenerator.java | 2 +- .../defaults/InheritanceChangeHandler.java | 11 +- .../renamings/RenameDetector.java | 3 + frontend/src/lib/sharedState.svelte.js | 1 + .../src/routes/migrate/steps/Export.svelte | 52 +++- .../routes/migrate/steps/SelectSchemas.svelte | 15 +- 21 files changed, 554 insertions(+), 94 deletions(-) rename backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/{MigrationScriptRESTController.java => MigrationArtifactsRESTController.java} (62%) create mode 100644 backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/GenerateMigrationReportUseCase.java rename backend/src/main/java/org/rdfarchitect/services/schemamigration/{scriptgeneration => artifacts}/GenerateMigrationScriptUseCase.java (93%) create mode 100644 backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MarkdownMigrationReportBuilder.java create mode 100644 backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MigrationReportBuilder.java rename backend/src/main/java/org/rdfarchitect/services/schemamigration/{scriptgeneration => artifacts}/MigrationScriptBuilder.java (92%) rename backend/src/main/java/org/rdfarchitect/services/schemamigration/{scriptgeneration => artifacts}/SparqlMigrationBuilder.java (99%) rename backend/src/main/java/org/rdfarchitect/services/schemamigration/{scriptgeneration => artifacts}/SparqlUpdateGenerator.java (99%) diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationScriptRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationArtifactsRESTController.java similarity index 62% rename from backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationScriptRESTController.java rename to backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationArtifactsRESTController.java index 83889dd5..bd2fbb1f 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationScriptRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationArtifactsRESTController.java @@ -21,12 +21,17 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.responses.ApiResponse; - +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import lombok.RequiredArgsConstructor; - import org.apache.jena.riot.RDFFormat; import org.rdfarchitect.context.MigrationSessionStore; -import org.rdfarchitect.services.schemamigration.scriptgeneration.GenerateMigrationScriptUseCase; +import org.rdfarchitect.services.schemamigration.artifacts.GenerateMigrationReportUseCase; +import org.rdfarchitect.services.schemamigration.artifacts.GenerateMigrationScriptUseCase; import org.rdfarchitect.services.shacl.SHACLExportUseCase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,25 +40,20 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - @RestController @RequiredArgsConstructor -public class MigrationScriptRESTController { +public class MigrationArtifactsRESTController { private static final Logger logger = - LoggerFactory.getLogger(MigrationScriptRESTController.class); + LoggerFactory.getLogger(MigrationArtifactsRESTController.class); private final GenerateMigrationScriptUseCase generateMigrationScriptUseCase; private final SHACLExportUseCase shaclExportUseCase; private final MigrationSessionStore migrationSessionStore; + private final GenerateMigrationReportUseCase generateMigrationReportUseCase; @Operation( summary = "generate migration script", @@ -102,4 +102,50 @@ public ResponseEntity generateMigrationScript( return response; } + + @Operation( + summary = "generate migration script", + description = + "Generates a migration script based on the previously computed migration actions and the shacl shapes of the new schema.", + tags = {"migration"}, + responses = { + @ApiResponse( + responseCode = "200", + content = @Content(mediaType = "application/zip")) + }) + @GetMapping("/api/migrations/report") + public ResponseEntity generateMigrationReport( + @Parameter(description = "The name/url of the inquirer.") + @RequestHeader(value = "origin", required = false, defaultValue = "unknown") + String originURL, + @Parameter(description = "Type of the migration report to generate. Can be either SUMMARY or DETAILED.") + @RequestParam(value = "type", defaultValue = "SUMMARY") MigrationReportType reportType) + throws IOException { + logger.info("Received GET request: \"/api/migrations/report\" from \"{}\".", originURL); + + var report = switch (reportType) { + case SUMMARY -> generateMigrationReportUseCase.generateSummaryMigrationReport(); + case DETAILED -> generateMigrationReportUseCase.generateDetailedMigrationReport(); + }; + + var body = report.getBytes(StandardCharsets.UTF_8); + var headers = new HttpHeaders(); + headers.setAccessControlExposeHeaders(List.of("Content-disposition")); + var response = ResponseEntity.ok() + .headers(headers) + .header(HttpHeaders.CONTENT_DISPOSITION, "migration_report.md") + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(body); + + logger.info( + "Sending response to GET request: \"/api/migrations/report\" from \"{}\".", + originURL); + + return response; + } + + public enum MigrationReportType { + SUMMARY, + DETAILED + } } diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationContextRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationContextRESTController.java index 59189772..f2633288 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationContextRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationContextRESTController.java @@ -89,12 +89,18 @@ public void computeMigrationContext( description = "URI of graph B. Required together with dataset_b when not uploading graph B as a file.") @RequestParam(required = false) - String graphB) { + String graphB, + @Parameter( + description = + "Whether to ignore prefixes in the comparison and rename detection. Defaults to false.") + @RequestParam(required = false, defaultValue = "false") + boolean ignorePrefixes + ){ logger.info("Received POST request: \"/api/migrations/context\" from \"{}\".", originURL); // file to file if (fileA != null && fileB != null) { - setMigrationContextUseCase.setMigrationContext(fileA, fileB); + setMigrationContextUseCase.setMigrationContext(fileA, fileB, ignorePrefixes); } // stored to stored else if (datasetA != null && datasetB != null && graphA != null && graphB != null) { @@ -102,19 +108,25 @@ else if (datasetA != null && datasetB != null && graphA != null && graphB != nul var extendedGraphURIB = expandURIUseCase.expandUri(datasetB, graphB); setMigrationContextUseCase.setMigrationContext( new GraphIdentifier(datasetA, extendedGraphURIA), - new GraphIdentifier(datasetB, extendedGraphURIB)); + new GraphIdentifier(datasetB, extendedGraphURIB), + ignorePrefixes); } // file to stored else if (fileA != null && datasetB != null && graphB != null) { var extendedGraphURIB = expandURIUseCase.expandUri(datasetB, graphB); setMigrationContextUseCase.setMigrationContext( - fileA, new GraphIdentifier(datasetB, extendedGraphURIB)); + fileA, + new GraphIdentifier(datasetB, extendedGraphURIB), + ignorePrefixes + ); } // stored to file else if (datasetA != null && graphA != null && fileA != null) { var extendedGraphURIA = expandURIUseCase.expandUri(datasetA, graphA); setMigrationContextUseCase.setMigrationContext( - new GraphIdentifier(datasetA, extendedGraphURIA), fileA); + new GraphIdentifier(datasetA, extendedGraphURIA), + fileA, + ignorePrefixes); } else { logger.warn( "Invalid request to POST \"/api/migrations/context\" from \"{}\". Missing required parameters.", diff --git a/backend/src/main/java/org/rdfarchitect/context/SchemaMigrationContext.java b/backend/src/main/java/org/rdfarchitect/context/SchemaMigrationContext.java index 6420f3d0..59ee1440 100644 --- a/backend/src/main/java/org/rdfarchitect/context/SchemaMigrationContext.java +++ b/backend/src/main/java/org/rdfarchitect/context/SchemaMigrationContext.java @@ -29,6 +29,9 @@ @Data public class SchemaMigrationContext { + // whether the comparison ignores prefixes in comparison and rename detection + private boolean ignorePrefixes; + // original graph of the schema private Graph originalSchema; diff --git a/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticAssociationChange.java b/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticAssociationChange.java index e24fb781..252627ec 100644 --- a/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticAssociationChange.java +++ b/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticAssociationChange.java @@ -42,6 +42,8 @@ public final class SemanticAssociationChange extends SemanticResourceChange { private boolean associationUsed; + private String inheritedFrom; + public SemanticAssociationChange(TripleResourceChange tripleChange) { super(tripleChange); } diff --git a/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticAttributeChange.java b/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticAttributeChange.java index aea4f984..4335e9bb 100644 --- a/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticAttributeChange.java +++ b/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticAttributeChange.java @@ -25,6 +25,7 @@ import org.apache.jena.rdf.model.Resource; import org.rdfarchitect.models.changes.triplechanges.TripleResourceChange; +import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; import java.util.ArrayList; import java.util.List; @@ -51,6 +52,8 @@ public final class SemanticAttributeChange extends SemanticResourceChange { private boolean forceDefaultValue = false; + private String inheritedFrom; + @Builder.Default private List allowedValues = new ArrayList<>(); public SemanticAttributeChange(TripleResourceChange tripleChange) { diff --git a/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticResourceChange.java b/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticResourceChange.java index efd8f9d5..8f3f7c8b 100644 --- a/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticResourceChange.java +++ b/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticResourceChange.java @@ -60,6 +60,8 @@ public sealed class SemanticResourceChange protected String label; + protected String comment; + // only set in case of a rename protected String oldIRI; @@ -84,6 +86,7 @@ public SemanticResourceChange(SemanticResourceChange other) { this.iri = other.getIri(); this.semanticResourceChangeType = other.getSemanticResourceChangeType(); this.changes = other.getChanges(); + this.comment = other.getComment(); } public SemanticResourceChange(Resource resource, SemanticResourceChangeType changeType) { diff --git a/backend/src/main/java/org/rdfarchitect/services/compare/TripleChangeAnalyser.java b/backend/src/main/java/org/rdfarchitect/services/compare/TripleChangeAnalyser.java index 4ed0ce21..878b00bb 100644 --- a/backend/src/main/java/org/rdfarchitect/services/compare/TripleChangeAnalyser.java +++ b/backend/src/main/java/org/rdfarchitect/services/compare/TripleChangeAnalyser.java @@ -37,6 +37,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -207,58 +208,22 @@ private TripleClassChange compareClass( * @return A map of property URIs to TriplePropertyChange objects representing the differences * in properties. */ - private List compareResource( - Graph originalGraph, Graph updatedGraph, String uri) { + private List compareResource(Graph originalGraph, Graph updatedGraph, String uri) { var originalTriples = originalGraph.find(NodeFactory.createURI(uri), Node.ANY, Node.ANY).toList(); var updatedTriples = updatedGraph.find(NodeFactory.createURI(uri), Node.ANY, Node.ANY).toList(); var propertyChanges = new ArrayList(); - var originalValues = - originalTriples.stream() - .filter(triple -> !triple.getPredicate().equals(CIMS.stereotype.asNode())) - .collect( - Collectors.toMap( - Triple::getPredicate, Triple::getObject, (a, b) -> a)); - - var updatedValues = - updatedTriples.stream() - .filter(triple -> !triple.getPredicate().equals(CIMS.stereotype.asNode())) - .collect( - Collectors.toMap( - Triple::getPredicate, Triple::getObject, (a, b) -> a)); - - Set allPredicates = new HashSet<>(); - allPredicates.addAll(originalValues.keySet()); - allPredicates.addAll(updatedValues.keySet()); + propertyChanges.addAll(compareSimplePredicates(originalTriples, updatedTriples)); + propertyChanges.addAll(compareStereotypes(originalTriples, updatedTriples)); - for (Node predicate : allPredicates) { - // ignore uuid predicates, stereotypes are handled separately - if (predicate.equals(RDFA.uuid.asNode()) - || predicate.equals(CIMS.stereotype.asNode())) { - continue; - } + return propertyChanges; + } - var from = originalValues.get(predicate); - var to = updatedValues.get(predicate); - var valuesDiffer = !Objects.equals(from, to); - if (RDFS.comment.asNode().equals(predicate)) { - var normalizedFrom = normalizeCommentValue(from); - var normalizedTo = normalizeCommentValue(to); - valuesDiffer = !Objects.equals(normalizedFrom, normalizedTo); - } + private List compareStereotypes(List originalTriples, List updatedTriples) { + var changes = new ArrayList(); - if (valuesDiffer) { - var change = new TriplePropertyChange(); - change.setPredicate(predicate.toString()); - change.setFrom(from != null ? from.toString() : null); - change.setTo(to != null ? to.toString() : null); - propertyChanges.add(change); - } - } - - // handle stereotypes separately var originalStereotypes = originalTriples.stream() .filter(triple -> triple.getPredicate().equals(CIMS.stereotype.asNode())) @@ -275,7 +240,7 @@ private List compareResource( var change = new TriplePropertyChange(); change.setPredicate(CIMS.stereotype.toString()); change.setFrom(originalStereotype); - propertyChanges.add(change); + changes.add(change); } else { updatedStereotypes.remove(originalStereotype); } @@ -285,10 +250,68 @@ private List compareResource( var change = new TriplePropertyChange(); change.setPredicate(CIMS.stereotype.toString()); change.setTo(updatedStereotype); - propertyChanges.add(change); + changes.add(change); } - return propertyChanges; + return changes; + } + + private List compareSimplePredicates(List originalTriples, List updatedTriples) { + var changes = new ArrayList(); + + var originalValues = + originalTriples.stream() + .filter(triple -> !triple.getPredicate().equals(CIMS.stereotype.asNode())) + .collect( + Collectors.toMap( + Triple::getPredicate, Triple::getObject, (a, _) -> a)); + + var updatedValues = + updatedTriples.stream() + .filter(triple -> !triple.getPredicate().equals(CIMS.stereotype.asNode())) + .collect( + Collectors.toMap( + Triple::getPredicate, Triple::getObject, (a, _) -> a)); + + Set allPredicates = new HashSet<>(); + allPredicates.addAll(originalValues.keySet()); + allPredicates.addAll(updatedValues.keySet()); + + for (Node predicate : allPredicates) { + var change = comparePredicate(predicate, originalValues, updatedValues); + if (change != null) { + changes.add(change); + } + } + + return changes; + } + + private static TriplePropertyChange comparePredicate(Node predicate, Map originalValues, Map updatedValues) { + // ignore uuid predicates, stereotypes are handled separately + if (predicate.equals(RDFA.uuid.asNode())) { + return null; + } + + var from = originalValues.get(predicate); + var to = updatedValues.get(predicate); + var valuesDiffer = !Objects.equals(from, to); + + if (RDFS.comment.asNode().equals(predicate)) { + var normalizedFrom = normalizeCommentValue(from); + var normalizedTo = normalizeCommentValue(to); + valuesDiffer = !Objects.equals(normalizedFrom, normalizedTo); + } + + if (valuesDiffer) { + var change = new TriplePropertyChange(); + change.setPredicate(predicate.toString()); + change.setFrom(from != null ? from.toString() : null); + change.setTo(to != null ? to.toString() : null); + return change; + } else { + return null; + } } private String normalizeCommentValue(Node value) { diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/SchemaMigrationService.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/SchemaMigrationService.java index 0efe0004..d47ecdef 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/SchemaMigrationService.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/SchemaMigrationService.java @@ -38,6 +38,8 @@ import org.rdfarchitect.rdf.graph.GraphUtils; import org.rdfarchitect.rdf.graph.source.builder.implementations.GraphFileSourceBuilderImpl; import org.rdfarchitect.services.compare.TripleChangeAnalyser; +import org.rdfarchitect.services.schemamigration.artifacts.GenerateMigrationReportUseCase; +import org.rdfarchitect.services.schemamigration.artifacts.MigrationReportBuilder; import org.rdfarchitect.services.schemamigration.defaults.DefaultValueAssigner; import org.rdfarchitect.services.schemamigration.defaults.GetDefaultValueViewsUseCase; import org.rdfarchitect.services.schemamigration.defaults.InheritanceChangeHandler; @@ -48,8 +50,8 @@ import org.rdfarchitect.services.schemamigration.renamings.GetPropertyRenamingsUseCase; import org.rdfarchitect.services.schemamigration.renamings.RenameDetector; import org.rdfarchitect.services.schemamigration.renamings.RenameObjectBuilder; -import org.rdfarchitect.services.schemamigration.scriptgeneration.GenerateMigrationScriptUseCase; -import org.rdfarchitect.services.schemamigration.scriptgeneration.MigrationScriptBuilder; +import org.rdfarchitect.services.schemamigration.artifacts.GenerateMigrationScriptUseCase; +import org.rdfarchitect.services.schemamigration.artifacts.MigrationScriptBuilder; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -63,21 +65,23 @@ public class SchemaMigrationService implements SetMigrationContextUseCase, GetClassRenamingsUseCase, GetPropertyRenamingsUseCase, - GenerateMigrationScriptUseCase, + GenerateMigrationScriptUseCase, ClassRenamingsUseCase, ConfirmPropertyRenamingsUseCase, ClearMigrationContextUseCase, GetDefaultValueViewsUseCase, - SubmitDefaultValuesUseCase { + SubmitDefaultValuesUseCase, + GenerateMigrationReportUseCase { private final MigrationSessionStore migrationSessionStore; private final DatabasePort databasePort; private final MigrationScriptBuilder migrationScriptBuilder; + private final MigrationReportBuilder migrationReportBuilder; private static final String GRAPH_URI = "http://example.org/graph"; @Override - public void setMigrationContext(MultipartFile originalSchema, GraphIdentifier updatedSchema) { + public void setMigrationContext(MultipartFile originalSchema, GraphIdentifier updatedSchema, boolean ignorePrefixes) { var originalGraph = new GraphFileSourceBuilderImpl() .setFile(originalSchema) @@ -91,11 +95,11 @@ public void setMigrationContext(MultipartFile originalSchema, GraphIdentifier up updatedGraph = GraphUtils.deepCopy(updatedCtx.getRdfGraph()); } - initContext(originalGraph, updatedGraph); + initContext(originalGraph, updatedGraph, ignorePrefixes); } @Override - public void setMigrationContext(GraphIdentifier originalSchema, GraphIdentifier updatedSchema) { + public void setMigrationContext(GraphIdentifier originalSchema, GraphIdentifier updatedSchema, boolean ignorePrefixes) { Graph originalGraph; try (var originalCtx = databasePort.getGraphWithContext(originalSchema).begin(ReadWrite.READ)) { @@ -108,11 +112,11 @@ public void setMigrationContext(GraphIdentifier originalSchema, GraphIdentifier updatedGraph = GraphUtils.deepCopy(updatedCtx.getRdfGraph()); } - initContext(originalGraph, updatedGraph); + initContext(originalGraph, updatedGraph, ignorePrefixes); } @Override - public void setMigrationContext(GraphIdentifier originalSchema, MultipartFile updatedSchema) { + public void setMigrationContext(GraphIdentifier originalSchema, MultipartFile updatedSchema, boolean ignorePrefixes) { Graph originalGraph; try (var originalCtx = databasePort.getGraphWithContext(originalSchema).begin(ReadWrite.READ)) { @@ -126,11 +130,11 @@ public void setMigrationContext(GraphIdentifier originalSchema, MultipartFile up .build() .graph(); - initContext(originalGraph, updatedGraph); + initContext(originalGraph, updatedGraph, ignorePrefixes); } @Override - public void setMigrationContext(MultipartFile originalSchema, MultipartFile updatedSchema) { + public void setMigrationContext(MultipartFile originalSchema, MultipartFile updatedSchema, boolean ignorePrefixes) { var originalGraph = new GraphFileSourceBuilderImpl() .setFile(originalSchema) @@ -144,14 +148,15 @@ public void setMigrationContext(MultipartFile originalSchema, MultipartFile upda .build() .graph(); - initContext(originalGraph, updatedGraph); + initContext(originalGraph, updatedGraph, ignorePrefixes); } - private void initContext(Graph originalGraph, Graph updatedGraph) { + private void initContext(Graph originalGraph, Graph updatedGraph, boolean ignorePrefixes) { var context = migrationSessionStore.getContext(); context.clear(); context.setOriginalSchema(originalGraph); context.setUpdatedSchema(updatedGraph); + context.setIgnorePrefixes(ignorePrefixes); var tripleChanges = TripleChangeAnalyser.compareGraphsDisregardingPackages(originalGraph, updatedGraph); context.setTripleDiff(tripleChanges); @@ -355,6 +360,20 @@ public String generateMigrationScript() { return migrationScriptBuilder.generateMigrationScript(changes); } + @Override + public String generateDetailedMigrationReport() { + var graph = migrationSessionStore.getContext().getOriginalSchema(); + var changes = migrationSessionStore.getContext().getDiffAfterDefaultValueConfirm(); + return migrationReportBuilder.generateDetailedMigrationReport(changes, graph); + } + + @Override + public String generateSummaryMigrationReport() { + var graph = migrationSessionStore.getContext().getOriginalSchema(); + var changes = migrationSessionStore.getContext().getDiffAfterDefaultValueConfirm(); + return migrationReportBuilder.generateSummaryMigrationReport(changes, graph); + } + @Override public void clearMigrationContext() { migrationSessionStore.clearContext(); diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/SetMigrationContextUseCase.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/SetMigrationContextUseCase.java index a44f37f3..2a8865cb 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/SetMigrationContextUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/SetMigrationContextUseCase.java @@ -29,8 +29,9 @@ public interface SetMigrationContextUseCase { * * @param originalSchema the graph identifier of the edited schema loaded in memory * @param updatesSchema the uploaded old schema + * @param ignorePrefixes whether to ignore prefixes in comparison and rename detection */ - void setMigrationContext(MultipartFile originalSchema, GraphIdentifier updatesSchema); + void setMigrationContext(MultipartFile originalSchema, GraphIdentifier updatesSchema, boolean ignorePrefixes); /** * Sets the migration context for the given graph identifier of an edited schema loaded in @@ -38,22 +39,25 @@ public interface SetMigrationContextUseCase { * * @param originalSchema the graph identifier of the edited schema loaded in memory * @param updatedSchema the graph identifier of the old schema loaded in memory + * @param ignorePrefixes whether to ignore prefixes in comparison and rename detection */ - void setMigrationContext(GraphIdentifier originalSchema, GraphIdentifier updatedSchema); + void setMigrationContext(GraphIdentifier originalSchema, GraphIdentifier updatedSchema, boolean ignorePrefixes); /** * Sets the migration context for a stored original schema and an uploaded updated schema. * * @param originalSchema the graph identifier of the original schema loaded in memory * @param updatedSchema the uploaded updated schema + * @param ignorePrefixes whether to ignore prefixes in comparison and rename detection */ - void setMigrationContext(GraphIdentifier originalSchema, MultipartFile updatedSchema); + void setMigrationContext(GraphIdentifier originalSchema, MultipartFile updatedSchema, boolean ignorePrefixes); /** * Sets the migration context for the two uploaded schema files. * * @param originalSchema the graph identifier of the edited schema loaded in memory * @param updatedSchema the graph identifier of the old schema loaded in memory + * @param ignorePrefixes whether to ignore prefixes in comparison and rename detection */ - void setMigrationContext(MultipartFile originalSchema, MultipartFile updatedSchema); + void setMigrationContext(MultipartFile originalSchema, MultipartFile updatedSchema, boolean ignorePrefixes); } diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/GenerateMigrationReportUseCase.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/GenerateMigrationReportUseCase.java new file mode 100644 index 00000000..bfa8f780 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/GenerateMigrationReportUseCase.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.services.schemamigration.artifacts; + +public interface GenerateMigrationReportUseCase { + String generateDetailedMigrationReport(); + + String generateSummaryMigrationReport(); +} diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/scriptgeneration/GenerateMigrationScriptUseCase.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/GenerateMigrationScriptUseCase.java similarity index 93% rename from backend/src/main/java/org/rdfarchitect/services/schemamigration/scriptgeneration/GenerateMigrationScriptUseCase.java rename to backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/GenerateMigrationScriptUseCase.java index e7eae31a..1bf25bfa 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/scriptgeneration/GenerateMigrationScriptUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/GenerateMigrationScriptUseCase.java @@ -15,7 +15,7 @@ * */ -package org.rdfarchitect.services.schemamigration.scriptgeneration; +package org.rdfarchitect.services.schemamigration.artifacts; /** Interface for generating the migration actions when migrating between two schemas. */ public interface GenerateMigrationScriptUseCase { diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MarkdownMigrationReportBuilder.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MarkdownMigrationReportBuilder.java new file mode 100644 index 00000000..827ec401 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MarkdownMigrationReportBuilder.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.services.schemamigration.artifacts; + +import org.apache.jena.graph.Graph; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import org.rdfarchitect.models.changes.semanticchanges.SemanticClassChange; +import org.rdfarchitect.models.changes.semanticchanges.SemanticFieldChange; +import org.rdfarchitect.models.changes.semanticchanges.SemanticResourceChange; +import org.rdfarchitect.models.changes.semanticchanges.SemanticResourceChangeType; +import org.rdfarchitect.models.cim.rdf.resources.CIMS; +import org.rdfarchitect.models.cim.rdf.resources.CIMStereotypes; +import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Service +public class MarkdownMigrationReportBuilder implements MigrationReportBuilder { + + @Override + public String generateDetailedMigrationReport(List classChanges, Graph originalGraph) { + var sb = new StringBuilder(); + sb.append("# Migration Report — Detailed View\n\n"); + sb.append("All affected concrete classes are listed alphabetically. "); + sb.append("Inherited changes are shown under each affected subclass.\n\n"); + + var model = ModelFactory.createModelForGraph(originalGraph); + var concreteClassIRIs = model.listResourcesWithProperty(RDF.type, RDFS.Class) + .filterKeep(r -> r.hasProperty(CIMS.stereotype, CIMStereotypes.concrete)) + .filterDrop(r -> r.hasProperty(CIMS.stereotype, CIMStereotypes.enumeration)) + .filterDrop(r -> r.hasProperty(CIMS.stereotype, CIMStereotypes.cimDataType)) + .mapWith(Resource::getURI) + .toList(); + + var concreteChanges = classChanges.stream() + .filter(c -> concreteClassIRIs.contains(c.getIri())) + .sorted(Comparator.comparing(SemanticResourceChange::getLabel)) + .toList(); + + for (var classChange : concreteChanges) { + appendClassSection(sb, classChange); + } + + return sb.toString(); + } + + private void appendClassSection(StringBuilder sb, SemanticClassChange classChange) { + sb.append("## ").append(formatChangeType(classChange.getSemanticResourceChangeType())) + .append(" `").append(classChange.getLabel()).append("`\n\n"); + sb.append("**IRI:** `").append(classChange.getIri()).append("`\n\n"); + + if (classChange.getOldIRI() != null) { + sb.append("**Previous IRI:** `").append(classChange.getOldIRI()).append("`\n\n"); + } + + appendFieldChangesTable(sb, classChange.getChanges()); + appendPropertySection(sb, "Attributes", classChange.getAttributes()); + appendPropertySection(sb, "Associations", classChange.getAssociations()); + appendPropertySection(sb, "Enum Entries", classChange.getEnumEntries()); + sb.append("\n---\n\n"); + } + + @Override + public String generateSummaryMigrationReport(List classChanges, Graph originalGraph) { + var sb = new StringBuilder(); + sb.append("# Migration Report — Summary View\n\n"); + sb.append("Classes with direct changes. For parent classes, affected concrete subclasses are listed.\n\n"); + + // Classes that have at least one direct (non-inherited) change + var directlyChanged = classChanges.stream() + .filter(this::hasDirectChange) + .sorted(Comparator.comparing(SemanticResourceChange::getLabel)) + .toList(); + + for (var classChange : directlyChanged) { + sb.append("## ").append(formatChangeType(classChange.getSemanticResourceChangeType())) + .append(" `").append(classChange.getLabel()).append("`\n\n"); + sb.append("**IRI:** `").append(classChange.getIri()).append("`\n\n"); + + // Show only direct changes + var directFieldChanges = classChange.getChanges(); // class-level are always direct + appendFieldChangesTable(sb, directFieldChanges); + + appendDirectProperties(sb, "Attributes", classChange.getAttributes()); + appendDirectProperties(sb, "Associations", classChange.getAssociations()); + appendDirectProperties(sb, "Enum Entries", classChange.getEnumEntries()); + + // List affected concrete subclasses + var affectedSubclasses = findAffectedSubclasses(classChange, classChanges); + if (!affectedSubclasses.isEmpty()) { + sb.append("\n**Affected concrete classes:**\n\n"); + for (var sub : affectedSubclasses) { + sb.append("- `").append(sub.getLabel()).append("`\n"); + } + sb.append("\n"); + } + + sb.append("---\n\n"); + } + + return sb.toString(); + } + + private boolean hasDirectChange(SemanticClassChange c) { + if (!c.getChanges().isEmpty()) return true; + var allProperties = new ArrayList(c.getAssociations()); + allProperties.addAll(c.getAttributes()); + allProperties.addAll(c.getEnumEntries()); + + if (allProperties.isEmpty()) return false; + return allProperties.stream().anyMatch(p -> + p.getSemanticResourceChangeType() != SemanticResourceChangeType.ADDED_FROM_INHERITANCE + && p.getSemanticResourceChangeType() != SemanticResourceChangeType.DELETED_FROM_INHERITANCE); + } + + private List findAffectedSubclasses( + SemanticClassChange parentChange, + List allChanges) { + var directPropertyLabels = collectDirectPropertyLabels(parentChange); + + return allChanges.stream() + .filter(c -> !c.getIri().equals(parentChange.getIri())) + .filter(c -> hasMatchingInheritedProperty(c, directPropertyLabels)) + .sorted(Comparator.comparing(SemanticResourceChange::getLabel)) + .toList(); + } + + private Set collectDirectPropertyLabels(SemanticClassChange classChange) { + var labels = new HashSet(); + addDirectLabels(labels, classChange.getAttributes()); + addDirectLabels(labels, classChange.getAssociations()); + addDirectLabels(labels, classChange.getEnumEntries()); + return labels; + } + + private void addDirectLabels(Set labels, List properties) { + if (properties == null) return; + properties.stream() + .filter(p -> p.getSemanticResourceChangeType() != SemanticResourceChangeType.ADDED_FROM_INHERITANCE + && p.getSemanticResourceChangeType() != SemanticResourceChangeType.DELETED_FROM_INHERITANCE) + .forEach(p -> labels.add(p.getLabel())); + } + + private boolean hasMatchingInheritedProperty( + SemanticClassChange classChange, Set parentPropertyLabels) { + return matchesAny(classChange.getAttributes(), parentPropertyLabels) + || matchesAny(classChange.getAssociations(), parentPropertyLabels) + || matchesAny(classChange.getEnumEntries(), parentPropertyLabels); + } + + private boolean matchesAny(List properties, Set labels) { + if (properties == null) return false; + return properties.stream() + .filter(p -> p.getSemanticResourceChangeType() == SemanticResourceChangeType.ADDED_FROM_INHERITANCE + || p.getSemanticResourceChangeType() == SemanticResourceChangeType.DELETED_FROM_INHERITANCE) + .anyMatch(p -> labels.contains(p.getLabel())); + } + + private void appendFieldChangesTable(StringBuilder sb, List changes) { + if (changes == null || changes.isEmpty()) return; + sb.append("| Change | From | To |\n"); + sb.append("|--------|------|----|\n"); + for (var c : changes) { + sb.append("| ").append(c.getSemanticFieldChangeType()) + .append(" | ").append(c.getFrom() != null ? "`" + c.getFrom() + "`" : "—") + .append(" | ").append(c.getTo() != null ? "`" + c.getTo() + "`" : "—") + .append(" |\n"); + } + sb.append("\n"); + } + + private void appendPropertySection( + StringBuilder sb, String title, List properties) { + if (properties == null || properties.isEmpty()) return; + sb.append("### ").append(title).append("\n\n"); + for (var prop : properties) { + sb.append("- ").append(formatChangeType(prop.getSemanticResourceChangeType())) + .append(" `").append(prop.getLabel()).append("`"); + if (!prop.getChanges().isEmpty()) { + sb.append("\n"); + appendFieldChangesTable(sb, prop.getChanges()); + } else { + sb.append("\n"); + } + } + sb.append("\n"); + } + + private void appendDirectProperties( + StringBuilder sb, String title, List properties) { + if (properties == null) return; + var direct = properties.stream() + .filter(p -> p.getSemanticResourceChangeType() != SemanticResourceChangeType.ADDED_FROM_INHERITANCE + && p.getSemanticResourceChangeType() != SemanticResourceChangeType.DELETED_FROM_INHERITANCE) + .toList(); + if (direct.isEmpty()) return; + appendPropertySection(sb, title, direct); + } + + private String formatChangeType(SemanticResourceChangeType type) { + return switch (type) { + case ADD -> "[Added]"; + case DELETE -> "[Deleted]"; + case CHANGE -> "[Changed]"; + case RENAME -> "[Renamed]"; + case ADDED_FROM_INHERITANCE -> "[Added via inheritance]"; + case DELETED_FROM_INHERITANCE -> "[Deleted via inheritance]"; + }; + } +} diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MigrationReportBuilder.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MigrationReportBuilder.java new file mode 100644 index 00000000..bdc86a5e --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MigrationReportBuilder.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rdfarchitect.services.schemamigration.artifacts; + +import org.apache.jena.graph.Graph; +import org.rdfarchitect.models.changes.semanticchanges.SemanticClassChange; +import java.util.List; + +public interface MigrationReportBuilder { + String generateDetailedMigrationReport(List classChanges, Graph originalGraph); + + String generateSummaryMigrationReport(List classChanges, Graph originalGraph); +} diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/scriptgeneration/MigrationScriptBuilder.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MigrationScriptBuilder.java similarity index 92% rename from backend/src/main/java/org/rdfarchitect/services/schemamigration/scriptgeneration/MigrationScriptBuilder.java rename to backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MigrationScriptBuilder.java index fa7350c6..7fd638fa 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/scriptgeneration/MigrationScriptBuilder.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MigrationScriptBuilder.java @@ -15,7 +15,7 @@ * */ -package org.rdfarchitect.services.schemamigration.scriptgeneration; +package org.rdfarchitect.services.schemamigration.artifacts; import org.rdfarchitect.models.changes.semanticchanges.SemanticClassChange; diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/scriptgeneration/SparqlMigrationBuilder.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/SparqlMigrationBuilder.java similarity index 99% rename from backend/src/main/java/org/rdfarchitect/services/schemamigration/scriptgeneration/SparqlMigrationBuilder.java rename to backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/SparqlMigrationBuilder.java index bbd46748..b98a74b3 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/scriptgeneration/SparqlMigrationBuilder.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/SparqlMigrationBuilder.java @@ -15,7 +15,7 @@ * */ -package org.rdfarchitect.services.schemamigration.scriptgeneration; +package org.rdfarchitect.services.schemamigration.artifacts; import lombok.RequiredArgsConstructor; diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/scriptgeneration/SparqlUpdateGenerator.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/SparqlUpdateGenerator.java similarity index 99% rename from backend/src/main/java/org/rdfarchitect/services/schemamigration/scriptgeneration/SparqlUpdateGenerator.java rename to backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/SparqlUpdateGenerator.java index bca9e04f..c398be5b 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/scriptgeneration/SparqlUpdateGenerator.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/SparqlUpdateGenerator.java @@ -15,7 +15,7 @@ * */ -package org.rdfarchitect.services.schemamigration.scriptgeneration; +package org.rdfarchitect.services.schemamigration.artifacts; import lombok.RequiredArgsConstructor; diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/defaults/InheritanceChangeHandler.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/defaults/InheritanceChangeHandler.java index bcddb55f..146e5e1f 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/defaults/InheritanceChangeHandler.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/defaults/InheritanceChangeHandler.java @@ -21,6 +21,7 @@ import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDFS; import org.rdfarchitect.models.changes.RenameCandidate; import org.rdfarchitect.models.changes.semanticchanges.SemanticAssociationChange; import org.rdfarchitect.models.changes.semanticchanges.SemanticAttributeChange; @@ -241,13 +242,19 @@ private void addNewInheritedProperties( property, SemanticResourceChangeType.ADDED_FROM_INHERITANCE); propertyChange.setLabel(new URI(property.getURI()).getSuffix()); if (CIMPropertyUtils.isAttribute(property)) { + var domain = property.getProperty(RDFS.domain).getResource().getProperty(RDFS.label).getResource().getURI(); + var attributeChange = new SemanticAttributeChange(propertyChange); + attributeChange.setInheritedFrom(domain); derivingClassChange .getAttributes() - .add(new SemanticAttributeChange(propertyChange)); + .add(attributeChange); } else if (CIMPropertyUtils.isAssociation(property)) { + var domain = property.getProperty(RDFS.domain).getResource().getProperty(RDFS.label).getResource().getURI(); + var associationChange = new SemanticAssociationChange(propertyChange); + associationChange.setInheritedFrom(domain); derivingClassChange .getAssociations() - .add(new SemanticAssociationChange(propertyChange)); + .add(associationChange); } } } diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/renamings/RenameDetector.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/renamings/RenameDetector.java index e4bbe6a3..e5a5cb0f 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/renamings/RenameDetector.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/renamings/RenameDetector.java @@ -84,6 +84,9 @@ private List> matchBySimil T bestMatch = null; for (T newItem : unmatchedAdded) { + if (newItem.getLabel().equals(deletedItem.getLabel())) { + + } double score = SimilarityCalculator.calculateSimilarity(newItem, deletedItem); if (score > bestScore) { bestScore = score; diff --git a/frontend/src/lib/sharedState.svelte.js b/frontend/src/lib/sharedState.svelte.js index a9bafb40..a41d6562 100644 --- a/frontend/src/lib/sharedState.svelte.js +++ b/frontend/src/lib/sharedState.svelte.js @@ -255,4 +255,5 @@ export const migrationState = writable({ fileB: null, cgmesVersionA: null, cgmesVersionB: null, + ignorePrefixes: false, }); diff --git a/frontend/src/routes/migrate/steps/Export.svelte b/frontend/src/routes/migrate/steps/Export.svelte index 47239f94..734fba95 100644 --- a/frontend/src/routes/migrate/steps/Export.svelte +++ b/frontend/src/routes/migrate/steps/Export.svelte @@ -23,6 +23,8 @@ import { goto } from "$app/navigation"; + let reportType = $state("SUMMARY"); + export async function onNext() { await goto("/mainpage"); } @@ -40,6 +42,19 @@ } } + async function generateMigrationReport() { + try { + const response = await fetchReport(); + const suggestedFilename = response.headers.get( + "content-disposition", + ); + const blob = await response.blob(); + saveFile(blob, suggestedFilename, sparqlMediaType); + } catch (e) { + console.error("Failed to generate report:", e); + } + } + async function fetchScript() { return fetch(PUBLIC_BACKEND_URL + "/migrations/export", { method: "GET", @@ -47,9 +62,23 @@ credentials: "include", }); } + + async function fetchReport() { + const params = new URLSearchParams({ reportType: reportType }) + return fetch(PUBLIC_BACKEND_URL + `/migrations/report?${params}`, { + method: "GET", + headers: { "Content-Type": "application/json" }, + credentials: "include", + }) + }
+ + In this step you can choose which artifacts you want to generate for the migration. You can generate a migration + package containing the SPARQL updates for automatically migrating your data as well as SHACL shapes for validating your data against the new schema. + You can also generate a markdown file based on the semantic changes configured during this migration process. + Please note that the script generation might not be able to handle all edge cases yet, one such case being multiplicity changes on @@ -61,17 +90,26 @@
-
-

Migration Script

-

- The migration script contains SPARQL UPDATE queries that will - transform your data from the source schema to the target schema - based on the mappings and defaults you've configured. -

+
+ +
Generate and download script + + Generate and download report +
diff --git a/frontend/src/routes/migrate/steps/SelectSchemas.svelte b/frontend/src/routes/migrate/steps/SelectSchemas.svelte index ea5aae21..c6d4b7f2 100644 --- a/frontend/src/routes/migrate/steps/SelectSchemas.svelte +++ b/frontend/src/routes/migrate/steps/SelectSchemas.svelte @@ -29,6 +29,8 @@ import { toastStore } from "$lib/eventhandling/toastStore.svelte.js"; import { CGMESVersion } from "$lib/models/cgmes-constants.js"; import { migrationState } from "$lib/sharedState.svelte.js"; + import { Checkbox } from "$lib/components/bitsui/checkbox/index.js"; + import CheckBoxEditControl from "$lib/components/CheckBoxEditControl.svelte"; let { disableNext = $bindable() } = $props(); @@ -40,6 +42,7 @@ }); let compareMode = $state(CompareMode.FILE_TO_STORED); + let ignorePrefixes = $state(false); let cgmesVersionA = $state(CGMESVersion.V3_0); let cgmesVersionB = $state(CGMESVersion.V3_0); @@ -184,6 +187,7 @@ graphB, fileA, fileB, + ignorePrefixes, }); } else { toastStore.error( @@ -213,7 +217,7 @@
-
+
@@ -225,6 +229,15 @@ getOptionLabel={o => o.label} onchange={onCompareModeChange} /> +
+ + +
{#if compareMode === CompareMode.STORED_TO_STORED} From 6f995ec1b61a8aba046aead826fa14d7e3aace9d Mon Sep 17 00:00:00 2001 From: tarn Date: Wed, 8 Jul 2026 09:22:58 +0200 Subject: [PATCH 2/8] draft --- .../MigrationArtifactsRESTController.java | 55 ++-- .../MigrationContextRESTController.java | 16 +- .../SemanticAttributeChange.java | 2 +- .../SemanticFieldChangeType.java | 1 + .../compare/TripleChangeAnalyser.java | 14 +- .../SchemaMigrationService.java | 26 +- .../SemanticChangeAnalyser.java | 30 +- .../SemanticFieldChangeTypeMapper.java | 2 +- .../SetMigrationContextUseCase.java | 12 +- .../MarkdownMigrationReportBuilder.java | 260 ++++++++++++------ .../artifacts/MigrationReportBuilder.java | 7 +- .../artifacts/SparqlMigrationBuilder.java | 97 ++++--- .../defaults/InheritanceChangeHandler.java | 22 +- .../renamings/RenameDetector.java | 9 +- .../renamings/RenameObjectBuilder.java | 12 + .../renamings/SimilarityCalculator.java | 11 +- frontend/src/lib/utils/migrationUtils.js | 27 ++ .../migrate/steps/ConfirmClassRenames.svelte | 57 ++-- .../steps/ConfirmPropertyRenames.svelte | 43 ++- .../routes/migrate/steps/SelectSchemas.svelte | 6 +- 20 files changed, 482 insertions(+), 227 deletions(-) create mode 100644 frontend/src/lib/utils/migrationUtils.js diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationArtifactsRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationArtifactsRESTController.java index bd2fbb1f..19473e91 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationArtifactsRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationArtifactsRESTController.java @@ -21,13 +21,9 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.responses.ApiResponse; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; + import lombok.RequiredArgsConstructor; + import org.apache.jena.riot.RDFFormat; import org.rdfarchitect.context.MigrationSessionStore; import org.rdfarchitect.services.schemamigration.artifacts.GenerateMigrationReportUseCase; @@ -43,6 +39,13 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + @RestController @RequiredArgsConstructor public class MigrationArtifactsRESTController { @@ -109,33 +112,39 @@ public ResponseEntity generateMigrationScript( "Generates a migration script based on the previously computed migration actions and the shacl shapes of the new schema.", tags = {"migration"}, responses = { - @ApiResponse( - responseCode = "200", - content = @Content(mediaType = "application/zip")) + @ApiResponse( + responseCode = "200", + content = @Content(mediaType = "application/zip")) }) @GetMapping("/api/migrations/report") public ResponseEntity generateMigrationReport( @Parameter(description = "The name/url of the inquirer.") - @RequestHeader(value = "origin", required = false, defaultValue = "unknown") - String originURL, - @Parameter(description = "Type of the migration report to generate. Can be either SUMMARY or DETAILED.") - @RequestParam(value = "type", defaultValue = "SUMMARY") MigrationReportType reportType) - throws IOException { + @RequestHeader(value = "origin", required = false, defaultValue = "unknown") + String originURL, + @Parameter( + description = + "Type of the migration report to generate. Can be either SUMMARY or DETAILED.") + @RequestParam(value = "type", defaultValue = "SUMMARY") + MigrationReportType reportType) + { logger.info("Received GET request: \"/api/migrations/report\" from \"{}\".", originURL); - var report = switch (reportType) { - case SUMMARY -> generateMigrationReportUseCase.generateSummaryMigrationReport(); - case DETAILED -> generateMigrationReportUseCase.generateDetailedMigrationReport(); - }; + var report = + switch (reportType) { + case SUMMARY -> generateMigrationReportUseCase.generateSummaryMigrationReport(); + case DETAILED -> + generateMigrationReportUseCase.generateDetailedMigrationReport(); + }; var body = report.getBytes(StandardCharsets.UTF_8); var headers = new HttpHeaders(); headers.setAccessControlExposeHeaders(List.of("Content-disposition")); - var response = ResponseEntity.ok() - .headers(headers) - .header(HttpHeaders.CONTENT_DISPOSITION, "migration_report.md") - .contentType(MediaType.APPLICATION_OCTET_STREAM) - .body(body); + var response = + ResponseEntity.ok() + .headers(headers) + .header(HttpHeaders.CONTENT_DISPOSITION, "migration_report.md") + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(body); logger.info( "Sending response to GET request: \"/api/migrations/report\" from \"{}\".", diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationContextRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationContextRESTController.java index f2633288..32a5ec31 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationContextRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationContextRESTController.java @@ -91,11 +91,10 @@ public void computeMigrationContext( @RequestParam(required = false) String graphB, @Parameter( - description = - "Whether to ignore prefixes in the comparison and rename detection. Defaults to false.") + description = + "Whether to ignore prefixes in the comparison and rename detection. Defaults to false.") @RequestParam(required = false, defaultValue = "false") - boolean ignorePrefixes - ){ + boolean ignorePrefixes) { logger.info("Received POST request: \"/api/migrations/context\" from \"{}\".", originURL); // file to file @@ -115,18 +114,13 @@ else if (datasetA != null && datasetB != null && graphA != null && graphB != nul else if (fileA != null && datasetB != null && graphB != null) { var extendedGraphURIB = expandURIUseCase.expandUri(datasetB, graphB); setMigrationContextUseCase.setMigrationContext( - fileA, - new GraphIdentifier(datasetB, extendedGraphURIB), - ignorePrefixes - ); + fileA, new GraphIdentifier(datasetB, extendedGraphURIB), ignorePrefixes); } // stored to file else if (datasetA != null && graphA != null && fileA != null) { var extendedGraphURIA = expandURIUseCase.expandUri(datasetA, graphA); setMigrationContextUseCase.setMigrationContext( - new GraphIdentifier(datasetA, extendedGraphURIA), - fileA, - ignorePrefixes); + new GraphIdentifier(datasetA, extendedGraphURIA), fileA, ignorePrefixes); } else { logger.warn( "Invalid request to POST \"/api/migrations/context\" from \"{}\". Missing required parameters.", diff --git a/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticAttributeChange.java b/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticAttributeChange.java index 4335e9bb..877850d3 100644 --- a/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticAttributeChange.java +++ b/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticAttributeChange.java @@ -25,7 +25,6 @@ import org.apache.jena.rdf.model.Resource; import org.rdfarchitect.models.changes.triplechanges.TripleResourceChange; -import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; import java.util.ArrayList; import java.util.List; @@ -69,6 +68,7 @@ public SemanticAttributeChange(SemanticAttributeChange other) { optional = other.isOptional(); forceDefaultValue = other.isForceDefaultValue(); allowedValues = new ArrayList<>(other.getAllowedValues()); + inheritedFrom = other.getInheritedFrom(); } public SemanticAttributeChange(SemanticResourceChange resourceChange) { diff --git a/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticFieldChangeType.java b/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticFieldChangeType.java index 49794410..197d0490 100644 --- a/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticFieldChangeType.java +++ b/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticFieldChangeType.java @@ -22,6 +22,7 @@ public enum SemanticFieldChangeType { LABEL_CHANGE, COMMENT_CHANGE, SUPERCLASS_CHANGE, + SUPERCLASS_RENAME, BELONGS_TO_CATEGORY_CHANGE, DATATYPE_CHANGE, DATATYPE_RENAME, diff --git a/backend/src/main/java/org/rdfarchitect/services/compare/TripleChangeAnalyser.java b/backend/src/main/java/org/rdfarchitect/services/compare/TripleChangeAnalyser.java index 878b00bb..33dc698f 100644 --- a/backend/src/main/java/org/rdfarchitect/services/compare/TripleChangeAnalyser.java +++ b/backend/src/main/java/org/rdfarchitect/services/compare/TripleChangeAnalyser.java @@ -208,7 +208,8 @@ private TripleClassChange compareClass( * @return A map of property URIs to TriplePropertyChange objects representing the differences * in properties. */ - private List compareResource(Graph originalGraph, Graph updatedGraph, String uri) { + private List compareResource( + Graph originalGraph, Graph updatedGraph, String uri) { var originalTriples = originalGraph.find(NodeFactory.createURI(uri), Node.ANY, Node.ANY).toList(); var updatedTriples = @@ -221,8 +222,9 @@ private List compareResource(Graph originalGraph, Graph up return propertyChanges; } - private List compareStereotypes(List originalTriples, List updatedTriples) { - var changes = new ArrayList(); + private List compareStereotypes( + List originalTriples, List updatedTriples) { + var changes = new ArrayList(); var originalStereotypes = originalTriples.stream() @@ -256,7 +258,8 @@ private List compareStereotypes(List originalTripl return changes; } - private List compareSimplePredicates(List originalTriples, List updatedTriples) { + private List compareSimplePredicates( + List originalTriples, List updatedTriples) { var changes = new ArrayList(); var originalValues = @@ -287,7 +290,8 @@ private List compareSimplePredicates(List original return changes; } - private static TriplePropertyChange comparePredicate(Node predicate, Map originalValues, Map updatedValues) { + private static TriplePropertyChange comparePredicate( + Node predicate, Map originalValues, Map updatedValues) { // ignore uuid predicates, stereotypes are handled separately if (predicate.equals(RDFA.uuid.asNode())) { return null; diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/SchemaMigrationService.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/SchemaMigrationService.java index d47ecdef..09c1bfcf 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/SchemaMigrationService.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/SchemaMigrationService.java @@ -39,7 +39,9 @@ import org.rdfarchitect.rdf.graph.source.builder.implementations.GraphFileSourceBuilderImpl; import org.rdfarchitect.services.compare.TripleChangeAnalyser; import org.rdfarchitect.services.schemamigration.artifacts.GenerateMigrationReportUseCase; +import org.rdfarchitect.services.schemamigration.artifacts.GenerateMigrationScriptUseCase; import org.rdfarchitect.services.schemamigration.artifacts.MigrationReportBuilder; +import org.rdfarchitect.services.schemamigration.artifacts.MigrationScriptBuilder; import org.rdfarchitect.services.schemamigration.defaults.DefaultValueAssigner; import org.rdfarchitect.services.schemamigration.defaults.GetDefaultValueViewsUseCase; import org.rdfarchitect.services.schemamigration.defaults.InheritanceChangeHandler; @@ -50,8 +52,6 @@ import org.rdfarchitect.services.schemamigration.renamings.GetPropertyRenamingsUseCase; import org.rdfarchitect.services.schemamigration.renamings.RenameDetector; import org.rdfarchitect.services.schemamigration.renamings.RenameObjectBuilder; -import org.rdfarchitect.services.schemamigration.artifacts.GenerateMigrationScriptUseCase; -import org.rdfarchitect.services.schemamigration.artifacts.MigrationScriptBuilder; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -65,13 +65,13 @@ public class SchemaMigrationService implements SetMigrationContextUseCase, GetClassRenamingsUseCase, GetPropertyRenamingsUseCase, - GenerateMigrationScriptUseCase, + GenerateMigrationScriptUseCase, ClassRenamingsUseCase, ConfirmPropertyRenamingsUseCase, ClearMigrationContextUseCase, GetDefaultValueViewsUseCase, SubmitDefaultValuesUseCase, - GenerateMigrationReportUseCase { + GenerateMigrationReportUseCase { private final MigrationSessionStore migrationSessionStore; private final DatabasePort databasePort; @@ -81,7 +81,8 @@ public class SchemaMigrationService private static final String GRAPH_URI = "http://example.org/graph"; @Override - public void setMigrationContext(MultipartFile originalSchema, GraphIdentifier updatedSchema, boolean ignorePrefixes) { + public void setMigrationContext( + MultipartFile originalSchema, GraphIdentifier updatedSchema, boolean ignorePrefixes) { var originalGraph = new GraphFileSourceBuilderImpl() .setFile(originalSchema) @@ -99,7 +100,8 @@ public void setMigrationContext(MultipartFile originalSchema, GraphIdentifier up } @Override - public void setMigrationContext(GraphIdentifier originalSchema, GraphIdentifier updatedSchema, boolean ignorePrefixes) { + public void setMigrationContext( + GraphIdentifier originalSchema, GraphIdentifier updatedSchema, boolean ignorePrefixes) { Graph originalGraph; try (var originalCtx = databasePort.getGraphWithContext(originalSchema).begin(ReadWrite.READ)) { @@ -116,7 +118,8 @@ public void setMigrationContext(GraphIdentifier originalSchema, GraphIdentifier } @Override - public void setMigrationContext(GraphIdentifier originalSchema, MultipartFile updatedSchema, boolean ignorePrefixes) { + public void setMigrationContext( + GraphIdentifier originalSchema, MultipartFile updatedSchema, boolean ignorePrefixes) { Graph originalGraph; try (var originalCtx = databasePort.getGraphWithContext(originalSchema).begin(ReadWrite.READ)) { @@ -134,7 +137,8 @@ public void setMigrationContext(GraphIdentifier originalSchema, MultipartFile up } @Override - public void setMigrationContext(MultipartFile originalSchema, MultipartFile updatedSchema, boolean ignorePrefixes) { + public void setMigrationContext( + MultipartFile originalSchema, MultipartFile updatedSchema, boolean ignorePrefixes) { var originalGraph = new GraphFileSourceBuilderImpl() .setFile(originalSchema) @@ -227,15 +231,15 @@ public List getPropertyRenamings() { new ArrayList<>(migrationSessionStore.getContext().getDiffAfterClassConfirm()); var result = new ArrayList(); for (var cls : classes) { - if (cls.getAttributeRenameCandidates() == null) { + if (cls.getAttributeRenameCandidates().isEmpty()) { cls.setAttributeRenameCandidates( RenameDetector.detectPropertyRenames(cls.getAttributes())); } - if (cls.getAssociationRenameCandidates() == null) { + if (cls.getAssociationRenameCandidates().isEmpty()) { cls.setAssociationRenameCandidates( RenameDetector.detectPropertyRenames(cls.getAssociations())); } - if (cls.getEnumEntryRenameCandidates() == null) { + if (cls.getEnumEntryRenameCandidates().isEmpty()) { cls.setEnumEntryRenameCandidates( RenameDetector.detectPropertyRenames(cls.getEnumEntries())); } diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/SemanticChangeAnalyser.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/SemanticChangeAnalyser.java index 3e0d868a..235253a7 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/SemanticChangeAnalyser.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/SemanticChangeAnalyser.java @@ -69,14 +69,18 @@ private SemanticClassChange getSemanticChangesForClass(TripleClassChange tripleC if (tripleClassChange.getAttributes() != null) { for (var attribute : tripleClassChange.getAttributes()) { - semanticChangeObject.getAttributes().add(getSemanticChangesForAttribute(attribute)); + var attributeChange = getSemanticChangesForAttribute(attribute); + if (attributeChange != null) { + semanticChangeObject.getAttributes().add(attributeChange); + } } } if (tripleClassChange.getAssociations() != null) { for (var association : tripleClassChange.getAssociations()) { - semanticChangeObject - .getAssociations() - .add(getSemanticChangesForAssociation(association)); + var associationChange = getSemanticChangesForAssociation(association); + if (associationChange != null) { + semanticChangeObject.getAssociations().add(associationChange); + } } } if (tripleClassChange.getEnumEntries() != null) { @@ -109,7 +113,12 @@ private SemanticAttributeChange getSemanticChangesForAttribute(TripleResourceCha } } - return semanticChangeObject; + // reformatting of multiplicity can lead to change object with no semantic changes + if (!changes.isEmpty()) { + return semanticChangeObject; + } else { + return null; + } } private SemanticAssociationChange getSemanticChangesForAssociation( @@ -120,7 +129,7 @@ private SemanticAssociationChange getSemanticChangesForAssociation( semanticChangeObject.setSemanticResourceChangeType( getResourceChangeType(associationChanges)); - // parse properties for individual changes + var changes = semanticChangeObject.getChanges(); for (var propertyChange : associationChanges) { var action = new SemanticFieldChange(propertyChange); var mappedType = @@ -128,11 +137,16 @@ private SemanticAssociationChange getSemanticChangesForAssociation( "Association", propertyChange); if (mappedType != null) { action.setSemanticFieldChangeType(mappedType); - semanticChangeObject.getChanges().add(action); + changes.add(action); } } - return semanticChangeObject; + // reformatting of multiplicity can lead to change object with no semantic changes + if (!changes.isEmpty()) { + return semanticChangeObject; + } else { + return null; + } } private SemanticEnumEntryChange getSemanticChangesForEnumEntry(TripleResourceChange enumEntry) { diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/SemanticFieldChangeTypeMapper.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/SemanticFieldChangeTypeMapper.java index b40800f5..8b9ab370 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/SemanticFieldChangeTypeMapper.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/SemanticFieldChangeTypeMapper.java @@ -84,7 +84,7 @@ private static SemanticFieldChangeType mapAttributePredicateToChangeType( new URI(propertyChange.getTo()).getSuffix()); // only syntactic change in multiplicity if (propertyChange.getFrom() != null - && CIMPropertyUtils.resolveMultiplicity(propertyChange.getFrom()) + && CIMPropertyUtils.resolveMultiplicity(new URI(propertyChange.getFrom()).getSuffix()) .equals(newMultiplicity)) { return null; } diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/SetMigrationContextUseCase.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/SetMigrationContextUseCase.java index 2a8865cb..da262380 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/SetMigrationContextUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/SetMigrationContextUseCase.java @@ -31,7 +31,8 @@ public interface SetMigrationContextUseCase { * @param updatesSchema the uploaded old schema * @param ignorePrefixes whether to ignore prefixes in comparison and rename detection */ - void setMigrationContext(MultipartFile originalSchema, GraphIdentifier updatesSchema, boolean ignorePrefixes); + void setMigrationContext( + MultipartFile originalSchema, GraphIdentifier updatesSchema, boolean ignorePrefixes); /** * Sets the migration context for the given graph identifier of an edited schema loaded in @@ -41,7 +42,8 @@ public interface SetMigrationContextUseCase { * @param updatedSchema the graph identifier of the old schema loaded in memory * @param ignorePrefixes whether to ignore prefixes in comparison and rename detection */ - void setMigrationContext(GraphIdentifier originalSchema, GraphIdentifier updatedSchema, boolean ignorePrefixes); + void setMigrationContext( + GraphIdentifier originalSchema, GraphIdentifier updatedSchema, boolean ignorePrefixes); /** * Sets the migration context for a stored original schema and an uploaded updated schema. @@ -50,7 +52,8 @@ public interface SetMigrationContextUseCase { * @param updatedSchema the uploaded updated schema * @param ignorePrefixes whether to ignore prefixes in comparison and rename detection */ - void setMigrationContext(GraphIdentifier originalSchema, MultipartFile updatedSchema, boolean ignorePrefixes); + void setMigrationContext( + GraphIdentifier originalSchema, MultipartFile updatedSchema, boolean ignorePrefixes); /** * Sets the migration context for the two uploaded schema files. @@ -59,5 +62,6 @@ public interface SetMigrationContextUseCase { * @param updatedSchema the graph identifier of the old schema loaded in memory * @param ignorePrefixes whether to ignore prefixes in comparison and rename detection */ - void setMigrationContext(MultipartFile originalSchema, MultipartFile updatedSchema, boolean ignorePrefixes); + void setMigrationContext( + MultipartFile originalSchema, MultipartFile updatedSchema, boolean ignorePrefixes); } diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MarkdownMigrationReportBuilder.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MarkdownMigrationReportBuilder.java index 827ec401..8257c28b 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MarkdownMigrationReportBuilder.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MarkdownMigrationReportBuilder.java @@ -20,15 +20,18 @@ import org.apache.jena.graph.Graph; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.Resource; +import org.apache.jena.shared.PrefixMapping; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; import org.rdfarchitect.models.changes.semanticchanges.SemanticClassChange; import org.rdfarchitect.models.changes.semanticchanges.SemanticFieldChange; import org.rdfarchitect.models.changes.semanticchanges.SemanticResourceChange; import org.rdfarchitect.models.changes.semanticchanges.SemanticResourceChangeType; +import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; import org.rdfarchitect.models.cim.rdf.resources.CIMS; import org.rdfarchitect.models.cim.rdf.resources.CIMStereotypes; import org.springframework.stereotype.Service; + import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; @@ -38,25 +41,32 @@ @Service public class MarkdownMigrationReportBuilder implements MigrationReportBuilder { + private PrefixMapping prefixMapping; + @Override - public String generateDetailedMigrationReport(List classChanges, Graph originalGraph) { + public String generateDetailedMigrationReport( + List classChanges, Graph originalGraph) { + prefixMapping = originalGraph.getPrefixMapping(); + var sb = new StringBuilder(); sb.append("# Migration Report — Detailed View\n\n"); sb.append("All affected concrete classes are listed alphabetically. "); sb.append("Inherited changes are shown under each affected subclass.\n\n"); var model = ModelFactory.createModelForGraph(originalGraph); - var concreteClassIRIs = model.listResourcesWithProperty(RDF.type, RDFS.Class) - .filterKeep(r -> r.hasProperty(CIMS.stereotype, CIMStereotypes.concrete)) - .filterDrop(r -> r.hasProperty(CIMS.stereotype, CIMStereotypes.enumeration)) - .filterDrop(r -> r.hasProperty(CIMS.stereotype, CIMStereotypes.cimDataType)) - .mapWith(Resource::getURI) - .toList(); + var concreteClassIRIs = + model.listResourcesWithProperty(RDF.type, RDFS.Class) + .filterKeep(r -> r.hasProperty(CIMS.stereotype, CIMStereotypes.concrete)) + .filterDrop(r -> r.hasProperty(CIMS.stereotype, CIMStereotypes.enumeration)) + .filterDrop(r -> r.hasProperty(CIMS.stereotype, CIMStereotypes.cimDataType)) + .mapWith(Resource::getURI) + .toList(); - var concreteChanges = classChanges.stream() - .filter(c -> concreteClassIRIs.contains(c.getIri())) - .sorted(Comparator.comparing(SemanticResourceChange::getLabel)) - .toList(); + var concreteChanges = + classChanges.stream() + .filter(c -> concreteClassIRIs.contains(c.getIri())) + .sorted(Comparator.comparing(SemanticResourceChange::getLabel)) + .toList(); for (var classChange : concreteChanges) { appendClassSection(sb, classChange); @@ -66,15 +76,26 @@ public String generateDetailedMigrationReport(List classCha } private void appendClassSection(StringBuilder sb, SemanticClassChange classChange) { - sb.append("## ").append(formatChangeType(classChange.getSemanticResourceChangeType())) - .append(" `").append(classChange.getLabel()).append("`\n\n"); - sb.append("**IRI:** `").append(classChange.getIri()).append("`\n\n"); + if (classChange.getSemanticResourceChangeType() == SemanticResourceChangeType.RENAME) { + var oldLabel = new URI(classChange.getOldIRI()).getSuffix(); + sb.append("## Renamed from ").append(oldLabel).append(" to ").append(classChange.getLabel()) + .append("\n\n"); + sb.append("**Old IRI:** ").append(shorten(classChange.getOldIRI())).append("\n\n"); + sb.append("**IRI:** ").append(shorten(classChange.getIri())).append("\n\n"); + } else { + sb.append("## ") + .append(formatChangeType(classChange.getSemanticResourceChangeType())) + .append(" ") + .append(classChange.getLabel()) + .append("\n\n"); + sb.append("**IRI:** ").append(shorten(classChange.getIri())).append("\n\n"); + } - if (classChange.getOldIRI() != null) { - sb.append("**Previous IRI:** `").append(classChange.getOldIRI()).append("`\n\n"); + if (classChange.getComment() != null && !classChange.getComment().isBlank()) { + sb.append("> ").append(classChange.getComment()).append("\n\n"); } - appendFieldChangesTable(sb, classChange.getChanges()); + appendFieldChangesAsSentences(sb, classChange.getChanges()); appendPropertySection(sb, "Attributes", classChange.getAttributes()); appendPropertySection(sb, "Associations", classChange.getAssociations()); appendPropertySection(sb, "Enum Entries", classChange.getEnumEntries()); @@ -82,36 +103,41 @@ private void appendClassSection(StringBuilder sb, SemanticClassChange classChang } @Override - public String generateSummaryMigrationReport(List classChanges, Graph originalGraph) { + public String generateSummaryMigrationReport( + List classChanges, Graph originalGraph) { var sb = new StringBuilder(); sb.append("# Migration Report — Summary View\n\n"); - sb.append("Classes with direct changes. For parent classes, affected concrete subclasses are listed.\n\n"); + sb.append( + "Classes with direct changes. For parent classes, affected concrete subclasses are listed.\n\n"); - // Classes that have at least one direct (non-inherited) change - var directlyChanged = classChanges.stream() - .filter(this::hasDirectChange) - .sorted(Comparator.comparing(SemanticResourceChange::getLabel)) - .toList(); + var directlyChanged = + classChanges.stream() + .filter(this::hasDirectChange) + .sorted(Comparator.comparing(SemanticResourceChange::getLabel)) + .toList(); for (var classChange : directlyChanged) { - sb.append("## ").append(formatChangeType(classChange.getSemanticResourceChangeType())) - .append(" `").append(classChange.getLabel()).append("`\n\n"); - sb.append("**IRI:** `").append(classChange.getIri()).append("`\n\n"); + sb.append("## ") + .append(formatChangeType(classChange.getSemanticResourceChangeType())) + .append(" ") + .append(classChange.getLabel()) + .append("\n\n"); + sb.append("**IRI:** ").append(shorten(classChange.getIri())).append("\n\n"); - // Show only direct changes - var directFieldChanges = classChange.getChanges(); // class-level are always direct - appendFieldChangesTable(sb, directFieldChanges); + if (classChange.getComment() != null && !classChange.getComment().isBlank()) { + sb.append("> ").append(classChange.getComment()).append("\n\n"); + } + appendFieldChangesAsSentences(sb, classChange.getChanges()); appendDirectProperties(sb, "Attributes", classChange.getAttributes()); appendDirectProperties(sb, "Associations", classChange.getAssociations()); appendDirectProperties(sb, "Enum Entries", classChange.getEnumEntries()); - // List affected concrete subclasses var affectedSubclasses = findAffectedSubclasses(classChange, classChanges); if (!affectedSubclasses.isEmpty()) { - sb.append("\n**Affected concrete classes:**\n\n"); + sb.append("**Affected concrete classes:**\n\n"); for (var sub : affectedSubclasses) { - sb.append("- `").append(sub.getLabel()).append("`\n"); + sb.append("- ").append(sub.getLabel()).append("\n"); } sb.append("\n"); } @@ -122,6 +148,97 @@ public String generateSummaryMigrationReport(List classChan return sb.toString(); } + private void appendFieldChangesAsSentences(StringBuilder sb, List changes) { + if (changes == null || changes.isEmpty()) return; + for (var change : changes) { + sb.append("- ").append(fieldChangeToSentence(change)).append("\n"); + } + sb.append("\n"); + } + + private String fieldChangeToSentence(SemanticFieldChange change) { + var from = change.getFrom(); + var to = change.getTo(); + + return switch (change.getSemanticFieldChangeType()) { + case LABEL_CHANGE -> formatTransition("Label changed", from, to); + case COMMENT_CHANGE -> "Comment was updated."; + case SUPERCLASS_CHANGE -> formatTransition("Superclass changed", from, to); + case SUPERCLASS_RENAME -> formatTransition("Superclass renamed", from, to); + case BELONGS_TO_CATEGORY_CHANGE -> formatTransition("Package changed", from, to); + case DATATYPE_CHANGE -> formatTransition("Datatype changed", from, to); + case DATATYPE_RENAME -> formatTransition("Datatype renamed", from, to); + case MADE_OPTIONAL -> "Was made optional."; + case MADE_REQUIRED -> "Was made required."; + case MULTIPLICITY_CHANGE -> formatTransition("Multiplicity changed", from, to); + case STEREOTYPE_ADDED -> "Stereotype added: `" + shorten(to) + "`."; + case STEREOTYPE_REMOVED -> "Stereotype removed: " + shorten(from) + "."; + case MADE_ABSTRACT -> "Was made abstract."; + case DOMAIN_CHANGE -> formatTransition("Domain changed", from, to); + case DOMAIN_RENAME -> formatTransition("Domain renamed", from, to); + case TARGET_CHANGE -> formatTransition("Target changed", from, to); + case ASSOCIATION_USED_CHANGE -> formatTransition("Association used changed", from, to); + case DEFAULT_VALUE_CHANGE -> formatTransition("Default value changed", from, to); + case FIXED_VALUE_CHANGE -> formatTransition("Fixed value changed", from, to); + }; + } + + private String formatTransition(String description, String from, String to) { + var shortFrom = shorten(from); + var shortTo = shorten(to); + if (from == null && to != null) { + return description + " to `" + shortTo + "`."; + } else if (from != null && to == null) { + return description + " (removed `" + shortFrom + "`)."; + } else if (from != null) { + return description + " from `" + shortFrom + "` to `" + shortTo + "`."; + } + return description + "."; + } + + private void appendPropertySection( + StringBuilder sb, String title, List properties) { + if (properties == null || properties.isEmpty()) return; + sb.append("### ").append(title).append("\n\n"); + + for (var prop : properties) { + if (prop.getSemanticResourceChangeType() == SemanticResourceChangeType.RENAME + && prop.getOldIRI() != null) { + var oldLabel = new URI(prop.getOldIRI()).getSuffix(); + sb.append("#### Renamed from ").append(oldLabel).append(" to ") + .append(prop.getLabel()).append("\n\n"); + } else { + sb.append("#### ") + .append(formatChangeType(prop.getSemanticResourceChangeType())) + .append(" ") + .append(prop.getLabel()) + .append("\n\n"); + } + + if (prop.getComment() != null && !prop.getComment().isBlank()) { + sb.append("> ").append(prop.getComment()).append("\n\n"); + } + + appendFieldChangesAsSentences(sb, prop.getChanges()); + } + } + + private void appendDirectProperties( + StringBuilder sb, String title, List properties) { + if (properties == null) return; + var direct = + properties.stream() + .filter( + p -> + p.getSemanticResourceChangeType() + != SemanticResourceChangeType.ADDED_FROM_INHERITANCE + && p.getSemanticResourceChangeType() + != SemanticResourceChangeType.DELETED_FROM_INHERITANCE) + .toList(); + if (direct.isEmpty()) return; + appendPropertySection(sb, title, direct); + } + private boolean hasDirectChange(SemanticClassChange c) { if (!c.getChanges().isEmpty()) return true; var allProperties = new ArrayList(c.getAssociations()); @@ -129,16 +246,15 @@ private boolean hasDirectChange(SemanticClassChange c) { allProperties.addAll(c.getEnumEntries()); if (allProperties.isEmpty()) return false; - return allProperties.stream().anyMatch(p -> - p.getSemanticResourceChangeType() != SemanticResourceChangeType.ADDED_FROM_INHERITANCE - && p.getSemanticResourceChangeType() != SemanticResourceChangeType.DELETED_FROM_INHERITANCE); + return allProperties.stream() + .anyMatch( + p -> p.getSemanticResourceChangeType() != SemanticResourceChangeType.ADDED_FROM_INHERITANCE + && p.getSemanticResourceChangeType() != SemanticResourceChangeType.DELETED_FROM_INHERITANCE); } private List findAffectedSubclasses( - SemanticClassChange parentChange, - List allChanges) { + SemanticClassChange parentChange, List allChanges) { var directPropertyLabels = collectDirectPropertyLabels(parentChange); - return allChanges.stream() .filter(c -> !c.getIri().equals(parentChange.getIri())) .filter(c -> hasMatchingInheritedProperty(c, directPropertyLabels)) @@ -162,8 +278,7 @@ private void addDirectLabels(Set labe .forEach(p -> labels.add(p.getLabel())); } - private boolean hasMatchingInheritedProperty( - SemanticClassChange classChange, Set parentPropertyLabels) { + private boolean hasMatchingInheritedProperty(SemanticClassChange classChange, Set parentPropertyLabels) { return matchesAny(classChange.getAttributes(), parentPropertyLabels) || matchesAny(classChange.getAssociations(), parentPropertyLabels) || matchesAny(classChange.getEnumEntries(), parentPropertyLabels); @@ -177,55 +292,24 @@ private boolean matchesAny(List properties .anyMatch(p -> labels.contains(p.getLabel())); } - private void appendFieldChangesTable(StringBuilder sb, List changes) { - if (changes == null || changes.isEmpty()) return; - sb.append("| Change | From | To |\n"); - sb.append("|--------|------|----|\n"); - for (var c : changes) { - sb.append("| ").append(c.getSemanticFieldChangeType()) - .append(" | ").append(c.getFrom() != null ? "`" + c.getFrom() + "`" : "—") - .append(" | ").append(c.getTo() != null ? "`" + c.getTo() + "`" : "—") - .append(" |\n"); - } - sb.append("\n"); - } - - private void appendPropertySection( - StringBuilder sb, String title, List properties) { - if (properties == null || properties.isEmpty()) return; - sb.append("### ").append(title).append("\n\n"); - for (var prop : properties) { - sb.append("- ").append(formatChangeType(prop.getSemanticResourceChangeType())) - .append(" `").append(prop.getLabel()).append("`"); - if (!prop.getChanges().isEmpty()) { - sb.append("\n"); - appendFieldChangesTable(sb, prop.getChanges()); - } else { - sb.append("\n"); - } - } - sb.append("\n"); - } - - private void appendDirectProperties( - StringBuilder sb, String title, List properties) { - if (properties == null) return; - var direct = properties.stream() - .filter(p -> p.getSemanticResourceChangeType() != SemanticResourceChangeType.ADDED_FROM_INHERITANCE - && p.getSemanticResourceChangeType() != SemanticResourceChangeType.DELETED_FROM_INHERITANCE) - .toList(); - if (direct.isEmpty()) return; - appendPropertySection(sb, title, direct); - } - private String formatChangeType(SemanticResourceChangeType type) { return switch (type) { - case ADD -> "[Added]"; - case DELETE -> "[Deleted]"; - case CHANGE -> "[Changed]"; - case RENAME -> "[Renamed]"; - case ADDED_FROM_INHERITANCE -> "[Added via inheritance]"; - case DELETED_FROM_INHERITANCE -> "[Deleted via inheritance]"; + case ADD -> "Added"; + case DELETE -> "Deleted"; + case CHANGE -> "Changed"; + case RENAME -> "Renamed"; + case ADDED_FROM_INHERITANCE -> "[Added via inheritance"; + case DELETED_FROM_INHERITANCE -> "[Deleted via inheritance"; }; } + + private String shorten(String value) { + if (value == null) return null; + + var shortened = prefixMapping.shortForm(value); + if (shortened.equals(value)) { + return new URI(value).getSuffix(); + } + return shortened; + } } diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MigrationReportBuilder.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MigrationReportBuilder.java index bdc86a5e..ef385d44 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MigrationReportBuilder.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MigrationReportBuilder.java @@ -19,10 +19,13 @@ import org.apache.jena.graph.Graph; import org.rdfarchitect.models.changes.semanticchanges.SemanticClassChange; + import java.util.List; public interface MigrationReportBuilder { - String generateDetailedMigrationReport(List classChanges, Graph originalGraph); + String generateDetailedMigrationReport( + List classChanges, Graph originalGraph); - String generateSummaryMigrationReport(List classChanges, Graph originalGraph); + String generateSummaryMigrationReport( + List classChanges, Graph originalGraph); } diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/SparqlMigrationBuilder.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/SparqlMigrationBuilder.java index b98a74b3..84eb9836 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/SparqlMigrationBuilder.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/SparqlMigrationBuilder.java @@ -19,12 +19,12 @@ import lombok.RequiredArgsConstructor; -import org.apache.jena.update.UpdateRequest; import org.rdfarchitect.models.changes.semanticchanges.SemanticAssociationChange; import org.rdfarchitect.models.changes.semanticchanges.SemanticAttributeChange; import org.rdfarchitect.models.changes.semanticchanges.SemanticClassChange; import org.rdfarchitect.models.changes.semanticchanges.SemanticEnumEntryChange; import org.rdfarchitect.models.changes.semanticchanges.SemanticFieldChangeType; +import org.rdfarchitect.models.changes.semanticchanges.SemanticResourceChange; import org.rdfarchitect.models.changes.semanticchanges.SemanticResourceChangeType; import org.springframework.stereotype.Service; @@ -38,15 +38,20 @@ public class SparqlMigrationBuilder implements MigrationScriptBuilder { @Override public String generateMigrationScript(List classChanges) { - var script = new UpdateRequest(); + var sb = new StringBuilder(); for (var classChange : classChanges) { - script.add(generateUpdateForClass(classChange)); + var classBlock = generateUpdateForClass(classChange); + if (!classBlock.isBlank()) { + sb.append(classBlock); + } } - return script.toString(); + return sb.toString(); } private String generateUpdateForClass(SemanticClassChange classChange) { - var result = new UpdateRequest(); + var sb = new StringBuilder(); + + appendComment(sb, classChange); String update = switch (classChange.getSemanticResourceChangeType()) { @@ -55,47 +60,57 @@ private String generateUpdateForClass(SemanticClassChange classChange) { default -> null; }; - if (update != null) { - result.add(update); + if (update != null && !update.isBlank()) { + sb.append(update).append("\n"); } if (classChange.getSemanticResourceChangeType() != SemanticResourceChangeType.DELETE && classChange.getSemanticResourceChangeType() != SemanticResourceChangeType.ADD) { - processClassChanges(classChange, result); + processClassChanges(classChange, sb); } - processPropertyChanges(classChange, result); + processPropertyChanges(classChange, sb); - return result.toString(); + return sb.toString(); } - private void processClassChanges(SemanticClassChange classChange, UpdateRequest result) { + private void processClassChanges(SemanticClassChange classChange, StringBuilder sb) { for (var change : classChange.getChanges()) { if (change.getSemanticFieldChangeType() == SemanticFieldChangeType.MADE_ABSTRACT) { - result.add(updateGenerator.generateDeleteClassUpdate(classChange)); + sb.append(updateGenerator.generateDeleteClassUpdate(classChange)).append("\n"); } } } - private void processPropertyChanges(SemanticClassChange classChange, UpdateRequest result) { + private void processPropertyChanges(SemanticClassChange classChange, StringBuilder sb) { for (var attribute : classChange.getAttributes()) { - result.add(generateUpdateForAttribute(attribute, classChange.getIri())); + var block = generateUpdateForAttribute(attribute, classChange.getIri()); + if (!block.isBlank()) { + sb.append(block); + } } for (var association : classChange.getAssociations()) { - result.add(generateUpdateForAssociation(association)); + var block = generateUpdateForAssociation(association); + if (!block.isBlank()) { + sb.append(block); + } } if (classChange.getSemanticResourceChangeType() != SemanticResourceChangeType.DELETE) { for (var enumEntry : classChange.getEnumEntries()) { - result.add(generateUpdateForEnumEntry(enumEntry)); + var block = generateUpdateForEnumEntry(enumEntry); + if (!block.isBlank()) { + sb.append(block); + } } } } private String generateUpdateForAttribute( SemanticAttributeChange attributeChange, String classIri) { - var result = new UpdateRequest(); + var sb = new StringBuilder(); + appendComment(sb, attributeChange); var update = switch (attributeChange.getSemanticResourceChangeType()) { @@ -112,21 +127,21 @@ private String generateUpdateForAttribute( default -> null; }; - if (update != null) { - result.add(update); + if (update != null && !update.isBlank()) { + sb.append(update).append("\n"); } var type = attributeChange.getSemanticResourceChangeType(); if (type == SemanticResourceChangeType.CHANGE || type == SemanticResourceChangeType.RENAME) { - processAttributeFieldChanges(attributeChange, classIri, result); + processAttributeFieldChanges(attributeChange, classIri, sb); } - return result.toString(); + return sb.toString(); } private void processAttributeFieldChanges( - SemanticAttributeChange attributeChange, String classIri, UpdateRequest result) { + SemanticAttributeChange attributeChange, String classIri, StringBuilder sb) { for (var change : attributeChange.getChanges()) { var update = switch (change.getSemanticFieldChangeType()) { @@ -142,14 +157,15 @@ private void processAttributeFieldChanges( default -> null; }; - if (update != null) { - result.add(update); + if (update != null && !update.isBlank()) { + sb.append(update).append("\n"); } } } private String generateUpdateForEnumEntry(SemanticEnumEntryChange enumEntryChange) { - var result = new UpdateRequest(); + var sb = new StringBuilder(); + appendComment(sb, enumEntryChange); var update = switch (enumEntryChange.getSemanticResourceChangeType()) { @@ -158,15 +174,16 @@ private String generateUpdateForEnumEntry(SemanticEnumEntryChange enumEntryChang default -> null; }; - if (update != null) { - result.add(update); + if (update != null && !update.isBlank()) { + sb.append(update).append("\n"); } - return result.toString(); + return sb.toString(); } private String generateUpdateForAssociation(SemanticAssociationChange associationChange) { - var result = new UpdateRequest(); + var sb = new StringBuilder(); + appendComment(sb, associationChange); var update = switch (associationChange.getSemanticResourceChangeType()) { @@ -180,21 +197,21 @@ private String generateUpdateForAssociation(SemanticAssociationChange associatio default -> null; }; - if (update != null) { - result.add(update); + if (update != null && !update.isBlank()) { + sb.append(update).append("\n"); } var type = associationChange.getSemanticResourceChangeType(); if (type == SemanticResourceChangeType.CHANGE || type == SemanticResourceChangeType.RENAME) { - processAssociationFieldChanges(associationChange, result); + processAssociationFieldChanges(associationChange, sb); } - return result.toString(); + return sb.toString(); } private void processAssociationFieldChanges( - SemanticAssociationChange associationChange, UpdateRequest result) { + SemanticAssociationChange associationChange, StringBuilder sb) { for (var change : associationChange.getChanges()) { var update = switch (change.getSemanticFieldChangeType()) { @@ -208,8 +225,16 @@ private void processAssociationFieldChanges( default -> null; }; - if (update != null) { - result.add(update); + if (update != null && !update.isBlank()) { + sb.append(update).append("\n"); + } + } + } + + private void appendComment(StringBuilder sb, SemanticResourceChange resourceChange) { + if (resourceChange.getComment() != null && !resourceChange.getComment().isBlank()) { + for (var line : resourceChange.getComment().split("\\R")) { + sb.append("# ").append(line).append("\n"); } } } diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/defaults/InheritanceChangeHandler.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/defaults/InheritanceChangeHandler.java index 146e5e1f..19c27dac 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/defaults/InheritanceChangeHandler.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/defaults/InheritanceChangeHandler.java @@ -242,19 +242,25 @@ private void addNewInheritedProperties( property, SemanticResourceChangeType.ADDED_FROM_INHERITANCE); propertyChange.setLabel(new URI(property.getURI()).getSuffix()); if (CIMPropertyUtils.isAttribute(property)) { - var domain = property.getProperty(RDFS.domain).getResource().getProperty(RDFS.label).getResource().getURI(); + var domain = + property.getProperty(RDFS.domain) + .getResource() + .getProperty(RDFS.label) + .getResource() + .getURI(); var attributeChange = new SemanticAttributeChange(propertyChange); attributeChange.setInheritedFrom(domain); - derivingClassChange - .getAttributes() - .add(attributeChange); + derivingClassChange.getAttributes().add(attributeChange); } else if (CIMPropertyUtils.isAssociation(property)) { - var domain = property.getProperty(RDFS.domain).getResource().getProperty(RDFS.label).getResource().getURI(); + var domain = + property.getProperty(RDFS.domain) + .getResource() + .getProperty(RDFS.label) + .getResource() + .getURI(); var associationChange = new SemanticAssociationChange(propertyChange); associationChange.setInheritedFrom(domain); - derivingClassChange - .getAssociations() - .add(associationChange); + derivingClassChange.getAssociations().add(associationChange); } } } diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/renamings/RenameDetector.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/renamings/RenameDetector.java index e5a5cb0f..6c4b1e3e 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/renamings/RenameDetector.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/renamings/RenameDetector.java @@ -80,14 +80,17 @@ private List> matchBySimil Set unmatchedAdded = new HashSet<>(added); for (T deletedItem : deleted) { - double bestScore = 0.0; + var bestScore = 0.; T bestMatch = null; for (T newItem : unmatchedAdded) { + double score; if (newItem.getLabel().equals(deletedItem.getLabel())) { - + score = 1.; + } else { + score = SimilarityCalculator.calculateSimilarity(newItem, deletedItem); } - double score = SimilarityCalculator.calculateSimilarity(newItem, deletedItem); + if (score > bestScore) { bestScore = score; bestMatch = newItem; diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/renamings/RenameObjectBuilder.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/renamings/RenameObjectBuilder.java index 4ba636bf..0c7dd82c 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/renamings/RenameObjectBuilder.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/renamings/RenameObjectBuilder.java @@ -163,6 +163,18 @@ private SemanticFieldChange findMatchingChange( .orElse(null); } + if (target.getSemanticFieldChangeType() == SemanticFieldChangeType.STEREOTYPE_REMOVED + || target.getSemanticFieldChangeType() == SemanticFieldChangeType.MADE_ABSTRACT) { + return changes.stream() + .filter( + c -> + c.getSemanticFieldChangeType() + == SemanticFieldChangeType.STEREOTYPE_ADDED) + .filter(c -> Objects.equals(c.getTo(), target.getFrom())) + .findFirst() + .orElse(null); + } + return changes.stream() .filter( change -> diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/renamings/SimilarityCalculator.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/renamings/SimilarityCalculator.java index a9ab130b..9f1704ca 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/renamings/SimilarityCalculator.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/renamings/SimilarityCalculator.java @@ -27,6 +27,8 @@ import org.rdfarchitect.models.changes.semanticchanges.SemanticFieldChange; import org.rdfarchitect.models.changes.semanticchanges.SemanticFieldChangeType; import org.rdfarchitect.models.changes.semanticchanges.SemanticResourceChange; +import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; +import org.rdfarchitect.models.cim.relations.model.properties.CIMPropertyUtils; import java.util.ArrayList; import java.util.HashMap; @@ -192,7 +194,14 @@ private double compareFieldValues( getValueForChangeType(added, changeType) .map(SemanticFieldChange::getTo) .orElse(null); - return Objects.equals(deletedValue, addedValue) ? 1.0 : 0.0; + + if (changeType == SemanticFieldChangeType.MULTIPLICITY_CHANGE && deletedValue != null && addedValue != null) { + var oldMultiplicity = CIMPropertyUtils.resolveMultiplicity(new URI(deletedValue).getSuffix()); + var newMultiplicity = CIMPropertyUtils.resolveMultiplicity(new URI(addedValue).getSuffix()); + return Objects.equals(oldMultiplicity, newMultiplicity) ? 1.0 : 0.0; + } else { + return Objects.equals(deletedValue, addedValue) ? 1.0 : 0.0; + } } private Optional getValueForChangeType( diff --git a/frontend/src/lib/utils/migrationUtils.js b/frontend/src/lib/utils/migrationUtils.js new file mode 100644 index 00000000..72cc1996 --- /dev/null +++ b/frontend/src/lib/utils/migrationUtils.js @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { URI } from "$lib/models/dto/index.ts"; + +export function isPrefixOnlyRename(rename) { + if (!rename.newResource) { + return false; + } + const oldName = new URI(rename.oldResource.iri).suffix; + const newName = new URI(rename.newResource.iri).suffix; + return oldName === newName; +} diff --git a/frontend/src/routes/migrate/steps/ConfirmClassRenames.svelte b/frontend/src/routes/migrate/steps/ConfirmClassRenames.svelte index e93b4d2a..bb716056 100644 --- a/frontend/src/routes/migrate/steps/ConfirmClassRenames.svelte +++ b/frontend/src/routes/migrate/steps/ConfirmClassRenames.svelte @@ -17,44 +17,56 @@
- In this step you can choose which artifacts you want to generate for the migration. You can generate a migration - package containing the SPARQL updates for automatically migrating your data as well as SHACL shapes for validating your data against the new schema. - You can also generate a markdown file based on the semantic changes configured during this migration process. + In this step you can choose which artifacts you want to generate for the + migration. You can generate a migration package containing the SPARQL + updates for automatically migrating your data as well as SHACL shapes + for validating your data against the new schema. You can also generate a + markdown file based on the semantic changes configured during this + migration process. Please note that the script generation might not be able to handle all @@ -95,12 +98,8 @@ class="border-border bg-input-default-background text-default-text focus:ring-border-select rounded-md border px-2 py-1 text-sm focus:ring-2 focus:outline-none w-64" bind:value={reportType} > - - + +
diff --git a/frontend/src/routes/migrate/steps/ResourceChangeCard.svelte b/frontend/src/routes/migrate/steps/ResourceChangeCard.svelte new file mode 100644 index 00000000..5f4fa275 --- /dev/null +++ b/frontend/src/routes/migrate/steps/ResourceChangeCard.svelte @@ -0,0 +1,184 @@ + + + + +
+
+
+ {#if title} + {title} + {/if} +

+ {resource.label ?? resource.iri} +

+
+ + {RESOURCE_CHANGE_LABELS[resource.semanticResourceChangeType] ?? + resource.semanticResourceChangeType ?? + "—"} + +
+ + {#if ignorePrefixes && resource.oldIRI && newLabel !== oldLabel} +
+ + {ignorePrefixes ? oldLabel : resource.oldIRI} + + + + {ignorePrefixes ? newLabel : resource.iri} + +
+ {/if} + + {#if resource.changes && resource.changes.length > 0 && resource.semanticResourceChangeType !== "DELETE"} +
+ Changes + {#if expanded} + + {:else} + + {/if} +
+ {#if expanded} +
    + {#each resource.changes as change} +
  • + + {getChangeLabel()} + +
    + + {change.from ?? "—"} + + + + {change.to ?? "—"} + +
    +
  • + {/each} +
+ {/if} + {/if} + +
+
Comment
+ +
+
diff --git a/frontend/src/routes/migrate/steps/ReviewChanges.svelte b/frontend/src/routes/migrate/steps/ReviewChanges.svelte new file mode 100644 index 00000000..9dd701e4 --- /dev/null +++ b/frontend/src/routes/migrate/steps/ReviewChanges.svelte @@ -0,0 +1,162 @@ + + + + +
+ {#if isLoading} +

Loading changes…

+ {:else if classes.length === 0} + + {:else} + {#each classes as cls, i} +
+ + + {#if cls.attributes?.length > 0} +
+

+ Attributes +

+ {#each cls.attributes as attribute (attribute.iri)} + {#if attribute.semanticResourceChangeType !== "DELETED_FROM_INHERITANCE" && attribute.semanticResourceChangeType !== "ADDED_FROM_INHERITANCE"} + + {/if} + {/each} +
+ {/if} + + {#if cls.associations?.length > 0} +
+

+ Associations +

+ {#each cls.associations as association (association.iri)} + {#if association.semanticResourceChangeType !== "DELETED_FROM_INHERITANCE" && association.semanticResourceChangeType !== "ADDED_FROM_INHERITANCE"} + + {/if} + {/each} +
+ {/if} + + {#if cls.enumEntries?.length > 0} +
+

+ Enum Entries +

+ {#each cls.enumEntries as enumEntry (enumEntry.iri)} + + {/each} +
+ {/if} +
+ {/each} + {/if} +
diff --git a/frontend/src/routes/migrate/steps/SelectSchemas.svelte b/frontend/src/routes/migrate/steps/SelectSchemas.svelte index 358acde9..da380f34 100644 --- a/frontend/src/routes/migrate/steps/SelectSchemas.svelte +++ b/frontend/src/routes/migrate/steps/SelectSchemas.svelte @@ -219,7 +219,9 @@
-
+
diff --git a/frontend/src/routes/migrate/steps/propertyrenames/ConfirmAssociationRenames.svelte b/frontend/src/routes/migrate/steps/propertyrenames/ConfirmAssociationRenames.svelte index bec87a4d..7b7ae1d1 100644 --- a/frontend/src/routes/migrate/steps/propertyrenames/ConfirmAssociationRenames.svelte +++ b/frontend/src/routes/migrate/steps/propertyrenames/ConfirmAssociationRenames.svelte @@ -90,7 +90,6 @@ return ( cls.associations && cls.associations.added.length + - cls.associations.modified.length + cls.associations.deletedAndRenamed.length > 0 ); @@ -179,21 +178,6 @@
{/if} - - {#if cls.associations.modified.length > 0} -
-

- Modified Associations -

-
    - {#each cls.associations.modified as modifiedAssociation} -
  • {modifiedAssociation.label}
  • - {/each} -
-
- {/if}
diff --git a/frontend/src/routes/migrate/steps/propertyrenames/ConfirmAttributeRenames.svelte b/frontend/src/routes/migrate/steps/propertyrenames/ConfirmAttributeRenames.svelte index 99c61f4e..b27eff5d 100644 --- a/frontend/src/routes/migrate/steps/propertyrenames/ConfirmAttributeRenames.svelte +++ b/frontend/src/routes/migrate/steps/propertyrenames/ConfirmAttributeRenames.svelte @@ -89,7 +89,6 @@ return ( cls.attributes && cls.attributes.added.length + - cls.attributes.modified.length + cls.attributes.deletedAndRenamed.length > 0 ); @@ -180,21 +179,6 @@
{/if} - - {#if cls.attributes.modified.length > 0} -
-

- Modified Attributes -

-
    - {#each cls.attributes.modified as modifiedAttribute} -
  • {modifiedAttribute.label}
  • - {/each} -
-
- {/if}
diff --git a/frontend/src/routes/migrate/steps/propertyrenames/ConfirmEnumEntryRenames.svelte b/frontend/src/routes/migrate/steps/propertyrenames/ConfirmEnumEntryRenames.svelte index 4ede6dfa..e8c56968 100644 --- a/frontend/src/routes/migrate/steps/propertyrenames/ConfirmEnumEntryRenames.svelte +++ b/frontend/src/routes/migrate/steps/propertyrenames/ConfirmEnumEntryRenames.svelte @@ -90,7 +90,6 @@ return ( cls.enumEntries && cls.enumEntries.added.length + - cls.enumEntries.modified.length + cls.enumEntries.deletedAndRenamed.length > 0 ); @@ -179,21 +178,6 @@
{/if} - - {#if cls.enumEntries.modified.length > 0} -
-

- Modified Enum Entries -

-
    - {#each cls.enumEntries.modified as modifiedEnumEntry} -
  • {modifiedEnumEntry.label}
  • - {/each} -
-
- {/if} From 2f5024ec6d28f54eca9e10d2849771762834baf5 Mon Sep 17 00:00:00 2001 From: tarn Date: Wed, 8 Jul 2026 16:43:31 +0200 Subject: [PATCH 4/8] lint --- .../MarkdownMigrationReportBuilder.java | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MarkdownMigrationReportBuilder.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MarkdownMigrationReportBuilder.java index 3e214569..cebd1683 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MarkdownMigrationReportBuilder.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MarkdownMigrationReportBuilder.java @@ -148,6 +148,9 @@ private void appendStatsSummary(StringBuilder sb, List clas case ADD -> added++; case DELETE -> deleted++; case CHANGE, RENAME -> changed++; + default -> { + // changes from inheritance should not be included in the stat summary + } } } @@ -192,7 +195,10 @@ private void appendClassSection(StringBuilder sb, SemanticClassChange classChang private void appendFieldChangesAsSentences( StringBuilder sb, List changes) { - if (changes == null || changes.isEmpty()) return; + if (changes == null || changes.isEmpty()) { + return; + } + for (var change : changes) { sb.append("- ").append(fieldChangeToSentence(change)).append("\n"); } @@ -241,7 +247,10 @@ private String formatTransition(String description, String from, String to) { private void appendPropertySection( StringBuilder sb, String title, List properties) { - if (properties == null || properties.isEmpty()) return; + if (properties == null || properties.isEmpty()) { + return; + } + sb.append("### ").append(title).append("\n\n"); for (var prop : properties) { @@ -271,7 +280,10 @@ private void appendPropertySection( private void appendDirectProperties( StringBuilder sb, String title, List properties) { - if (properties == null) return; + if (properties == null) { + return; + } + var direct = properties.stream() .filter( @@ -283,17 +295,24 @@ private void appendDirectProperties( != SemanticResourceChangeType .DELETED_FROM_INHERITANCE) .toList(); - if (direct.isEmpty()) return; + if (direct.isEmpty()) { + return; + } appendPropertySection(sb, title, direct); } private boolean hasDirectChange(SemanticClassChange c) { - if (!c.getChanges().isEmpty()) return true; + if (!c.getChanges().isEmpty()) { + return true; + } var allProperties = new ArrayList(c.getAssociations()); allProperties.addAll(c.getAttributes()); allProperties.addAll(c.getEnumEntries()); - if (allProperties.isEmpty()) return false; + if (allProperties.isEmpty()) { + return false; + } + return allProperties.stream() .anyMatch( p -> @@ -324,7 +343,10 @@ private Set collectDirectPropertyLabels(SemanticClassChange classChange) private void addDirectLabels( Set labels, List properties) { - if (properties == null) return; + if (properties == null) { + return; + } + properties.stream() .filter( p -> @@ -345,7 +367,10 @@ private boolean hasMatchingInheritedProperty( private boolean matchesAny( List properties, Set labels) { - if (properties == null) return false; + if (properties == null) { + return false; + } + return properties.stream() .filter( p -> @@ -369,7 +394,9 @@ private String formatChangeType(SemanticResourceChangeType type) { } private String shorten(String value) { - if (value == null) return null; + if (value == null) { + return null; + } var shortened = defaultPrefixes.shortForm(value); if (shortened.equals(value)) { @@ -403,7 +430,9 @@ private SemanticClassChange downgradeIfPrefixOnlyRename(SemanticClassChange clas } private List filterProperties(List properties) { - if (properties == null) return Collections.emptyList(); + if (properties == null) { + return Collections.emptyList(); + } return properties.stream() .map( From d64ef8465d0e5e1811c003c27f1bf38ebedc8c1b Mon Sep 17 00:00:00 2001 From: tarn Date: Wed, 8 Jul 2026 16:56:01 +0200 Subject: [PATCH 5/8] code quality test --- .../schemamigration/SchemaMigrationService.java | 4 +--- .../artifacts/MarkdownMigrationReportBuilder.java | 2 +- .../artifacts/MigrationReportBuilder.java | 2 +- .../SparqlUpdateGeneratorTest.java | 14 ++++++++------ 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/SchemaMigrationService.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/SchemaMigrationService.java index 6ebed807..bf920ca8 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/SchemaMigrationService.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/SchemaMigrationService.java @@ -392,7 +392,6 @@ public String generateDetailedMigrationReport() { @Override public String generateSummaryMigrationReport() { - var graph = migrationSessionStore.getContext().getOriginalSchema(); var changes = migrationSessionStore.getContext().getDiffAfterDefaultValueConfirm(); boolean ignorePrefixes = migrationSessionStore.getContext().isIgnorePrefixes(); @@ -401,8 +400,7 @@ public String generateSummaryMigrationReport() { newChangeList.add(new SemanticClassChange(change)); } - return migrationReportBuilder.generateSummaryMigrationReport( - newChangeList, graph, ignorePrefixes); + return migrationReportBuilder.generateSummaryMigrationReport(newChangeList, ignorePrefixes); } @Override diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MarkdownMigrationReportBuilder.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MarkdownMigrationReportBuilder.java index cebd1683..10cc5369 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MarkdownMigrationReportBuilder.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MarkdownMigrationReportBuilder.java @@ -89,7 +89,7 @@ public String generateDetailedMigrationReport( @Override public String generateSummaryMigrationReport( - List classChanges, Graph originalGraph, boolean ignorePrefixes) { + List classChanges, boolean ignorePrefixes) { var visibleChanges = ignorePrefixes ? applyPrefixRenameFilter(classChanges) : classChanges; var sb = new StringBuilder(); diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MigrationReportBuilder.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MigrationReportBuilder.java index 5dea8033..1235f871 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MigrationReportBuilder.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/artifacts/MigrationReportBuilder.java @@ -27,5 +27,5 @@ String generateDetailedMigrationReport( List classChanges, Graph originalGraph, boolean ignorePrefixes); String generateSummaryMigrationReport( - List classChanges, Graph originalGraph, boolean ignorePrefixes); + List classChanges, boolean ignorePrefixes); } diff --git a/backend/src/test/java/org/rdfarchitect/services/schemamigration/scriptgeneration/SparqlUpdateGeneratorTest.java b/backend/src/test/java/org/rdfarchitect/services/schemamigration/scriptgeneration/SparqlUpdateGeneratorTest.java index 1e55e9ad..d410a8f6 100644 --- a/backend/src/test/java/org/rdfarchitect/services/schemamigration/scriptgeneration/SparqlUpdateGeneratorTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/schemamigration/scriptgeneration/SparqlUpdateGeneratorTest.java @@ -36,6 +36,7 @@ import org.rdfarchitect.models.changes.semanticchanges.SemanticAttributeChange; import org.rdfarchitect.models.cim.rdf.resources.CIMS; import org.rdfarchitect.models.cim.rdf.resources.CIMStereotypes; +import org.rdfarchitect.services.schemamigration.artifacts.SparqlUpdateGenerator; @ExtendWith(MockitoExtension.class) class SparqlUpdateGeneratorTest { @@ -70,8 +71,9 @@ void generateAddAttributeUpdate_concreteClassWithSubclass_updatesClassAndAllSubc generator.generateAddAttributeUpdate(attributeChange, PREFIX + "DiagramObject"); // The declaring class itself must be migrated, not only its deriving subclass. - assertThat(script).contains(PREFIX + "DiagramObject>"); - assertThat(script).contains(PREFIX + "TextDiagramObject>"); + assertThat(script) + .contains(PREFIX + "DiagramObject>") + .contains(PREFIX + "TextDiagramObject>"); } @Test @@ -86,8 +88,7 @@ void generateAddAttributeUpdate_leafConcreteClass_updatesThatClass() { var script = generator.generateAddAttributeUpdate(attributeChange, PREFIX + "TextDiagramObject"); - assertThat(script).isNotBlank(); - assertThat(script).contains(PREFIX + "TextDiagramObject>"); + assertThat(script).isNotBlank().contains(PREFIX + "TextDiagramObject>"); } @Test @@ -103,8 +104,9 @@ void generateAddAttributeUpdate_abstractClass_onlyUpdatesConcreteSubclasses() { var script = generator.generateAddAttributeUpdate(attributeChange, PREFIX + "DiagramObject"); - assertThat(script).contains(PREFIX + "TextDiagramObject>"); - assertThat(script).doesNotContain(PREFIX + "DiagramObject>"); + assertThat(script) + .contains(PREFIX + "TextDiagramObject>") + .doesNotContain(PREFIX + "DiagramObject>"); } @Test From 9bdaccd5c25af183fff0708d0a22f83e451f405c Mon Sep 17 00:00:00 2001 From: tarn Date: Wed, 8 Jul 2026 17:14:23 +0200 Subject: [PATCH 6/8] fix tests --- .../SemanticAssociationChange.java | 2 - .../SemanticAttributeChange.java | 3 -- .../defaults/InheritanceChangeHandler.java | 15 ------- .../renamings/SimilarityCalculatorTest.java | 40 +++++++++---------- 4 files changed, 20 insertions(+), 40 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticAssociationChange.java b/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticAssociationChange.java index 252627ec..e24fb781 100644 --- a/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticAssociationChange.java +++ b/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticAssociationChange.java @@ -42,8 +42,6 @@ public final class SemanticAssociationChange extends SemanticResourceChange { private boolean associationUsed; - private String inheritedFrom; - public SemanticAssociationChange(TripleResourceChange tripleChange) { super(tripleChange); } diff --git a/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticAttributeChange.java b/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticAttributeChange.java index 877850d3..aea4f984 100644 --- a/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticAttributeChange.java +++ b/backend/src/main/java/org/rdfarchitect/models/changes/semanticchanges/SemanticAttributeChange.java @@ -51,8 +51,6 @@ public final class SemanticAttributeChange extends SemanticResourceChange { private boolean forceDefaultValue = false; - private String inheritedFrom; - @Builder.Default private List allowedValues = new ArrayList<>(); public SemanticAttributeChange(TripleResourceChange tripleChange) { @@ -68,7 +66,6 @@ public SemanticAttributeChange(SemanticAttributeChange other) { optional = other.isOptional(); forceDefaultValue = other.isForceDefaultValue(); allowedValues = new ArrayList<>(other.getAllowedValues()); - inheritedFrom = other.getInheritedFrom(); } public SemanticAttributeChange(SemanticResourceChange resourceChange) { diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/defaults/InheritanceChangeHandler.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/defaults/InheritanceChangeHandler.java index 19c27dac..4ffe0ece 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/defaults/InheritanceChangeHandler.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/defaults/InheritanceChangeHandler.java @@ -21,7 +21,6 @@ import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Resource; -import org.apache.jena.vocabulary.RDFS; import org.rdfarchitect.models.changes.RenameCandidate; import org.rdfarchitect.models.changes.semanticchanges.SemanticAssociationChange; import org.rdfarchitect.models.changes.semanticchanges.SemanticAttributeChange; @@ -242,24 +241,10 @@ private void addNewInheritedProperties( property, SemanticResourceChangeType.ADDED_FROM_INHERITANCE); propertyChange.setLabel(new URI(property.getURI()).getSuffix()); if (CIMPropertyUtils.isAttribute(property)) { - var domain = - property.getProperty(RDFS.domain) - .getResource() - .getProperty(RDFS.label) - .getResource() - .getURI(); var attributeChange = new SemanticAttributeChange(propertyChange); - attributeChange.setInheritedFrom(domain); derivingClassChange.getAttributes().add(attributeChange); } else if (CIMPropertyUtils.isAssociation(property)) { - var domain = - property.getProperty(RDFS.domain) - .getResource() - .getProperty(RDFS.label) - .getResource() - .getURI(); var associationChange = new SemanticAssociationChange(propertyChange); - associationChange.setInheritedFrom(domain); derivingClassChange.getAssociations().add(associationChange); } } diff --git a/backend/src/test/java/org/rdfarchitect/services/schemamigration/renamings/SimilarityCalculatorTest.java b/backend/src/test/java/org/rdfarchitect/services/schemamigration/renamings/SimilarityCalculatorTest.java index ca678a02..219c454e 100644 --- a/backend/src/test/java/org/rdfarchitect/services/schemamigration/renamings/SimilarityCalculatorTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/schemamigration/renamings/SimilarityCalculatorTest.java @@ -344,8 +344,8 @@ void calculateSimilarity_attributesWithSameMultiplicity_contributesToScore() { List.of( ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.MULTIPLICITY_CHANGE, - "0..1", - "0..1"))) + "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..1", + "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..1"))) .build(); var added = @@ -356,8 +356,8 @@ void calculateSimilarity_attributesWithSameMultiplicity_contributesToScore() { List.of( ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.MULTIPLICITY_CHANGE, - "0..1", - "0..1"))) + "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..1", + "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..1"))) .build(); var similarity = SimilarityCalculator.calculateSimilarity(added, deleted); @@ -410,12 +410,12 @@ void calculateSimilarity_attributesWithAllMatchingProperties_returnsVeryHighScor "Float"), ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.MULTIPLICITY_CHANGE, - "1..1", - "1..1"), + "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:1..1", + "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:1..1"), ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.DEFAULT_VALUE_CHANGE, - "0.0", - "0.0"))) + "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0.0", + "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0.0"))) .build(); var added = @@ -430,12 +430,12 @@ void calculateSimilarity_attributesWithAllMatchingProperties_returnsVeryHighScor "Float"), ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.MULTIPLICITY_CHANGE, - "1..1", - "1..1"), + "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:1..1", + "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:1..1"), ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.DEFAULT_VALUE_CHANGE, - "0.0", - "0.0"))) + "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0.0", + "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0.0"))) .build(); var similarity = SimilarityCalculator.calculateSimilarity(added, deleted); @@ -519,8 +519,8 @@ void calculateSimilarity_associationsWithSameMultiplicity_contributesToScore() { List.of( ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.MULTIPLICITY_CHANGE, - "0..*", - "0..*"))) + "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..*", + "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..*"))) .build(); var added = @@ -531,8 +531,8 @@ void calculateSimilarity_associationsWithSameMultiplicity_contributesToScore() { List.of( ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.MULTIPLICITY_CHANGE, - "0..*", - "0..*"))) + "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..*", + "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..*"))) .build(); var similarity = SimilarityCalculator.calculateSimilarity(added, deleted); @@ -554,8 +554,8 @@ void calculateSimilarity_associationsWithAllMatchingProperties_returnsVeryHighSc "Equipment"), ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.MULTIPLICITY_CHANGE, - "0..*", - "0..*"), + "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..*", + "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..*"), ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.ASSOCIATION_USED_CHANGE, "true", @@ -574,8 +574,8 @@ void calculateSimilarity_associationsWithAllMatchingProperties_returnsVeryHighSc "Equipment"), ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.MULTIPLICITY_CHANGE, - "0..*", - "0..*"), + "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..*", + "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..*"), ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.ASSOCIATION_USED_CHANGE, "true", From ad72d426b17bff09e3208ba353d666e3c8782d4c Mon Sep 17 00:00:00 2001 From: tarn Date: Wed, 15 Jul 2026 11:22:32 +0200 Subject: [PATCH 7/8] review changes --- .../MigrationChangesRESTController.java | 6 +-- .../renamings/SimilarityCalculatorTest.java | 42 ++++++++++--------- frontend/src/lib/api/backend.js | 28 +++++++++++++ .../src/routes/migrate/steps/Export.svelte | 14 ++----- .../routes/migrate/steps/ReviewChanges.svelte | 25 +++++------ .../routes/migrate/steps/SelectSchemas.svelte | 4 ++ 6 files changed, 72 insertions(+), 47 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationChangesRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationChangesRESTController.java index aaa7445d..90880c39 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationChangesRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationChangesRESTController.java @@ -78,7 +78,7 @@ public List getMigrationChanges( var classes = migrationChangesUseCase.getMigrationChanges(); logger.info( - "Sending response to GET request: \"/api/datasets/migrations/changes\" from \"{}\".", + "Sending response to GET request: \"/api/migrations/changes\" from \"{}\".", originURL); return classes; @@ -96,13 +96,13 @@ public void confirmMigrationChanges( @Parameter(description = "The updated class renamings") @RequestBody List changes) { logger.info( - "Received POST request: \"/api/migrations/class-renamings\" from \"{}\".", + "Received POST request: \"/api/migrations/changes\" from \"{}\".", originURL); migrationChangesUseCase.confirmMigrationChanges(changes); logger.info( - "Sending response to POST request: \"/api/migrations/class-renamings\" from \"{}\".", + "Sending response to POST request: \"/api/migrations/changes\" from \"{}\".", originURL); } } diff --git a/backend/src/test/java/org/rdfarchitect/services/schemamigration/renamings/SimilarityCalculatorTest.java b/backend/src/test/java/org/rdfarchitect/services/schemamigration/renamings/SimilarityCalculatorTest.java index 219c454e..4ae04c55 100644 --- a/backend/src/test/java/org/rdfarchitect/services/schemamigration/renamings/SimilarityCalculatorTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/schemamigration/renamings/SimilarityCalculatorTest.java @@ -26,6 +26,8 @@ import org.rdfarchitect.models.changes.semanticchanges.SemanticClassChange; import org.rdfarchitect.models.changes.semanticchanges.SemanticFieldChangeType; import org.rdfarchitect.models.changes.semanticchanges.SemanticResourceChangeType; +import org.rdfarchitect.models.cim.rdf.resources.CIMS; +import org.rdfarchitect.models.cim.rdf.resources.CIMStereotypes; import org.rdfarchitect.services.schemamigration.ChangeObjectTestBuilder; import java.util.List; @@ -344,8 +346,8 @@ void calculateSimilarity_attributesWithSameMultiplicity_contributesToScore() { List.of( ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.MULTIPLICITY_CHANGE, - "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..1", - "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..1"))) + CIMS.namespace + "M:0..1", + CIMS.namespace + "M:0..1"))) .build(); var added = @@ -356,8 +358,8 @@ void calculateSimilarity_attributesWithSameMultiplicity_contributesToScore() { List.of( ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.MULTIPLICITY_CHANGE, - "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..1", - "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..1"))) + CIMS.namespace + "M:0..1", + CIMS.namespace + "M:0..1"))) .build(); var similarity = SimilarityCalculator.calculateSimilarity(added, deleted); @@ -410,12 +412,12 @@ void calculateSimilarity_attributesWithAllMatchingProperties_returnsVeryHighScor "Float"), ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.MULTIPLICITY_CHANGE, - "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:1..1", - "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:1..1"), + CIMS.namespace + "M:1..1", + CIMS.namespace + "M:1..1"), ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.DEFAULT_VALUE_CHANGE, - "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0.0", - "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0.0"))) + CIMS.namespace + "M:0.0", + CIMS.namespace + "M:0.0"))) .build(); var added = @@ -430,12 +432,12 @@ void calculateSimilarity_attributesWithAllMatchingProperties_returnsVeryHighScor "Float"), ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.MULTIPLICITY_CHANGE, - "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:1..1", - "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:1..1"), + CIMS.namespace + "M:1..1", + CIMS.namespace + "M:1..1"), ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.DEFAULT_VALUE_CHANGE, - "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0.0", - "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0.0"))) + CIMS.namespace + "M:0.0", + CIMS.namespace + "M:0.0"))) .build(); var similarity = SimilarityCalculator.calculateSimilarity(added, deleted); @@ -519,8 +521,8 @@ void calculateSimilarity_associationsWithSameMultiplicity_contributesToScore() { List.of( ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.MULTIPLICITY_CHANGE, - "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..*", - "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..*"))) + CIMS.namespace + "M:0..*", + CIMS.namespace + "M:0..*"))) .build(); var added = @@ -531,8 +533,8 @@ void calculateSimilarity_associationsWithSameMultiplicity_contributesToScore() { List.of( ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.MULTIPLICITY_CHANGE, - "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..*", - "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..*"))) + CIMS.namespace + "M:0..*", + CIMS.namespace + "M:0..*"))) .build(); var similarity = SimilarityCalculator.calculateSimilarity(added, deleted); @@ -554,8 +556,8 @@ void calculateSimilarity_associationsWithAllMatchingProperties_returnsVeryHighSc "Equipment"), ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.MULTIPLICITY_CHANGE, - "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..*", - "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..*"), + CIMS.namespace + "M:0..*", + CIMS.namespace + "M:0..*"), ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.ASSOCIATION_USED_CHANGE, "true", @@ -574,8 +576,8 @@ void calculateSimilarity_associationsWithAllMatchingProperties_returnsVeryHighSc "Equipment"), ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.MULTIPLICITY_CHANGE, - "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..*", - "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#M:0..*"), + CIMS.namespace + "M:0..*", + CIMS.namespace + "M:0..*"), ChangeObjectTestBuilder.fieldChange( SemanticFieldChangeType.ASSOCIATION_USED_CHANGE, "true", diff --git a/frontend/src/lib/api/backend.js b/frontend/src/lib/api/backend.js index 69555506..c209bdd5 100644 --- a/frontend/src/lib/api/backend.js +++ b/frontend/src/lib/api/backend.js @@ -741,4 +741,32 @@ export class BackendConnection { credentials: "include", }); } + + async getChanges() { + const url = `${PUBLIC_BACKEND_URL}/migrations/changes`; + return await fetch(url, { + method: "GET", + credentials: "include", + headers: new Headers({ "Content-Type": "application/json" }), + }) + } + + async confirmChanges(classes) { + const url = `${PUBLIC_BACKEND_URL}/migrations/changes`; + return await fetch(url, { + method: "POST", + credentials: "include", + headers: new Headers({ "Content-Type": "application/json" }), + body: JSON.stringify(classes) + }) + } + + async fetchReport(reportType) { + const params = new URLSearchParams({ reportType: reportType }); + return fetch(PUBLIC_BACKEND_URL + `/migrations/report?${params}`, { + method: "GET", + headers: { "Content-Type": "application/json" }, + credentials: "include", + }); + } } diff --git a/frontend/src/routes/migrate/steps/Export.svelte b/frontend/src/routes/migrate/steps/Export.svelte index c517498d..e274b56d 100644 --- a/frontend/src/routes/migrate/steps/Export.svelte +++ b/frontend/src/routes/migrate/steps/Export.svelte @@ -16,6 +16,7 @@ -->
diff --git a/frontend/src/routes/migrate/steps/ReviewChanges.svelte b/frontend/src/routes/migrate/steps/ReviewChanges.svelte index 9dd701e4..28651175 100644 --- a/frontend/src/routes/migrate/steps/ReviewChanges.svelte +++ b/frontend/src/routes/migrate/steps/ReviewChanges.svelte @@ -18,13 +18,17 @@ import { onMount } from "svelte"; import { get } from "svelte/store"; + import { BackendConnection } from "$lib/api/backend.js"; import EmptyStateCard from "$lib/components/EmptyStateCard.svelte"; + import InfoBox from "$lib/components/InfoBox.svelte"; import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; import { toastStore } from "$lib/eventhandling/toastStore.svelte.js"; import { migrationState } from "$lib/sharedState.svelte.js"; import ResourceChangeCard from "./ResourceChangeCard.svelte"; + const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); + let classes = $state([]); let isLoading = $state(true); let ignorePrefixes = $state(false); @@ -33,11 +37,7 @@ let storedState = get(migrationState); ignorePrefixes = storedState.ignorePrefixes; - fetch(PUBLIC_BACKEND_URL + "/migrations/changes", { - method: "GET", - headers: { "Content-Type": "application/json" }, - credentials: "include", - }) + bec.getChanges() .then(res => (res.ok ? res.json() : Promise.reject("Failed"))) .then(data => { classes = data.sort((a, b) => { @@ -52,15 +52,7 @@ export async function onNext() { try { - const res = await fetch( - PUBLIC_BACKEND_URL + "/migrations/changes", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - credentials: "include", - body: JSON.stringify(classes), - }, - ); + const res = await bec.confirmChanges(classes) if (!res.ok) { toastStore.error( "Save failed", @@ -78,6 +70,11 @@
+ + This step lets you get an overview over all changes made in the schema update and lets you assign comments to those changes. + These comments will be inserted in the migration script and the migration report you can download in the next step. + + {#if isLoading}

Loading changes…

{:else if classes.length === 0} diff --git a/frontend/src/routes/migrate/steps/SelectSchemas.svelte b/frontend/src/routes/migrate/steps/SelectSchemas.svelte index da380f34..75e52d26 100644 --- a/frontend/src/routes/migrate/steps/SelectSchemas.svelte +++ b/frontend/src/routes/migrate/steps/SelectSchemas.svelte @@ -216,6 +216,10 @@ system, or you can upload one or two files containing the schema(s) you want to migrate.

+

+ You can toggle the flag "ignore prefixes" which cause the rename detection to only consider labels and not the full IRI. + Renames due to a prefix change will therefore be shown as a simple change in the UI. +

From 9001aa27b8c6d3dc675ca8faf7fb44f0309a7805 Mon Sep 17 00:00:00 2001 From: tarn Date: Wed, 15 Jul 2026 11:26:55 +0200 Subject: [PATCH 8/8] lint --- .../graphs/migration/MigrationChangesRESTController.java | 4 +--- .../renamings/SimilarityCalculatorTest.java | 1 - frontend/src/lib/api/backend.js | 6 +++--- frontend/src/routes/migrate/steps/ReviewChanges.svelte | 8 +++++--- frontend/src/routes/migrate/steps/SelectSchemas.svelte | 6 ++++-- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationChangesRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationChangesRESTController.java index 90880c39..e310b691 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationChangesRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationChangesRESTController.java @@ -95,9 +95,7 @@ public void confirmMigrationChanges( String originURL, @Parameter(description = "The updated class renamings") @RequestBody List changes) { - logger.info( - "Received POST request: \"/api/migrations/changes\" from \"{}\".", - originURL); + logger.info("Received POST request: \"/api/migrations/changes\" from \"{}\".", originURL); migrationChangesUseCase.confirmMigrationChanges(changes); diff --git a/backend/src/test/java/org/rdfarchitect/services/schemamigration/renamings/SimilarityCalculatorTest.java b/backend/src/test/java/org/rdfarchitect/services/schemamigration/renamings/SimilarityCalculatorTest.java index 4ae04c55..7193ceca 100644 --- a/backend/src/test/java/org/rdfarchitect/services/schemamigration/renamings/SimilarityCalculatorTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/schemamigration/renamings/SimilarityCalculatorTest.java @@ -27,7 +27,6 @@ import org.rdfarchitect.models.changes.semanticchanges.SemanticFieldChangeType; import org.rdfarchitect.models.changes.semanticchanges.SemanticResourceChangeType; import org.rdfarchitect.models.cim.rdf.resources.CIMS; -import org.rdfarchitect.models.cim.rdf.resources.CIMStereotypes; import org.rdfarchitect.services.schemamigration.ChangeObjectTestBuilder; import java.util.List; diff --git a/frontend/src/lib/api/backend.js b/frontend/src/lib/api/backend.js index c209bdd5..a256d813 100644 --- a/frontend/src/lib/api/backend.js +++ b/frontend/src/lib/api/backend.js @@ -748,7 +748,7 @@ export class BackendConnection { method: "GET", credentials: "include", headers: new Headers({ "Content-Type": "application/json" }), - }) + }); } async confirmChanges(classes) { @@ -757,8 +757,8 @@ export class BackendConnection { method: "POST", credentials: "include", headers: new Headers({ "Content-Type": "application/json" }), - body: JSON.stringify(classes) - }) + body: JSON.stringify(classes), + }); } async fetchReport(reportType) { diff --git a/frontend/src/routes/migrate/steps/ReviewChanges.svelte b/frontend/src/routes/migrate/steps/ReviewChanges.svelte index 28651175..a151efea 100644 --- a/frontend/src/routes/migrate/steps/ReviewChanges.svelte +++ b/frontend/src/routes/migrate/steps/ReviewChanges.svelte @@ -52,7 +52,7 @@ export async function onNext() { try { - const res = await bec.confirmChanges(classes) + const res = await bec.confirmChanges(classes); if (!res.ok) { toastStore.error( "Save failed", @@ -71,8 +71,10 @@
- This step lets you get an overview over all changes made in the schema update and lets you assign comments to those changes. - These comments will be inserted in the migration script and the migration report you can download in the next step. + This step lets you get an overview over all changes made in the schema + update and lets you assign comments to those changes. These comments + will be inserted in the migration script and the migration report you + can download in the next step. {#if isLoading} diff --git a/frontend/src/routes/migrate/steps/SelectSchemas.svelte b/frontend/src/routes/migrate/steps/SelectSchemas.svelte index 75e52d26..e18aa4a3 100644 --- a/frontend/src/routes/migrate/steps/SelectSchemas.svelte +++ b/frontend/src/routes/migrate/steps/SelectSchemas.svelte @@ -217,8 +217,10 @@ you want to migrate.

- You can toggle the flag "ignore prefixes" which cause the rename detection to only consider labels and not the full IRI. - Renames due to a prefix change will therefore be shown as a simple change in the UI. + You can toggle the flag "ignore prefixes" which cause the rename + detection to only consider labels and not the full IRI. Renames due + to a prefix change will therefore be shown as a simple change in the + UI.