From 6dc469a9ffcc1a31f6611e5778f4d9b5625b5dcc Mon Sep 17 00:00:00 2001 From: Maximilian Date: Wed, 1 Apr 2026 18:26:10 +0200 Subject: [PATCH 01/31] Created first structure --- .../graphs/DeletionImpactRESTController.java | 77 +++++++++++++ .../api/dto/delete/DeleteRelations.java | 22 ++++ .../api/dto/delete/ResourceDeleteRequest.java | 22 ++++ .../CIMResourceTypeIdentifyingUtils.java | 87 +++++++++++++++ .../delete/DeleteResourcesService.java | 30 +++++ .../delete/DeleteResourcesUseCase.java | 27 +++++ .../delete/FindOnDeleteRelationsService.java | 105 ++++++++++++++++++ .../delete/FindOnDeleteRelationsUseCase.java | 28 +++++ 8 files changed, 398 insertions(+) create mode 100644 backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeletionImpactRESTController.java create mode 100644 backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteRelations.java create mode 100644 backend/src/main/java/org/rdfarchitect/api/dto/delete/ResourceDeleteRequest.java create mode 100644 backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java create mode 100644 backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java create mode 100644 backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesUseCase.java create mode 100644 backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java create mode 100644 backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsUseCase.java diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeletionImpactRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeletionImpactRESTController.java new file mode 100644 index 00000000..0c5a44fe --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeletionImpactRESTController.java @@ -0,0 +1,77 @@ +/* + * 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.api.controller.datasets.graphs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.rdfarchitect.api.dto.delete.DeleteRelations; +import org.rdfarchitect.services.delete.FindOnDeleteRelationsUseCase; +import org.rdfarchitect.database.GraphIdentifier; +import org.rdfarchitect.services.ExpandURIUseCase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@RequestMapping("api/datasets/{datasetName}/graphs/{graphURI}/uuid/{uuid}/deletion-impact") +@RequiredArgsConstructor +public class DeletionImpactRESTController { + + private static final Logger logger = LoggerFactory.getLogger(DeletionImpactRESTController.class); + + private final ExpandURIUseCase expandURIUseCase; + private final FindOnDeleteRelationsUseCase findOnDeleteRelationsUseCase; + + //TODO: anpassen der beschreibung + @Operation( + summary = "resolve iri identifier", + description = "Resolve iri identifier of a cim resource to its uuid.", + tags = {"graph"}, + responses = { + @ApiResponse(responseCode = "200") + } + ) + @GetMapping + public DeleteRelations getDeletionImpact( + @Parameter(description = "The name/url of the inquirer.") + @RequestHeader(value = HttpHeaders.ORIGIN, required = false, defaultValue = "unknown") + String originURL, + @Parameter(description = "The literal name of the dataset.") + @PathVariable + String datasetName, + @Parameter(description = "The url encoded uri of the graph, or \"default\" to access the default graph.") + @PathVariable + String graphURI, + @Parameter(description = "The url encoded iri identifier of the cim resource.") + @PathVariable + String uuid) { + logger.info("Received GET request: \"/api/datasets/{{}}/graphs/{{}}/uuid/{{}/deletion-impact\" from \"{}\".", datasetName, graphURI, uuid, originURL); + + var extendedGraphURI = expandURIUseCase.expandUri(datasetName, graphURI); + + var resultObj = findOnDeleteRelationsUseCase.getDeleteRelations(new GraphIdentifier(datasetName, extendedGraphURI), UUID.fromString(uuid)); + + logger.info("Sending response to GET request: \"/api/datasets/{{}}/graphs/{{}}/uuid/{{}/deletion-impact\" from \"{}\".", datasetName, graphURI, uuid, originURL); + return resultObj; + } +} diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteRelations.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteRelations.java new file mode 100644 index 00000000..147279ec --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteRelations.java @@ -0,0 +1,22 @@ +/* + * 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.api.dto.delete; + +public class DeleteRelations { + +} diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/ResourceDeleteRequest.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/ResourceDeleteRequest.java new file mode 100644 index 00000000..c4bfa953 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/ResourceDeleteRequest.java @@ -0,0 +1,22 @@ +/* + * 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.api.dto.delete; + +public class ResourceDeleteRequest { + +} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java new file mode 100644 index 00000000..df862688 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java @@ -0,0 +1,87 @@ +/* + * 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.cim.relations.model; + +import lombok.experimental.UtilityClass; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.OWL2; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import org.rdfarchitect.models.cim.rdf.resources.CIMS; +import org.rdfarchitect.models.cim.rdf.resources.CIMStereotypes; +import org.rdfarchitect.models.cim.rdf.resources.RDFA; +import org.rdfarchitect.models.cim.relations.model.properties.CIMPropertyUtils; + +import java.util.List; +import java.util.UUID; +import java.util.function.Predicate; + +@UtilityClass +public class CIMResourceTypeIdentifyingUtils { + + public enum CimResourceType { + PACKAGE, + CLASS, + ATTRIBUTE, + ASSOCIATION, + ENUM_ENTRY, + ONTOLOGY, + UNKNOWN, + } + + private record TypeRule(Predicate matches, CimResourceType type) { + + } + + private static final List TYPE_RULES = List.of( + new TypeRule(s -> s.hasProperty(RDF.type, CIMS.classCategory), CimResourceType.PACKAGE), + new TypeRule(s -> s.hasProperty(RDF.type, RDFS.Class), CimResourceType.CLASS), + new TypeRule(s -> s.hasProperty(RDF.type, OWL2.Ontology), CimResourceType.ONTOLOGY), + new TypeRule(CIMPropertyUtils::isAttribute, CimResourceType.ATTRIBUTE), + new TypeRule(CIMPropertyUtils::isAssociation, CimResourceType.ASSOCIATION), + new TypeRule(CIMResourceTypeIdentifyingUtils::isEnumEntry, CimResourceType.ENUM_ENTRY) + ); + + public CimResourceType getType(Model model, UUID uuid) { + var subject = findUniqueSubject(model, uuid); + + return TYPE_RULES.stream() + .filter(rule -> rule.matches().test(subject)) + .map(TypeRule::type) + .findFirst() + .orElse(CimResourceType.UNKNOWN); + } + + private Resource findUniqueSubject(Model model, UUID uuid) { + var subjects = model.listSubjectsWithProperty(RDFA.uuid, uuid.toString()).toList(); + if (subjects.size() != 1) { + throw new IllegalArgumentException("Expected exactly one subject with UUID " + uuid + ", but found " + subjects.size()); + } + return subjects.getFirst(); + } + + private boolean isEnumEntry(Resource subject) { + var types = subject.listProperties(RDF.type).toList(); + if (types.size() != 1 || !types.getFirst().getObject().isURIResource()) { + return false; + } + return types.getFirst().getObject().asResource() + .hasProperty(CIMS.stereotype, CIMStereotypes.enumeration); + } +} diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java new file mode 100644 index 00000000..21c39b4c --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java @@ -0,0 +1,30 @@ +/* + * 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.delete; + +import org.rdfarchitect.api.dto.delete.ResourceDeleteRequest; + +import java.util.List; + +public class DeleteResourcesService implements DeleteResourcesUseCase { + + @Override + public void deleteResources(String graphIdentifier, List deleteRequests) { + //TODO: impl + } +} diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesUseCase.java b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesUseCase.java new file mode 100644 index 00000000..55f158e6 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesUseCase.java @@ -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. + * + */ + +package org.rdfarchitect.services.delete; + +import org.rdfarchitect.api.dto.delete.ResourceDeleteRequest; + +import java.util.List; + +public interface DeleteResourcesUseCase { + + void deleteResources(String graphIdentifier, List deleteRequests); +} diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java b/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java new file mode 100644 index 00000000..a7953f3a --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java @@ -0,0 +1,105 @@ +/* + * 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.delete; + +import lombok.RequiredArgsConstructor; +import org.apache.jena.graph.Graph; +import org.apache.jena.query.TxnType; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.rdfarchitect.api.dto.delete.DeleteRelations; +import org.rdfarchitect.cim.relations.model.CIMResourceTypeIdentifyingUtils; +import org.rdfarchitect.database.DatabasePort; +import org.rdfarchitect.database.GraphIdentifier; +import org.rdfarchitect.rdf.graph.GraphUtils; +import org.rdfarchitect.rdf.graph.wrapper.GraphRewindable; +import org.springframework.stereotype.Service; +import org.rdfarchitect.cim.relations.model.CIMResourceTypeIdentifyingUtils.CimResourceType; + +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class FindOnDeleteRelationsService implements FindOnDeleteRelationsUseCase { + + private final DatabasePort databasePort; + + @Override + public DeleteRelations getDeleteRelations(GraphIdentifier graphIdentifier, UUID uuid) { + var model = ModelFactory.createModelForGraph(getCopyOfDatabaseGraph(graphIdentifier)); + var resourceType = CIMResourceTypeIdentifyingUtils.getType(model, uuid); + return switch (resourceType) { + case PACKAGE -> findAffectedRelationsForPackage(model, uuid); + case CLASS -> findAffectedRelationsForClass(model, uuid); + case ATTRIBUTE -> findAffectedRelationsForAttribute(model, uuid); + case ASSOCIATION -> findAffectedRelationsForAssociation(model, uuid); + case ENUM_ENTRY -> findAffectedRelationsForEnumEntry(model, uuid); + case ONTOLOGY -> findAffectedRelationsForOntology(model, uuid); + case UNKNOWN -> findAffectedRelationsForUnknown(model, uuid); + }; + } + + private DeleteRelations findAffectedRelationsForPackage(Model model, UUID uuid) { + // TODO: implement + throw new UnsupportedOperationException("Not yet implemented"); + } + + private DeleteRelations findAffectedRelationsForClass(Model model, UUID uuid) { + // TODO: implement + throw new UnsupportedOperationException("Not yet implemented"); + } + + private DeleteRelations findAffectedRelationsForAttribute(Model model, UUID uuid) { + // TODO: implement + throw new UnsupportedOperationException("Not yet implemented"); + } + + private DeleteRelations findAffectedRelationsForAssociation(Model model, UUID uuid) { + // TODO: implement + throw new UnsupportedOperationException("Not yet implemented"); + } + + private DeleteRelations findAffectedRelationsForEnumEntry(Model model, UUID uuid) { + // TODO: implement + throw new UnsupportedOperationException("Not yet implemented"); + } + + private DeleteRelations findAffectedRelationsForOntology(Model model, UUID uuid) { + // TODO: implement + throw new UnsupportedOperationException("Not yet implemented"); + } + + private DeleteRelations findAffectedRelationsForUnknown(Model model, UUID uuid) { + // TODO: implement + throw new UnsupportedOperationException("Not yet implemented"); + } + + private Graph getCopyOfDatabaseGraph(GraphIdentifier graphIdentifier){ + GraphRewindable graph = null; + try{ + graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); + graph.begin(TxnType.READ); + return GraphUtils.deepCopy(graph); + } finally { + if(graph != null) { + graph.end(); + } + } + } + +} diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsUseCase.java b/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsUseCase.java new file mode 100644 index 00000000..c0d44d97 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsUseCase.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.delete; + +import org.rdfarchitect.api.dto.delete.DeleteRelations; +import org.rdfarchitect.database.GraphIdentifier; + +import java.util.UUID; + +public interface FindOnDeleteRelationsUseCase { + + DeleteRelations getDeleteRelations(GraphIdentifier graphIdentifier, UUID uuid); +} From 2e89607e5c07af06f1ace12fedf671a62f674d99 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Thu, 9 Apr 2026 21:17:20 +0200 Subject: [PATCH 02/31] improved structure for deleteRelations response and started on implementing more methods --- .../graphs/DeletionImpactRESTController.java | 4 +- .../api/dto/delete/DeleteActions.java | 56 ++++++ .../api/dto/delete/DeleteRelations.java | 22 --- .../api/dto/delete/ResourceDeleteRequest.java | 7 + .../delete/relations/AffectedResource.java | 90 +++++++++ .../CIMResourceTypeIdentifyingUtils.java | 4 +- .../delete/FindOnDeleteRelationsService.java | 178 ++++++++++++++---- .../delete/FindOnDeleteRelationsUseCase.java | 4 +- 8 files changed, 302 insertions(+), 63 deletions(-) create mode 100644 backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteActions.java delete mode 100644 backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteRelations.java create mode 100644 backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeletionImpactRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeletionImpactRESTController.java index 0c5a44fe..622796a5 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeletionImpactRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeletionImpactRESTController.java @@ -21,7 +21,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; import lombok.RequiredArgsConstructor; -import org.rdfarchitect.api.dto.delete.DeleteRelations; +import org.rdfarchitect.api.dto.delete.relations.AffectedResource; import org.rdfarchitect.services.delete.FindOnDeleteRelationsUseCase; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.services.ExpandURIUseCase; @@ -52,7 +52,7 @@ public class DeletionImpactRESTController { } ) @GetMapping - public DeleteRelations getDeletionImpact( + public AffectedResource getDeletionImpact( @Parameter(description = "The name/url of the inquirer.") @RequestHeader(value = HttpHeaders.ORIGIN, required = false, defaultValue = "unknown") String originURL, diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteActions.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteActions.java new file mode 100644 index 00000000..a5190c2a --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteActions.java @@ -0,0 +1,56 @@ +/* + * 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.api.dto.delete; + +public enum DeleteActions { + + /** + * Delete the affected resource entirely. + * Applicable to all resource types. + */ + DELETE, + + /** + * Keep the affected resource as-is, even though + * it references a deleted resource. + * E.g. a class that extends a deleted class — keep the class + * but accept that the parent reference becomes invalid. + */ + KEEP, + + /** + * Remove the reference to the deleted resource without + * deleting the affected resource itself. + * E.g. a class extends a deleted class — remove the + * inheritance relationship but keep the class. + */ + REMOVE_REFERENCE, + + //nur falls delete von enum entries implementiert wird, aber eher unwahrscheinlich + /** + * Unset the default value of an attribute when the + * enum entry used as its default value is deleted. + */ + UNSET_DEFAULT_VALUE, + + /** + * Unset the fixed value of an attribute when the + * enum entry used as its fixed value is deleted. + */ + UNSET_FIXED_VALUE +} diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteRelations.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteRelations.java deleted file mode 100644 index 147279ec..00000000 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteRelations.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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.api.dto.delete; - -public class DeleteRelations { - -} diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/ResourceDeleteRequest.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/ResourceDeleteRequest.java index c4bfa953..7ec82960 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/ResourceDeleteRequest.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/ResourceDeleteRequest.java @@ -17,6 +17,13 @@ package org.rdfarchitect.api.dto.delete; +import lombok.Data; + +import java.util.UUID; + +@Data public class ResourceDeleteRequest { + private UUID uuid; + } diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java new file mode 100644 index 00000000..67ef588f --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java @@ -0,0 +1,90 @@ +/* + * 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.api.dto.delete.relations; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.rdfarchitect.api.dto.delete.DeleteActions; +import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils; + +import java.util.List; +import java.util.UUID; + +@Data +@Accessors(chain = true, fluent = true) +public class AffectedResource { + + private UUID uuid; + + private CIMResourceTypeIdentifyingUtils.CimResourceType type; + + private String label; + + private AffectedResourceReason reason; + + private List actions; + + private List children; + + public enum AffectedResourceReason { + CONTAINED_IN_PACKAGE, + USES_DELETE_CLASS_AS_DATATYPE, + REFENCES_DELETED_CLASS_VIA_ASSOCIATION, + CHILD_OF, + USES_DELETED_CLASS_AS_DEFAULT_VALUE, + USES_DELETED_CLASS_AS_FIXED_VALUE, + DELETION_REQUESTED_BY_USER, + } +} + + /* + Eine Assoziation kann nur von der inverse gelöscht referenziert werden, da aber immer alles zusammen gelöscht wird gibt es ein trickle effect über den man entscheiden muss + */ + + /* + Ein Attribut zu löschen hat keine Auswirkung auf irgend etwas anderes + */ + + /* + other classes + Another class is extending this one + - Class A (Keep refence to deleted Class) (delete reference) (Delete Class? (würde ich erstmal als overkill ansehen)) + + Attributes (sowohl enum als auch normal) + This class is used as a Datatype in the following attributes: + How would you like to proceed + - Attr1 von Klasse A (unreferenced Datatype) (Delete Attribute) + ... + + This class is Referenced Via an association as a Target: + - Association1 von Klasse A (unreferenced Target) (Delete Association) + ... + */ + + /* + Ein enum entry kann von theoretisch als default wert referenziert werden. + Problem ist, dass man dann beim löschen eines enum entries den put request analysieren muss, welche operation ausgeführt wird. + Aber wenn man das macht: + - Deleting a used enum entry in a default value. (delete Attribute) (delete default value) + */ + + /* + Do you want to delete the contents of this package? + - class A (Delete Class (extend into classDropdown)) (keep reference) (remove reference) + ... + */ diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java index df862688..ddc888c7 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java @@ -15,7 +15,7 @@ * */ -package org.rdfarchitect.cim.relations.model; +package org.rdfarchitect.models.cim.relations.model; import lombok.experimental.UtilityClass; import org.apache.jena.rdf.model.Model; @@ -68,7 +68,7 @@ public CimResourceType getType(Model model, UUID uuid) { .orElse(CimResourceType.UNKNOWN); } - private Resource findUniqueSubject(Model model, UUID uuid) { + public Resource findUniqueSubject(Model model, UUID uuid) { var subjects = model.listSubjectsWithProperty(RDFA.uuid, uuid.toString()).toList(); if (subjects.size() != 1) { throw new IllegalArgumentException("Expected exactly one subject with UUID " + uuid + ", but found " + subjects.size()); diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java b/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java index a7953f3a..f19a40e7 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java @@ -22,15 +22,23 @@ import org.apache.jena.query.TxnType; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; -import org.rdfarchitect.api.dto.delete.DeleteRelations; -import org.rdfarchitect.cim.relations.model.CIMResourceTypeIdentifyingUtils; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import org.rdfarchitect.api.dto.delete.DeleteActions; +import org.rdfarchitect.api.dto.delete.relations.AffectedResource; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; +import org.rdfarchitect.models.cim.rdf.resources.CIMS; +import org.rdfarchitect.models.cim.rdf.resources.RDFA; +import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils; +import org.rdfarchitect.models.cim.relations.model.properties.CIMPropertyUtils; import org.rdfarchitect.rdf.graph.GraphUtils; import org.rdfarchitect.rdf.graph.wrapper.GraphRewindable; import org.springframework.stereotype.Service; -import org.rdfarchitect.cim.relations.model.CIMResourceTypeIdentifyingUtils.CimResourceType; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; @Service @@ -40,66 +48,166 @@ public class FindOnDeleteRelationsService implements FindOnDeleteRelationsUseCas private final DatabasePort databasePort; @Override - public DeleteRelations getDeleteRelations(GraphIdentifier graphIdentifier, UUID uuid) { + public AffectedResource getDeleteRelations(GraphIdentifier graphIdentifier, UUID uuid) { var model = ModelFactory.createModelForGraph(getCopyOfDatabaseGraph(graphIdentifier)); var resourceType = CIMResourceTypeIdentifyingUtils.getType(model, uuid); + var defaultActions = List.of(DeleteActions.DELETE); return switch (resourceType) { - case PACKAGE -> findAffectedRelationsForPackage(model, uuid); - case CLASS -> findAffectedRelationsForClass(model, uuid); - case ATTRIBUTE -> findAffectedRelationsForAttribute(model, uuid); - case ASSOCIATION -> findAffectedRelationsForAssociation(model, uuid); - case ENUM_ENTRY -> findAffectedRelationsForEnumEntry(model, uuid); - case ONTOLOGY -> findAffectedRelationsForOntology(model, uuid); - case UNKNOWN -> findAffectedRelationsForUnknown(model, uuid); + case PACKAGE -> findAffectedRelationsForPackage(model, uuid, AffectedResource.AffectedResourceReason.DELETION_REQUESTED_BY_USER, defaultActions); + case CLASS -> findAffectedRelationsForClass(model, uuid, AffectedResource.AffectedResourceReason.DELETION_REQUESTED_BY_USER, defaultActions); + case ATTRIBUTE -> findAffectedRelationsForAttribute(model, uuid, AffectedResource.AffectedResourceReason.DELETION_REQUESTED_BY_USER, defaultActions); + case ASSOCIATION -> findAffectedRelationsForAssociation(model, uuid, AffectedResource.AffectedResourceReason.DELETION_REQUESTED_BY_USER, defaultActions); + case ENUM_ENTRY -> findAffectedRelationsForEnumEntry(model, uuid, AffectedResource.AffectedResourceReason.DELETION_REQUESTED_BY_USER, defaultActions); + case ONTOLOGY -> findAffectedRelationsForOntology(model, uuid, AffectedResource.AffectedResourceReason.DELETION_REQUESTED_BY_USER, defaultActions); + case UNKNOWN -> findAffectedRelationsForUnknown(model, uuid, AffectedResource.AffectedResourceReason.DELETION_REQUESTED_BY_USER, defaultActions); }; } - private DeleteRelations findAffectedRelationsForPackage(Model model, UUID uuid) { - // TODO: implement - throw new UnsupportedOperationException("Not yet implemented"); + private AffectedResource findAffectedRelationsForPackage(Model model, UUID uuid, AffectedResource.AffectedResourceReason reason, List deleteActions) { + var classesInPackage = listClassesInPackage(model, uuid); + var affectedResources = new ArrayList(); + var clsDeleteActions = List.of(DeleteActions.DELETE, DeleteActions.KEEP, DeleteActions.REMOVE_REFERENCE); + for (var cls : classesInPackage) { + var clsUuid = findUuidForResource(cls); + var affectedClassResource = findAffectedRelationsForClass(model, clsUuid, AffectedResource.AffectedResourceReason.CONTAINED_IN_PACKAGE, clsDeleteActions); + affectedResources.add(affectedClassResource); + } + return new AffectedResource().uuid(uuid) + .label(getLabelForUuid(model, uuid)) + .type(CIMResourceTypeIdentifyingUtils.CimResourceType.PACKAGE) + .actions(deleteActions) + .reason(reason) + .children(affectedResources); } - private DeleteRelations findAffectedRelationsForClass(Model model, UUID uuid) { - // TODO: implement - throw new UnsupportedOperationException("Not yet implemented"); + private UUID findUuidForResource(Resource resource) { + if (resource.hasProperty(RDFA.uuid)) { + throw new IllegalStateException("Resource " + resource + " does not have a UUID."); + } + return UUID.fromString(resource.getProperty(RDFA.uuid).getString()); } - private DeleteRelations findAffectedRelationsForAttribute(Model model, UUID uuid) { - // TODO: implement - throw new UnsupportedOperationException("Not yet implemented"); + private List listClassesInPackage(Model model, UUID uuid) { + var packageResource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, uuid); + if (!packageResource.hasProperty(RDF.type, CIMS.classCategory)) { + throw new IllegalArgumentException("Resource with UUID " + uuid + " is not a package."); + } + return model.listSubjectsWithProperty(CIMS.belongsToCategory, packageResource) + .filterKeep(cls -> cls.hasProperty(RDF.type, RDFS.Class)) + .toList(); } - private DeleteRelations findAffectedRelationsForAssociation(Model model, UUID uuid) { - // TODO: implement - throw new UnsupportedOperationException("Not yet implemented"); + private AffectedResource findAffectedRelationsForClass(Model model, UUID uuid, + AffectedResource.AffectedResourceReason reason, List deleteActions) { + + var childActions = List.of(DeleteActions.DELETE, DeleteActions.KEEP); + + var affectedResources = new ArrayList(); + + // attributes + listAttributesWithClassAsDatatype(model, uuid).stream() + .map(attr -> findAffectedRelationsForAttribute(model, + findUuidForResource(attr), + AffectedResource.AffectedResourceReason.USES_DELETE_CLASS_AS_DATATYPE, + childActions)) + .forEach(affectedResources::add); + + // associations + listAssociationsReferencingClass(model, uuid).stream() + .map(assoc -> findAffectedRelationsForAssociation(model, + findUuidForResource(assoc), + AffectedResource.AffectedResourceReason.REFENCES_DELETED_CLASS_VIA_ASSOCIATION, + childActions)) + .forEach(affectedResources::add); + + // Attribute, die Teile der Klasse sind, werden hier nicht abgefragt, da sie immer mitgelöscht werden. + // (Vllt. sollte man die aber trotzdem mit anzeigen und keine Option oder so geben?) + + return new AffectedResource().uuid(uuid) + .label(getLabelForUuid(model, uuid)) + .type(CIMResourceTypeIdentifyingUtils.CimResourceType.CLASS) + .actions(deleteActions) + .reason(reason) + .children(affectedResources); } - private DeleteRelations findAffectedRelationsForEnumEntry(Model model, UUID uuid) { - // TODO: implement - throw new UnsupportedOperationException("Not yet implemented"); + private List listAssociationsReferencingClass(Model model, UUID uuid) { + var classResource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, uuid); + return model.listSubjectsWithProperty(RDFS.domain, classResource) + .filterKeep(CIMPropertyUtils::isAssociation) + .toList(); } - private DeleteRelations findAffectedRelationsForOntology(Model model, UUID uuid) { - // TODO: implement - throw new UnsupportedOperationException("Not yet implemented"); + private List listAttributesWithClassAsDatatype(Model model, UUID uuid) { + var classResource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, uuid); + var byDatatype = model.listSubjectsWithProperty(CIMS.datatype, classResource); + var byRange = model.listSubjectsWithProperty(RDFS.range, classResource); + return byDatatype.andThen(byRange) + .filterKeep(CIMPropertyUtils::isAttribute) + .toList(); } - private DeleteRelations findAffectedRelationsForUnknown(Model model, UUID uuid) { - // TODO: implement - throw new UnsupportedOperationException("Not yet implemented"); + private AffectedResource findAffectedRelationsForAttribute(Model model, UUID uuid, AffectedResource.AffectedResourceReason reason, List deleteActions) { + return new AffectedResource().uuid(uuid) + .label(getLabelForUuid(model, uuid)) + .type(CIMResourceTypeIdentifyingUtils.CimResourceType.ATTRIBUTE) + .actions(deleteActions) + .reason(reason); } - private Graph getCopyOfDatabaseGraph(GraphIdentifier graphIdentifier){ + private AffectedResource findAffectedRelationsForAssociation(Model model, UUID uuid, AffectedResource.AffectedResourceReason reason, List deleteActions) { + return new AffectedResource().uuid(uuid) + .label(getLabelForUuid(model, uuid)) + .type(CIMResourceTypeIdentifyingUtils.CimResourceType.ASSOCIATION) + .actions(deleteActions) + .reason(reason); + } + + private AffectedResource findAffectedRelationsForEnumEntry(Model model, UUID uuid, AffectedResource.AffectedResourceReason reason, List deleteActions) { + //hier evtl einfügen, dass attrbiute die diese als default wert nutzen gesucht werden, aber noch unklar ob das gemacht wird. + return new AffectedResource().uuid(uuid) + .label(getLabelForUuid(model, uuid)) + .type(CIMResourceTypeIdentifyingUtils.CimResourceType.ENUM_ENTRY) + .actions(deleteActions) + .reason(reason); + } + + private AffectedResource findAffectedRelationsForOntology(Model model, UUID uuid, AffectedResource.AffectedResourceReason reason, List deleteActions) { + // sollte nichts geben + return new AffectedResource().uuid(uuid) + .label(getLabelForUuid(model, uuid)) + .type(CIMResourceTypeIdentifyingUtils.CimResourceType.ONTOLOGY) + .actions(deleteActions) + .reason(reason); + } + + private AffectedResource findAffectedRelationsForUnknown(Model model, UUID uuid, AffectedResource.AffectedResourceReason reason, List deleteActions) { + // ich denke hier entweder einen Fehler werfen oder einfach erlauben die Ressource zu löschen + return new AffectedResource().uuid(uuid) + .label(getLabelForUuid(model, uuid)) + .type(CIMResourceTypeIdentifyingUtils.CimResourceType.UNKNOWN) + .actions(deleteActions) + .reason(reason); + } + + private Graph getCopyOfDatabaseGraph(GraphIdentifier graphIdentifier) { GraphRewindable graph = null; - try{ + try { graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); graph.begin(TxnType.READ); return GraphUtils.deepCopy(graph); } finally { - if(graph != null) { + if (graph != null) { graph.end(); } } } + private String getLabelForUuid(Model model, UUID uuid) { + var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, uuid); + if (!resource.hasProperty(RDFS.label)) { + throw new IllegalStateException("Resource with UUID " + uuid + " does not have a label."); + } + return resource.getProperty(RDFS.label).getString(); + } } diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsUseCase.java b/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsUseCase.java index c0d44d97..b2275b3f 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsUseCase.java @@ -17,12 +17,12 @@ package org.rdfarchitect.services.delete; -import org.rdfarchitect.api.dto.delete.DeleteRelations; +import org.rdfarchitect.api.dto.delete.relations.AffectedResource; import org.rdfarchitect.database.GraphIdentifier; import java.util.UUID; public interface FindOnDeleteRelationsUseCase { - DeleteRelations getDeleteRelations(GraphIdentifier graphIdentifier, UUID uuid); + AffectedResource getDeleteRelations(GraphIdentifier graphIdentifier, UUID uuid); } From 0ddd2a9b55e25fb07eb9fb67ff131d598b63ef7d Mon Sep 17 00:00:00 2001 From: Maximilian Date: Mon, 13 Apr 2026 09:39:47 +0200 Subject: [PATCH 03/31] added needed context for displaying data correctly --- .../api/dto/delete/ResourceIdentifier.java | 31 +++ .../delete/relations/AffectedAssociation.java | 43 ++++ .../relations/AffectedOwnedResource.java | 42 ++++ .../delete/relations/AffectedResource.java | 27 ++- .../delete/FindOnDeleteRelationsService.java | 215 +++++++++--------- frontend/src/lib/api/backend.js | 8 + .../routes/DeleteClassConfirmDialog.svelte | 18 ++ 7 files changed, 272 insertions(+), 112 deletions(-) create mode 100644 backend/src/main/java/org/rdfarchitect/api/dto/delete/ResourceIdentifier.java create mode 100644 backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedAssociation.java create mode 100644 backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedOwnedResource.java diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/ResourceIdentifier.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/ResourceIdentifier.java new file mode 100644 index 00000000..979be1aa --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/ResourceIdentifier.java @@ -0,0 +1,31 @@ +/* + * 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.api.dto.delete; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.UUID; + +@Data +@Accessors(chain = true) +public class ResourceIdentifier { + private UUID uuid; + private String label; + private String namespace; +} \ No newline at end of file diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedAssociation.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedAssociation.java new file mode 100644 index 00000000..5b2704dd --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedAssociation.java @@ -0,0 +1,43 @@ +/* + * 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.api.dto.delete.relations; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.rdfarchitect.api.dto.delete.ResourceIdentifier; +import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils; + +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +public class AffectedAssociation extends AffectedOwnedResource { + + private ResourceIdentifier target; + + public AffectedAssociation(ResourceIdentifier resourceIdentifier, + CIMResourceTypeIdentifyingUtils.CimResourceType type, + AffectedResourceReason reason, + ResourceIdentifier domain, + ResourceIdentifier target) { + this.target = target; + super(resourceIdentifier, type, reason, domain); + } +} diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedOwnedResource.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedOwnedResource.java new file mode 100644 index 00000000..e449ede8 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedOwnedResource.java @@ -0,0 +1,42 @@ +/* + * 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.api.dto.delete.relations; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.rdfarchitect.api.dto.delete.ResourceIdentifier; +import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils; + +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +public class AffectedOwnedResource extends AffectedResource { + + private ResourceIdentifier domain; + + public AffectedOwnedResource(ResourceIdentifier resourceIdentifier, + CIMResourceTypeIdentifyingUtils.CimResourceType type, + AffectedResourceReason reason, + ResourceIdentifier domain) { + this.domain = domain; + super(resourceIdentifier, type, reason); + } +} diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java index 67ef588f..908bf439 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java @@ -18,28 +18,41 @@ package org.rdfarchitect.api.dto.delete.relations; import lombok.Data; +import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import org.rdfarchitect.api.dto.delete.DeleteActions; +import org.rdfarchitect.api.dto.delete.ResourceIdentifier; import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; -import java.util.UUID; +import java.util.Map; @Data -@Accessors(chain = true, fluent = true) +@NoArgsConstructor +@Accessors(chain = true) public class AffectedResource { - private UUID uuid; + private ResourceIdentifier resourceIdentifier; private CIMResourceTypeIdentifyingUtils.CimResourceType type; - private String label; - private AffectedResourceReason reason; - private List actions; + private List actions = new ArrayList<>(); + + private List children = new ArrayList<>(); - private List children; + private Map context = new HashMap<>(); + + public AffectedResource(ResourceIdentifier resourceIdentifier, + CIMResourceTypeIdentifyingUtils.CimResourceType type, + AffectedResourceReason reason) { + this.resourceIdentifier = resourceIdentifier; + this.type = type; + this.reason = reason; + } public enum AffectedResourceReason { CONTAINED_IN_PACKAGE, diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java b/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java index f19a40e7..98adb66b 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java @@ -26,12 +26,17 @@ import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; import org.rdfarchitect.api.dto.delete.DeleteActions; +import org.rdfarchitect.api.dto.delete.ResourceIdentifier; +import org.rdfarchitect.api.dto.delete.relations.AffectedAssociation; +import org.rdfarchitect.api.dto.delete.relations.AffectedOwnedResource; import org.rdfarchitect.api.dto.delete.relations.AffectedResource; +import org.rdfarchitect.api.dto.delete.relations.AffectedResource.AffectedResourceReason; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.models.cim.rdf.resources.CIMS; import org.rdfarchitect.models.cim.rdf.resources.RDFA; import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils; +import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils.CimResourceType; import org.rdfarchitect.models.cim.relations.model.properties.CIMPropertyUtils; import org.rdfarchitect.rdf.graph.GraphUtils; import org.rdfarchitect.rdf.graph.wrapper.GraphRewindable; @@ -52,36 +57,103 @@ public AffectedResource getDeleteRelations(GraphIdentifier graphIdentifier, UUID var model = ModelFactory.createModelForGraph(getCopyOfDatabaseGraph(graphIdentifier)); var resourceType = CIMResourceTypeIdentifyingUtils.getType(model, uuid); var defaultActions = List.of(DeleteActions.DELETE); + var reason = AffectedResourceReason.DELETION_REQUESTED_BY_USER; return switch (resourceType) { - case PACKAGE -> findAffectedRelationsForPackage(model, uuid, AffectedResource.AffectedResourceReason.DELETION_REQUESTED_BY_USER, defaultActions); - case CLASS -> findAffectedRelationsForClass(model, uuid, AffectedResource.AffectedResourceReason.DELETION_REQUESTED_BY_USER, defaultActions); - case ATTRIBUTE -> findAffectedRelationsForAttribute(model, uuid, AffectedResource.AffectedResourceReason.DELETION_REQUESTED_BY_USER, defaultActions); - case ASSOCIATION -> findAffectedRelationsForAssociation(model, uuid, AffectedResource.AffectedResourceReason.DELETION_REQUESTED_BY_USER, defaultActions); - case ENUM_ENTRY -> findAffectedRelationsForEnumEntry(model, uuid, AffectedResource.AffectedResourceReason.DELETION_REQUESTED_BY_USER, defaultActions); - case ONTOLOGY -> findAffectedRelationsForOntology(model, uuid, AffectedResource.AffectedResourceReason.DELETION_REQUESTED_BY_USER, defaultActions); - case UNKNOWN -> findAffectedRelationsForUnknown(model, uuid, AffectedResource.AffectedResourceReason.DELETION_REQUESTED_BY_USER, defaultActions); + case PACKAGE -> findAffectedRelationsForPackage(model, uuid, reason, defaultActions); + case CLASS -> findAffectedRelationsForClass(model, uuid, reason, defaultActions); + case ATTRIBUTE -> new AffectedResource(createResourceIdentifier(model, uuid), CimResourceType.ATTRIBUTE, reason) + .setActions(defaultActions); + case ASSOCIATION -> new AffectedResource(createResourceIdentifier(model, uuid), CimResourceType.ASSOCIATION, reason) + .setActions(defaultActions); + case ENUM_ENTRY -> new AffectedResource(createResourceIdentifier(model, uuid), CimResourceType.ENUM_ENTRY, reason) + .setActions(defaultActions); + case ONTOLOGY -> new AffectedResource(createResourceIdentifier(model, uuid), CimResourceType.ONTOLOGY, reason) + .setActions(defaultActions); + case UNKNOWN -> new AffectedResource(createResourceIdentifier(model, uuid), CimResourceType.UNKNOWN, reason) + .setActions(defaultActions); }; } - private AffectedResource findAffectedRelationsForPackage(Model model, UUID uuid, AffectedResource.AffectedResourceReason reason, List deleteActions) { + private AffectedResource findAffectedRelationsForPackage(Model model, UUID uuid, AffectedResourceReason reason, List deleteActions) { var classesInPackage = listClassesInPackage(model, uuid); var affectedResources = new ArrayList(); var clsDeleteActions = List.of(DeleteActions.DELETE, DeleteActions.KEEP, DeleteActions.REMOVE_REFERENCE); for (var cls : classesInPackage) { var clsUuid = findUuidForResource(cls); - var affectedClassResource = findAffectedRelationsForClass(model, clsUuid, AffectedResource.AffectedResourceReason.CONTAINED_IN_PACKAGE, clsDeleteActions); + var affectedClassResource = findAffectedRelationsForClass(model, clsUuid, AffectedResourceReason.CONTAINED_IN_PACKAGE, clsDeleteActions); affectedResources.add(affectedClassResource); } - return new AffectedResource().uuid(uuid) - .label(getLabelForUuid(model, uuid)) - .type(CIMResourceTypeIdentifyingUtils.CimResourceType.PACKAGE) - .actions(deleteActions) - .reason(reason) - .children(affectedResources); + return new AffectedResource(createResourceIdentifier(model, uuid), CimResourceType.PACKAGE, reason) + .setActions(deleteActions) + .setChildren(affectedResources); + } + + private AffectedResource findAffectedRelationsForClass(Model model, UUID uuid, + AffectedResourceReason reason, List deleteActions) { + var classResource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, uuid); + + var classResourceId = createResourceIdentifier(model, uuid); + var affectedResources = new ArrayList(); + affectedResources.addAll(findAffectedAttributesForClass(classResource, classResourceId)); + affectedResources.addAll(findAffectedAssociationsForClass(classResource, classResourceId)); + affectedResources.addAll(findAffectedChildClassesForClass(classResource)); + + // Attribute, die Teile der Klasse sind, werden hier nicht abgefragt, da sie immer mitgelöscht werden. + // (Vllt. sollte man die aber trotzdem mit anzeigen und keine Option oder so geben?) + + return new AffectedResource(classResourceId, CimResourceType.CLASS, reason) + .setActions(deleteActions) + .setChildren(affectedResources); + } + + private List findAffectedAttributesForClass(Resource classResource, ResourceIdentifier classResourceId) { + var childActions = List.of(DeleteActions.DELETE, DeleteActions.KEEP); + return listAttributesWithClassAsDatatype(classResource).stream() + .map(attr -> new AffectedOwnedResource(createResourceIdentifier(attr), + CimResourceType.ATTRIBUTE, + AffectedResourceReason.USES_DELETE_CLASS_AS_DATATYPE, + classResourceId) + .setActions(childActions)) + .toList(); + } + + private List findAffectedAssociationsForClass(Resource classResource, ResourceIdentifier classResourceId) { + var childActions = List.of(DeleteActions.DELETE, DeleteActions.KEEP); + return listAssociationsReferencingClass(classResource).stream() + .map(assoc -> new AffectedAssociation(createResourceIdentifier(assoc), + CimResourceType.ASSOCIATION, + AffectedResourceReason.REFENCES_DELETED_CLASS_VIA_ASSOCIATION, + classResourceId, + getAssociationTarget(assoc) + ) + .setActions(childActions)) + .toList(); + } + + private ResourceIdentifier getAssociationTarget(Resource associationResource) { + var rangeStatement = associationResource.getProperty(RDFS.range); + if(rangeStatement == null){ + throw new IllegalStateException("Association " + associationResource + " does not have a range."); + } + if(rangeStatement.getObject().isLiteral()){ + throw new IllegalStateException("Association " + associationResource + " has a literal as range, which is not supported."); + } + var rangeResource = rangeStatement.getObject().asResource(); + return createResourceIdentifier(rangeResource); + } + + private List findAffectedChildClassesForClass(Resource classResource) { + var childClassActions = List.of(DeleteActions.KEEP, DeleteActions.REMOVE_REFERENCE); + return listDirectlyDescendingClasses(classResource).stream() + .map(childClass -> new AffectedResource(createResourceIdentifier(childClass), + CimResourceType.CLASS, + AffectedResourceReason.CHILD_OF) + .setActions(childClassActions)) + .toList(); } private UUID findUuidForResource(Resource resource) { - if (resource.hasProperty(RDFA.uuid)) { + if (!resource.hasProperty(RDFA.uuid)) { throw new IllegalStateException("Resource " + resource + " does not have a UUID."); } return UUID.fromString(resource.getProperty(RDFA.uuid).getString()); @@ -90,104 +162,45 @@ private UUID findUuidForResource(Resource resource) { private List listClassesInPackage(Model model, UUID uuid) { var packageResource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, uuid); if (!packageResource.hasProperty(RDF.type, CIMS.classCategory)) { - throw new IllegalArgumentException("Resource with UUID " + uuid + " is not a package."); + throw new IllegalStateException("Resource with UUID " + uuid + " is not a package."); } return model.listSubjectsWithProperty(CIMS.belongsToCategory, packageResource) .filterKeep(cls -> cls.hasProperty(RDF.type, RDFS.Class)) .toList(); } - private AffectedResource findAffectedRelationsForClass(Model model, UUID uuid, - AffectedResource.AffectedResourceReason reason, List deleteActions) { - - var childActions = List.of(DeleteActions.DELETE, DeleteActions.KEEP); - - var affectedResources = new ArrayList(); - - // attributes - listAttributesWithClassAsDatatype(model, uuid).stream() - .map(attr -> findAffectedRelationsForAttribute(model, - findUuidForResource(attr), - AffectedResource.AffectedResourceReason.USES_DELETE_CLASS_AS_DATATYPE, - childActions)) - .forEach(affectedResources::add); - - // associations - listAssociationsReferencingClass(model, uuid).stream() - .map(assoc -> findAffectedRelationsForAssociation(model, - findUuidForResource(assoc), - AffectedResource.AffectedResourceReason.REFENCES_DELETED_CLASS_VIA_ASSOCIATION, - childActions)) - .forEach(affectedResources::add); - - // Attribute, die Teile der Klasse sind, werden hier nicht abgefragt, da sie immer mitgelöscht werden. - // (Vllt. sollte man die aber trotzdem mit anzeigen und keine Option oder so geben?) - - return new AffectedResource().uuid(uuid) - .label(getLabelForUuid(model, uuid)) - .type(CIMResourceTypeIdentifyingUtils.CimResourceType.CLASS) - .actions(deleteActions) - .reason(reason) - .children(affectedResources); + private List listDirectlyDescendingClasses(Resource classResource) { + return classResource.getModel().listSubjectsWithProperty(RDFS.subClassOf, classResource).toList(); } - private List listAssociationsReferencingClass(Model model, UUID uuid) { - var classResource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, uuid); - return model.listSubjectsWithProperty(RDFS.domain, classResource) - .filterKeep(CIMPropertyUtils::isAssociation) - .toList(); + private List listAssociationsReferencingClass(Resource classResource) { + return classResource.getModel().listSubjectsWithProperty(RDFS.domain, classResource) + .filterKeep(CIMPropertyUtils::isAssociation) + .toList(); } - private List listAttributesWithClassAsDatatype(Model model, UUID uuid) { - var classResource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, uuid); + private List listAttributesWithClassAsDatatype(Resource classResource) { + var model = classResource.getModel(); var byDatatype = model.listSubjectsWithProperty(CIMS.datatype, classResource); var byRange = model.listSubjectsWithProperty(RDFS.range, classResource); return byDatatype.andThen(byRange) .filterKeep(CIMPropertyUtils::isAttribute) - .toList(); - } - - private AffectedResource findAffectedRelationsForAttribute(Model model, UUID uuid, AffectedResource.AffectedResourceReason reason, List deleteActions) { - return new AffectedResource().uuid(uuid) - .label(getLabelForUuid(model, uuid)) - .type(CIMResourceTypeIdentifyingUtils.CimResourceType.ATTRIBUTE) - .actions(deleteActions) - .reason(reason); - } - - private AffectedResource findAffectedRelationsForAssociation(Model model, UUID uuid, AffectedResource.AffectedResourceReason reason, List deleteActions) { - return new AffectedResource().uuid(uuid) - .label(getLabelForUuid(model, uuid)) - .type(CIMResourceTypeIdentifyingUtils.CimResourceType.ASSOCIATION) - .actions(deleteActions) - .reason(reason); + .toList().stream().distinct().toList(); } - private AffectedResource findAffectedRelationsForEnumEntry(Model model, UUID uuid, AffectedResource.AffectedResourceReason reason, List deleteActions) { - //hier evtl einfügen, dass attrbiute die diese als default wert nutzen gesucht werden, aber noch unklar ob das gemacht wird. - return new AffectedResource().uuid(uuid) - .label(getLabelForUuid(model, uuid)) - .type(CIMResourceTypeIdentifyingUtils.CimResourceType.ENUM_ENTRY) - .actions(deleteActions) - .reason(reason); - } - - private AffectedResource findAffectedRelationsForOntology(Model model, UUID uuid, AffectedResource.AffectedResourceReason reason, List deleteActions) { - // sollte nichts geben - return new AffectedResource().uuid(uuid) - .label(getLabelForUuid(model, uuid)) - .type(CIMResourceTypeIdentifyingUtils.CimResourceType.ONTOLOGY) - .actions(deleteActions) - .reason(reason); + private ResourceIdentifier createResourceIdentifier(Model model, UUID uuid) { + var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, uuid); + return createResourceIdentifier(resource); } - private AffectedResource findAffectedRelationsForUnknown(Model model, UUID uuid, AffectedResource.AffectedResourceReason reason, List deleteActions) { - // ich denke hier entweder einen Fehler werfen oder einfach erlauben die Ressource zu löschen - return new AffectedResource().uuid(uuid) - .label(getLabelForUuid(model, uuid)) - .type(CIMResourceTypeIdentifyingUtils.CimResourceType.UNKNOWN) - .actions(deleteActions) - .reason(reason); + private ResourceIdentifier createResourceIdentifier(Resource resource) { + var uuid = findUuidForResource(resource); + if (!resource.hasProperty(RDFS.label)) { + throw new IllegalStateException("Resource with UUID " + uuid + " does not have a label."); + } + return new ResourceIdentifier().setUuid(uuid) + .setLabel(resource.getProperty(RDFS.label).getString()) + .setNamespace(resource.getNameSpace()); } private Graph getCopyOfDatabaseGraph(GraphIdentifier graphIdentifier) { @@ -202,12 +215,4 @@ private Graph getCopyOfDatabaseGraph(GraphIdentifier graphIdentifier) { } } } - - private String getLabelForUuid(Model model, UUID uuid) { - var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, uuid); - if (!resource.hasProperty(RDFS.label)) { - throw new IllegalStateException("Resource with UUID " + uuid + " does not have a label."); - } - return resource.getProperty(RDFS.label).getString(); - } -} +} \ No newline at end of file diff --git a/frontend/src/lib/api/backend.js b/frontend/src/lib/api/backend.js index d4e5e173..35882ce1 100644 --- a/frontend/src/lib/api/backend.js +++ b/frontend/src/lib/api/backend.js @@ -190,6 +190,14 @@ export class BackendConnection { }); } + async getDeleteRelation(datasetName, graphURI, resourceUuid) { + let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/uuid/${encodeURIComponent(resourceUuid)}/deletion-impact`; + return await fetch(url, { + method: "GET", + headers: new Headers({ "Content-Type": "application/json" }), + credentials: "include", + }); + } async deleteClass(datasetName, graphURI, classUUID) { let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/classes/${encodeURIComponent(classUUID)}`; return await fetch(url, { diff --git a/frontend/src/routes/DeleteClassConfirmDialog.svelte b/frontend/src/routes/DeleteClassConfirmDialog.svelte index 93341ffb..c5f44faa 100644 --- a/frontend/src/routes/DeleteClassConfirmDialog.svelte +++ b/frontend/src/routes/DeleteClassConfirmDialog.svelte @@ -36,6 +36,23 @@ const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); + async function onOpen() { + if (!datasetName || !graphUri || !classUuid) { + console.error( + "Missing required properties to delete class:", + datasetName, + graphUri, + classUuid, + ); + showDialog = false; + } + let res = await bec.getDeleteRelation(datasetName, graphUri, classUuid); + let json = await res.json(); + console.warn( + "Delete class response - check for warnings before confirming deletion:", + json, + ); + } async function deleteClass() { bec.deleteClass(datasetName, graphUri, classUuid).then( async response => { @@ -61,6 +78,7 @@ Date: Mon, 13 Apr 2026 13:15:02 +0200 Subject: [PATCH 04/31] implemented frontend ui for selecting delete dependencies --- .../delete/relations/AffectedResource.java | 2 +- .../delete/FindOnDeleteRelationsService.java | 2 +- frontend/src/lib/dialog/ActionDialog.svelte | 6 +- frontend/src/lib/dialog/DialogBase.svelte | 2 +- .../routes/DeleteClassConfirmDialog.svelte | 99 -------- .../DeleteDependenciesDialog.svelte | 199 ++++++++++++++++ .../DeleteDependencyNode.svelte | 215 ++++++++++++++++++ .../deleteDependencyDefaults.js | 47 ++++ .../src/routes/layout/menu-bar/Edit.svelte | 14 +- .../components/ClassEditorButtons.svelte | 13 +- .../packageNavigation/ClassEntry.svelte | 14 +- .../packageNavigation/PackageButton.svelte | 12 +- 12 files changed, 493 insertions(+), 132 deletions(-) delete mode 100644 frontend/src/routes/DeleteClassConfirmDialog.svelte create mode 100644 frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte create mode 100644 frontend/src/routes/delete-relations-dialog/DeleteDependencyNode.svelte create mode 100644 frontend/src/routes/delete-relations-dialog/deleteDependencyDefaults.js diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java index 908bf439..9674ec29 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java @@ -56,7 +56,7 @@ public AffectedResource(ResourceIdentifier resourceIdentifier, public enum AffectedResourceReason { CONTAINED_IN_PACKAGE, - USES_DELETE_CLASS_AS_DATATYPE, + USES_DELETED_CLASS_AS_DATATYPE, REFENCES_DELETED_CLASS_VIA_ASSOCIATION, CHILD_OF, USES_DELETED_CLASS_AS_DEFAULT_VALUE, diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java b/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java index 98adb66b..d7a5d937 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java @@ -111,7 +111,7 @@ private List findAffectedAttributesForClass(Resource classReso return listAttributesWithClassAsDatatype(classResource).stream() .map(attr -> new AffectedOwnedResource(createResourceIdentifier(attr), CimResourceType.ATTRIBUTE, - AffectedResourceReason.USES_DELETE_CLASS_AS_DATATYPE, + AffectedResourceReason.USES_DELETED_CLASS_AS_DATATYPE, classResourceId) .setActions(childActions)) .toList(); diff --git a/frontend/src/lib/dialog/ActionDialog.svelte b/frontend/src/lib/dialog/ActionDialog.svelte index 6e632a05..9e09f445 100644 --- a/frontend/src/lib/dialog/ActionDialog.svelte +++ b/frontend/src/lib/dialog/ActionDialog.svelte @@ -101,8 +101,8 @@ -
-
+
+

-
+
{@render children?.()}
{#if !readonly && (secondaryButtonExists || primaryButtonExists)} diff --git a/frontend/src/lib/dialog/DialogBase.svelte b/frontend/src/lib/dialog/DialogBase.svelte index 5c2bccd6..32e64af5 100644 --- a/frontend/src/lib/dialog/DialogBase.svelte +++ b/frontend/src/lib/dialog/DialogBase.svelte @@ -73,7 +73,7 @@ - - - - -
-

- The class will be removed from the model. -
- References to this class will remain. -

-
-
diff --git a/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte b/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte new file mode 100644 index 00000000..1676146d --- /dev/null +++ b/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte @@ -0,0 +1,199 @@ + + + + + +
+

+ Select how affected resources should be handled when deleting this + {type}. +

+ + {#if deleteDependencies} +
+ +
+ {:else} +
+ Loading dependencies... +
+ {/if} +
+
diff --git a/frontend/src/routes/delete-relations-dialog/DeleteDependencyNode.svelte b/frontend/src/routes/delete-relations-dialog/DeleteDependencyNode.svelte new file mode 100644 index 00000000..e9f83764 --- /dev/null +++ b/frontend/src/routes/delete-relations-dialog/DeleteDependencyNode.svelte @@ -0,0 +1,215 @@ + + + + +
+ +
0 && !disabled} + class:opacity-40={disabled} + style="padding-left: {depth * 1.25 + 0.75}rem;" + > + + + + + + {typeBadge} + + + +
+ {#if isAssociation && node.domain && node.target} + + {node.target.label} + + + + {node.domain.label} + + {:else} + + {node.resourceIdentifier.label} + + {/if} + {#if node.reason && !isRoot} + + {reasonLabels[node.reason] ?? node.reason} + + {/if} +
+ + +
+ {#each availableActions as action} + {@const config = actionConfig[action]} +
+ {#if node.actions.includes(action)} +
+ selectAction(action)} + icon={config.icon} + text={config.text} + variant={currentAction === action + ? config.variant + : "default"} + title={config.tooltip} + disabled={disabled || isRoot} + /> +
+ {/if} +
+ {/each} +
+
+ + + {#if hasChildren && expanded} +
+ {#each node.children as child, i (child.resourceIdentifier.uuid)} + + {#if i > 0 && (node.children[i - 1]?.children?.length > 0 || child.children?.length > 0)} +
+ {/if} + + {/each} +
+ {/if} +
diff --git a/frontend/src/routes/delete-relations-dialog/deleteDependencyDefaults.js b/frontend/src/routes/delete-relations-dialog/deleteDependencyDefaults.js new file mode 100644 index 00000000..20824d82 --- /dev/null +++ b/frontend/src/routes/delete-relations-dialog/deleteDependencyDefaults.js @@ -0,0 +1,47 @@ +/* + * 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. + * + */ + +/** + * Default action rules per reason (and optionally type). + * Evaluated top-to-bottom, first match wins. + * Add, remove, or reorder entries to adjust defaults. + */ +const defaultActionRules = [ + { reason: "CONTAINED_IN_PACKAGE", action: "DELETE" }, + { + reason: "REFENCES_DELETED_CLASS_VIA_ASSOCIATION", + type: "ASSOCIATION", + action: "KEEP", + }, + { reason: "USES_DELETED_CLASS_AS_DATATYPE", action: "KEEP" }, + { reason: "CHILD_OF", action: "KEEP" }, +]; + +/** + * Resolves the default action for a node based on the rules. + * Falls back to the first available action if no rule matches. + */ +export function getDefaultAction(node) { + for (const rule of defaultActionRules) { + if (node.reason !== rule.reason) continue; + if (rule.type && node.type !== rule.type) continue; + if (node.actions.includes(rule.action)) { + return rule.action; + } + } + return node.actions[0]; +} diff --git a/frontend/src/routes/layout/menu-bar/Edit.svelte b/frontend/src/routes/layout/menu-bar/Edit.svelte index eb0f1904..20b3edff 100644 --- a/frontend/src/routes/layout/menu-bar/Edit.svelte +++ b/frontend/src/routes/layout/menu-bar/Edit.svelte @@ -43,10 +43,10 @@ forceReloadTrigger, } from "$lib/sharedState.svelte.js"; + import DeleteDependenciesDialog from "../../delete-relations-dialog/DeleteDependenciesDialog.svelte"; import FilterViewDialog from "../../FilterViewDialog.svelte"; import PackageEditorDialog from "../../mainpage/packageEditorDialog.svelte"; import OntologyDialog from "../../mainpage/packageNavigation/ontology-editor-dialog/OntologyDialog.svelte"; - import PackageDeleteDialog from "../../mainpage/packageNavigation/PackageDeleteDialog.svelte"; import NamespacesDialog from "../../NamespacesDialog.svelte"; import NewClassDialog from "../../NewClassDialog.svelte"; import NewGraphDialog from "../../NewGraphDialog.svelte"; @@ -60,7 +60,7 @@ let showNewGraphDialog = $state(false); let showNewPackageDialog = $state(false); let showFilterViewDialog = $state(false); - let showPackageDeleteDialog = $state(false); + let showDeleteDependenciesDialog = $state(false); let showPackageEditorDialog = $state(false); let showNamespaceDialog = $state(false); let showEditOntologyDialog = $state(false); @@ -154,7 +154,7 @@ packageDialogTarget = { ...selectedPackageDetails }; packageDialogDataset = selectedDataset; packageDialogGraph = selectedGraph; - showPackageDeleteDialog = true; + showDeleteDependenciesDialog = true; } async function getPackages() { @@ -376,12 +376,12 @@ readonly={isDatasetReadOnly} /> {/if} -{#if packageDialogTarget && showPackageDeleteDialog} - {/if} diff --git a/frontend/src/routes/mainpage/classEditor/components/ClassEditorButtons.svelte b/frontend/src/routes/mainpage/classEditor/components/ClassEditorButtons.svelte index 735458c8..c95ef3e4 100644 --- a/frontend/src/routes/mainpage/classEditor/components/ClassEditorButtons.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/ClassEditorButtons.svelte @@ -33,7 +33,7 @@ forceReloadTrigger, } from "$lib/sharedState.svelte.js"; - import DeleteClassConfirmDialog from "../../../DeleteClassConfirmDialog.svelte"; + import DeleteDependenciesDialog from "../../../delete-relations-dialog/DeleteDependenciesDialog.svelte"; import SHACLClassSpecificPopUp from "../../../shacl/shaclclassspecific/SHACLClassSpecificPopUp.svelte"; let { @@ -49,7 +49,7 @@ const classEditorContext = getContext("classEditor"); - let showClassDeleteDialog = $state(false); + let showDeleteDependenciesDialog = $state(false); let showSHACLClassDialog = $state(false); let readonly = $derived(classEditorContext.readonly); let datasetName = $derived(classEditorContext.datasetName); @@ -144,18 +144,17 @@
(showClassDeleteDialog = true)} + callOnClick={() => (showDeleteDependenciesDialog = true)} icon={faTrash} variant="danger" text="Delete" title="Delete class" /> -
{/if} diff --git a/frontend/src/routes/mainpage/packageNavigation/ClassEntry.svelte b/frontend/src/routes/mainpage/packageNavigation/ClassEntry.svelte index e23b4626..1ec523a0 100644 --- a/frontend/src/routes/mainpage/packageNavigation/ClassEntry.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/ClassEntry.svelte @@ -30,7 +30,7 @@ import { shortenIri } from "$lib/utils/iri.js"; import { isSelectedClass } from "./packageNavigationUtils.svelte.js"; - import DeleteClassConfirmDialog from "../../DeleteClassConfirmDialog.svelte"; + import DeleteDependenciesDialog from "../../delete-relations-dialog/DeleteDependenciesDialog.svelte"; import SHACLClassSpecificPopUp from "../../shacl/shaclclassspecific/SHACLClassSpecificPopUp.svelte"; let { @@ -42,7 +42,7 @@ onPackChange = () => {}, } = $props(); - let showDeleteDialog = $state(false); + let showDeleteDependenciesDialog = $state(false); let showSHACLDialog = $state(false); const highlightLabel = $derived(shortenIri(namespaces, classNavEntry.id)); @@ -123,7 +123,7 @@ { selectClass(); - showDeleteDialog = true; + showDeleteDependenciesDialog = true; }} disabled={readonly} faIcon={faTrash} @@ -134,13 +134,13 @@ - + { - showDeletePackageDialog = true; + showDeleteDependenciesDialog = true; }} disabled={readonly || isProtectedPackage} faIcon={faTrash} @@ -197,9 +197,9 @@ {readonly} /> - From a2ad272b0a04a11458411c3f25dbfd45d4960260 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Tue, 14 Apr 2026 14:20:25 +0200 Subject: [PATCH 05/31] added first impl for actual deletion. - still buggy --- ...troller.java => DeleteRESTController.java} | 55 ++++- .../{DeleteActions.java => DeleteAction.java} | 25 +- .../api/dto/delete/ResourceDeleteRequest.java | 1 + .../delete/relations/AffectedResource.java | 4 +- .../delete/DeleteResourcesService.java | 218 +++++++++++++++++- .../delete/DeleteResourcesUseCase.java | 3 +- .../delete/FindOnDeleteRelationsService.java | 22 +- frontend/src/lib/api/backend.js | 10 + .../DeleteDependenciesDialog.svelte | 30 ++- .../DeleteDependencyNode.svelte | 24 +- 10 files changed, 335 insertions(+), 57 deletions(-) rename backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/{DeletionImpactRESTController.java => DeleteRESTController.java} (55%) rename backend/src/main/java/org/rdfarchitect/api/dto/delete/{DeleteActions.java => DeleteAction.java} (62%) diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeletionImpactRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeleteRESTController.java similarity index 55% rename from backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeletionImpactRESTController.java rename to backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeleteRESTController.java index 622796a5..7033127f 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeletionImpactRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeleteRESTController.java @@ -21,26 +21,36 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; import lombok.RequiredArgsConstructor; +import org.rdfarchitect.api.dto.delete.ResourceDeleteRequest; import org.rdfarchitect.api.dto.delete.relations.AffectedResource; -import org.rdfarchitect.services.delete.FindOnDeleteRelationsUseCase; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.services.ExpandURIUseCase; +import org.rdfarchitect.services.delete.DeleteResourcesUseCase; +import org.rdfarchitect.services.delete.FindOnDeleteRelationsUseCase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import java.util.List; import java.util.UUID; @RestController -@RequestMapping("api/datasets/{datasetName}/graphs/{graphURI}/uuid/{uuid}/deletion-impact") +@RequestMapping("api/datasets/{datasetName}/graphs/{graphURI}") @RequiredArgsConstructor -public class DeletionImpactRESTController { +public class DeleteRESTController { - private static final Logger logger = LoggerFactory.getLogger(DeletionImpactRESTController.class); + private static final Logger logger = LoggerFactory.getLogger(DeleteRESTController.class); private final ExpandURIUseCase expandURIUseCase; private final FindOnDeleteRelationsUseCase findOnDeleteRelationsUseCase; + private final DeleteResourcesUseCase deleteResourcesUseCase; //TODO: anpassen der beschreibung @Operation( @@ -51,7 +61,7 @@ public class DeletionImpactRESTController { @ApiResponse(responseCode = "200") } ) - @GetMapping + @GetMapping("/uuid/{uuid}/deletion-impact") public AffectedResource getDeletionImpact( @Parameter(description = "The name/url of the inquirer.") @RequestHeader(value = HttpHeaders.ORIGIN, required = false, defaultValue = "unknown") @@ -74,4 +84,37 @@ public AffectedResource getDeletionImpact( logger.info("Sending response to GET request: \"/api/datasets/{{}}/graphs/{{}}/uuid/{{}/deletion-impact\" from \"{}\".", datasetName, graphURI, uuid, originURL); return resultObj; } + + @Operation( + summary = "Deletes resources", + description = "Delete a list of resources.", + tags = {"graph"}, + responses = { + @ApiResponse(responseCode = "200") + } + ) + @PostMapping("/delete-requests") + public String deleteResources( + @Parameter(description = "The name/url of the inquirer.") + @RequestHeader(value = HttpHeaders.ORIGIN, required = false, defaultValue = "unknown") + String originURL, + @Parameter(description = "The literal name of the dataset.") + @PathVariable + String datasetName, + @Parameter(description = "The url encoded uri of the graph, or \"default\" to access the default graph.") + @PathVariable + String graphURI, + @Parameter(description = "A list of resource delete requests, each containing the uuid of the resource to delete and the type of deletion") + @RequestBody + List deleteRequests + ) { + logger.info("Received POST request: \"/api/datasets/{{}}/graphs/{{}}/delete\" from \"{}\".", datasetName, graphURI, originURL); + + var extendedGraphURI = expandURIUseCase.expandUri(datasetName, graphURI); + + deleteResourcesUseCase.deleteResources(new GraphIdentifier(datasetName, extendedGraphURI), deleteRequests); + + logger.info("Sending response to POST request: \"/api/datasets/{{}}/graphs/{{}}/delete\" from \"{}\".", datasetName, graphURI, originURL); + return "success"; + } } diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteActions.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteAction.java similarity index 62% rename from backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteActions.java rename to backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteAction.java index a5190c2a..1f33e203 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteActions.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteAction.java @@ -17,7 +17,7 @@ package org.rdfarchitect.api.dto.delete; -public enum DeleteActions { +public enum DeleteAction { /** * Delete the affected resource entirely. @@ -34,23 +34,14 @@ public enum DeleteActions { KEEP, /** - * Remove the reference to the deleted resource without - * deleting the affected resource itself. - * E.g. a class extends a deleted class — remove the - * inheritance relationship but keep the class. + * Remove the {@code cims:belongsToCategory} triple from a class + * whose package is being deleted. */ - REMOVE_REFERENCE, + REMOVE_PACKAGE_REFERENCE, - //nur falls delete von enum entries implementiert wird, aber eher unwahrscheinlich /** - * Unset the default value of an attribute when the - * enum entry used as its default value is deleted. + * Remove the {@code rdfs:subClassOf} triple from a class + * whose parent class is being deleted. */ - UNSET_DEFAULT_VALUE, - - /** - * Unset the fixed value of an attribute when the - * enum entry used as its fixed value is deleted. - */ - UNSET_FIXED_VALUE -} + REMOVE_SUBCLASS_REFERENCE; +} \ No newline at end of file diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/ResourceDeleteRequest.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/ResourceDeleteRequest.java index 7ec82960..52e5b468 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/ResourceDeleteRequest.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/ResourceDeleteRequest.java @@ -26,4 +26,5 @@ public class ResourceDeleteRequest { private UUID uuid; + private DeleteAction action; } diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java index 9674ec29..104869a5 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java @@ -20,7 +20,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; -import org.rdfarchitect.api.dto.delete.DeleteActions; +import org.rdfarchitect.api.dto.delete.DeleteAction; import org.rdfarchitect.api.dto.delete.ResourceIdentifier; import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils; @@ -40,7 +40,7 @@ public class AffectedResource { private AffectedResourceReason reason; - private List actions = new ArrayList<>(); + private List actions = new ArrayList<>(); private List children = new ArrayList<>(); diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java index 21c39b4c..8fc1e07a 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java @@ -17,14 +17,226 @@ package org.rdfarchitect.services.delete; +import lombok.RequiredArgsConstructor; +import org.apache.jena.query.TxnType; +import org.apache.jena.rdf.model.Model; +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.api.dto.delete.DeleteAction; import org.rdfarchitect.api.dto.delete.ResourceDeleteRequest; +import org.rdfarchitect.database.DatabasePort; +import org.rdfarchitect.database.GraphIdentifier; +import org.rdfarchitect.models.cim.rdf.resources.CIMS; +import org.rdfarchitect.models.cim.rdf.resources.RDFA; +import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils; +import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils.CimResourceType; +import org.rdfarchitect.rdf.graph.wrapper.GraphRewindable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import java.util.ArrayDeque; import java.util.List; +import java.util.Set; +@Service +@RequiredArgsConstructor public class DeleteResourcesService implements DeleteResourcesUseCase { + private static final Logger logger = LoggerFactory.getLogger(DeleteResourcesService.class); + + private final DatabasePort databasePort; + @Override - public void deleteResources(String graphIdentifier, List deleteRequests) { - //TODO: impl + public void deleteResources(GraphIdentifier graphIdentifier, List deleteRequests) { + GraphRewindable graph = null; + try { + graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); + graph.begin(TxnType.WRITE); + deleteResources(ModelFactory.createModelForGraph(graph), deleteRequests); + graph.commit(); + } finally { + if (graph != null) { + graph.end(); + } + } + } + + private void deleteResources(Model model, List deleteRequests) { + for (var deleteRequest : deleteRequests) { + try { + deleteResource(model, deleteRequest); + } catch (UnsupportedOperationException | IllegalArgumentException e) { + logger.warn("Skipping deletion of resource with UUID {} due to unsupported action: {} : {}", deleteRequest.getUuid(), deleteRequest.getAction(), e.getMessage()); + } catch (IllegalStateException e) { + logger.warn("Skipping deletion of resource with UUID {} due to illegal state: {}", deleteRequest.getUuid(), e.getMessage()); + } + } + } + + private void deleteResource(Model model, ResourceDeleteRequest deleteRequest) { + var resourceType = CIMResourceTypeIdentifyingUtils.getType(model, deleteRequest.getUuid()); + switch (resourceType) { + case PACKAGE -> deletePackage(model, deleteRequest); + case CLASS -> deleteClass(model, deleteRequest); + case ATTRIBUTE -> deleteAttribute(model, deleteRequest); + case ASSOCIATION -> deleteAssociation(model, deleteRequest); + case ENUM_ENTRY -> deleteEnumEntry(model, deleteRequest); + case ONTOLOGY -> deleteOntology(model, deleteRequest); + case UNKNOWN -> throw new IllegalArgumentException("Unknown resource type for resource with UUID: " + deleteRequest.getUuid()); + } + } + + /** + * Validates the given action against the supported actions for a resource type. + * + * @return {@code true} if the action is {@link DeleteAction#KEEP} and should be skipped, + * {@code false} if the action is supported and deletion should proceed. + * + * @throws IllegalArgumentException if the action is {@code null} + * @throws UnsupportedOperationException if the action is not in the supported set + */ + private boolean shouldSkipOrThrow(DeleteAction action, CimResourceType type, DeleteAction... supported) { + if (action == null) { + throw new IllegalArgumentException("Action must not be null"); + } + if (action == DeleteAction.KEEP) { + return true; + } + if (!Set.of(supported).contains(action)) { + throw new UnsupportedOperationException( + "Action " + action + " is not supported for " + type.name().toLowerCase().replace("_", " ") + "." + ); + } + return false; + } + + /** + * Removes a resource and all its transitively reachable blank nodes from the model. + * Nodes that are still referenced by other resources are not fully deleted; + * instead, only their {@code rdfa:uuid} triple is preserved to maintain referential integrity. + * + * @param model the RDF model to remove statements from + * @param resource the root resource to delete + */ + private void removeResource(Model model, Resource resource) { + var queue = new ArrayDeque(); + queue.add(resource); + + while (!queue.isEmpty()) { + var current = queue.poll(); + current.listProperties() + .toList() + .stream() + .filter(stmt -> stmt.getObject().isAnon()) + .forEach(stmt -> queue.add(stmt.getObject().asResource())); + + if (isReferencedElsewhere(model, current)) { + var uuidStmt = current.getProperty(RDFA.uuid); + model.removeAll(current, null, null); + if (uuidStmt != null) { + model.add(uuidStmt); + } + } else { + model.removeAll(current, null, null); + } + } + } + + private boolean isReferencedElsewhere(Model model, Resource resource) { + return model.listStatements(null, null, resource) + .filterDrop(stmt -> stmt.getPredicate().equals(RDFA.uuid)) + .hasNext(); + } + + private void deletePackage(Model model, ResourceDeleteRequest deleteRequest) { + if (shouldSkipOrThrow(deleteRequest.getAction(), CimResourceType.PACKAGE, DeleteAction.DELETE)) { + return; + } + var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); + removeResource(model, resource); + } + + /** + * Deletes a class and all its owned resources (attributes, associations, and enum entries). + * If the action is {@link DeleteAction#REMOVE_REFERENCE}, only the {@code rdfs:subClassOf} + * triple is removed, leaving the class itself intact. + */ + private void deleteClass(Model model, ResourceDeleteRequest deleteRequest) { + if (shouldSkipOrThrow(deleteRequest.getAction(), CimResourceType.CLASS, DeleteAction.DELETE, DeleteAction.REMOVE_SUBCLASS_REFERENCE, DeleteAction.REMOVE_PACKAGE_REFERENCE)) { + return; + } + var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); + if (deleteRequest.getAction() == DeleteAction.REMOVE_SUBCLASS_REFERENCE) { + model.remove(resource, RDFS.subClassOf, null); + return; + } + + if (deleteRequest.getAction() == DeleteAction.REMOVE_PACKAGE_REFERENCE) { + model.remove(resource, CIMS.belongsToCategory, null); + return; + } + + // Delete attributes and associations (linked via rdfs:domain) + model.listSubjectsWithProperty(RDFS.domain, resource) + .toList() + .forEach(owned -> removeResource(model, owned)); + + // Delete enum entries (linked via rdf:type) + model.listSubjectsWithProperty(RDF.type, resource) + .toList() + .forEach(entry -> removeResource(model, entry)); + + removeResource(model, resource); + } + + private void deleteAttribute(Model model, ResourceDeleteRequest deleteRequest) { + if (shouldSkipOrThrow(deleteRequest.getAction(), CimResourceType.ATTRIBUTE, DeleteAction.DELETE)) { + return; + } + var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); + removeResource(model, resource); + } + + /** + * Deletes an association and its inverse. Since associations reference each other + * via {@code cims:inverseRoleName}, the mutual references are removed first to + * prevent {@link #removeResource} from preserving stale UUID triples due to + * the circular reference. + */ + private void deleteAssociation(Model model, ResourceDeleteRequest deleteRequest) { + if (shouldSkipOrThrow(deleteRequest.getAction(), CimResourceType.ASSOCIATION, DeleteAction.DELETE)) { + return; + } + var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); + + var inverseStmt = resource.getProperty(CIMS.inverseRoleName); + if (inverseStmt != null && inverseStmt.getObject().isResource()) { + var inverse = inverseStmt.getObject().asResource(); + // Break circular reference before deleting + model.remove(inverse, CIMS.inverseRoleName, resource); + model.remove(resource, CIMS.inverseRoleName, inverse); + removeResource(model, inverse); + } + + removeResource(model, resource); + } + + private void deleteEnumEntry(Model model, ResourceDeleteRequest deleteRequest) { + if (shouldSkipOrThrow(deleteRequest.getAction(), CimResourceType.ENUM_ENTRY, DeleteAction.DELETE)) { + return; + } + var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); + removeResource(model, resource); + } + + private void deleteOntology(Model model, ResourceDeleteRequest deleteRequest) { + if (shouldSkipOrThrow(deleteRequest.getAction(), CimResourceType.ONTOLOGY, DeleteAction.DELETE)) { + return; + } + var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); + removeResource(model, resource); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesUseCase.java b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesUseCase.java index 55f158e6..64dc2c47 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesUseCase.java @@ -18,10 +18,11 @@ package org.rdfarchitect.services.delete; import org.rdfarchitect.api.dto.delete.ResourceDeleteRequest; +import org.rdfarchitect.database.GraphIdentifier; import java.util.List; public interface DeleteResourcesUseCase { - void deleteResources(String graphIdentifier, List deleteRequests); + void deleteResources(GraphIdentifier graphIdentifier, List deleteRequests); } diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java b/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java index d7a5d937..b287015b 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java @@ -25,7 +25,7 @@ import org.apache.jena.rdf.model.Resource; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; -import org.rdfarchitect.api.dto.delete.DeleteActions; +import org.rdfarchitect.api.dto.delete.DeleteAction; import org.rdfarchitect.api.dto.delete.ResourceIdentifier; import org.rdfarchitect.api.dto.delete.relations.AffectedAssociation; import org.rdfarchitect.api.dto.delete.relations.AffectedOwnedResource; @@ -56,7 +56,7 @@ public class FindOnDeleteRelationsService implements FindOnDeleteRelationsUseCas public AffectedResource getDeleteRelations(GraphIdentifier graphIdentifier, UUID uuid) { var model = ModelFactory.createModelForGraph(getCopyOfDatabaseGraph(graphIdentifier)); var resourceType = CIMResourceTypeIdentifyingUtils.getType(model, uuid); - var defaultActions = List.of(DeleteActions.DELETE); + var defaultActions = List.of(DeleteAction.DELETE); var reason = AffectedResourceReason.DELETION_REQUESTED_BY_USER; return switch (resourceType) { case PACKAGE -> findAffectedRelationsForPackage(model, uuid, reason, defaultActions); @@ -74,10 +74,10 @@ public AffectedResource getDeleteRelations(GraphIdentifier graphIdentifier, UUID }; } - private AffectedResource findAffectedRelationsForPackage(Model model, UUID uuid, AffectedResourceReason reason, List deleteActions) { + private AffectedResource findAffectedRelationsForPackage(Model model, UUID uuid, AffectedResourceReason reason, List deleteActions) { var classesInPackage = listClassesInPackage(model, uuid); var affectedResources = new ArrayList(); - var clsDeleteActions = List.of(DeleteActions.DELETE, DeleteActions.KEEP, DeleteActions.REMOVE_REFERENCE); + var clsDeleteActions = List.of(DeleteAction.DELETE, DeleteAction.KEEP, DeleteAction.REMOVE_PACKAGE_REFERENCE); for (var cls : classesInPackage) { var clsUuid = findUuidForResource(cls); var affectedClassResource = findAffectedRelationsForClass(model, clsUuid, AffectedResourceReason.CONTAINED_IN_PACKAGE, clsDeleteActions); @@ -89,7 +89,7 @@ private AffectedResource findAffectedRelationsForPackage(Model model, UUID uuid, } private AffectedResource findAffectedRelationsForClass(Model model, UUID uuid, - AffectedResourceReason reason, List deleteActions) { + AffectedResourceReason reason, List deleteActions) { var classResource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, uuid); var classResourceId = createResourceIdentifier(model, uuid); @@ -107,7 +107,7 @@ private AffectedResource findAffectedRelationsForClass(Model model, UUID uuid, } private List findAffectedAttributesForClass(Resource classResource, ResourceIdentifier classResourceId) { - var childActions = List.of(DeleteActions.DELETE, DeleteActions.KEEP); + var childActions = List.of(DeleteAction.DELETE, DeleteAction.KEEP); return listAttributesWithClassAsDatatype(classResource).stream() .map(attr -> new AffectedOwnedResource(createResourceIdentifier(attr), CimResourceType.ATTRIBUTE, @@ -118,24 +118,24 @@ private List findAffectedAttributesForClass(Resource classReso } private List findAffectedAssociationsForClass(Resource classResource, ResourceIdentifier classResourceId) { - var childActions = List.of(DeleteActions.DELETE, DeleteActions.KEEP); + var childActions = List.of(DeleteAction.DELETE, DeleteAction.KEEP); return listAssociationsReferencingClass(classResource).stream() .map(assoc -> new AffectedAssociation(createResourceIdentifier(assoc), CimResourceType.ASSOCIATION, AffectedResourceReason.REFENCES_DELETED_CLASS_VIA_ASSOCIATION, classResourceId, getAssociationTarget(assoc) - ) + ) .setActions(childActions)) .toList(); } private ResourceIdentifier getAssociationTarget(Resource associationResource) { var rangeStatement = associationResource.getProperty(RDFS.range); - if(rangeStatement == null){ + if (rangeStatement == null) { throw new IllegalStateException("Association " + associationResource + " does not have a range."); } - if(rangeStatement.getObject().isLiteral()){ + if (rangeStatement.getObject().isLiteral()) { throw new IllegalStateException("Association " + associationResource + " has a literal as range, which is not supported."); } var rangeResource = rangeStatement.getObject().asResource(); @@ -143,7 +143,7 @@ private ResourceIdentifier getAssociationTarget(Resource associationResource) { } private List findAffectedChildClassesForClass(Resource classResource) { - var childClassActions = List.of(DeleteActions.KEEP, DeleteActions.REMOVE_REFERENCE); + var childClassActions = List.of(DeleteAction.KEEP, DeleteAction.REMOVE_SUBCLASS_REFERENCE); return listDirectlyDescendingClasses(classResource).stream() .map(childClass -> new AffectedResource(createResourceIdentifier(childClass), CimResourceType.CLASS, diff --git a/frontend/src/lib/api/backend.js b/frontend/src/lib/api/backend.js index 35882ce1..02025d17 100644 --- a/frontend/src/lib/api/backend.js +++ b/frontend/src/lib/api/backend.js @@ -207,6 +207,16 @@ export class BackendConnection { }); } + async deleteResources(datasetName, graphURI, deleteRequests) { + let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/delete-requests`; + return await fetch(url, { + method: "POST", + headers: new Headers({ "Content-Type": "application/json" }), + body: JSON.stringify(deleteRequests), + credentials: "include", + }); + } + async postEnumEntry(datasetName, graphURI, classUUID, enumEntry) { let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/classes/${encodeURIComponent(classUUID)}/enumentries`; return await fetch(url, { diff --git a/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte b/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte index 1676146d..43b9f456 100644 --- a/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte +++ b/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte @@ -38,7 +38,7 @@ let deleteDependencies = $state(null); - /** @type {Map} uuid -> selected action */ + /** @type {Map} "uuid::reason" -> selected action */ let selectedActions = $state(new Map()); let type = $derived(deleteDependencies?.type.toLowerCase()); @@ -46,9 +46,12 @@ /** Ordered list of actions that exist anywhere in the tree */ let availableActions = $derived( deleteDependencies - ? ["DELETE", "KEEP", "REMOVE_REFERENCE"].filter(a => - collectActions(deleteDependencies).has(a), - ) + ? [ + "DELETE", + "KEEP", + "REMOVE_PACKAGE_REFERENCE", + "REMOVE_SUBCLASS_REFERENCE", + ].filter(a => collectActions(deleteDependencies).has(a)) : [], ); @@ -80,8 +83,9 @@ * @param {object} node */ function initSelectedActions(node) { + const key = `${node.resourceIdentifier.uuid}::${node.reason}`; const defaultAction = getDefaultAction(node); - selectedActions.set(node.resourceIdentifier.uuid, defaultAction); + selectedActions.set(key, defaultAction); if (node.children) { for (const child of node.children) { initSelectedActions(child); @@ -124,9 +128,8 @@ */ function buildPayload(node, parentActive = true, result = []) { if (!parentActive) return result; - const action = - selectedActions.get(node.resourceIdentifier.uuid) ?? - node.actions[0]; + const key = `${node.resourceIdentifier.uuid}::${node.reason}`; + const action = selectedActions.get(key) ?? node.actions[0]; result.push({ uuid: node.resourceIdentifier.uuid, action }); if (node.children) { for (const child of node.children) { @@ -140,11 +143,16 @@ if (!deleteDependencies) return; const payload = buildPayload(deleteDependencies); console.log("Submit delete with selections:", payload); - // TODO: await bec.submitDelete(datasetName, graphUri, payload); + let res = await bec.deleteResources(datasetName, graphUri, payload); + if (!res.ok) { + console.error("Failed to delete resources:", await res.text()); + } else { + console.log("Successfully submitted delete request"); + } } - function onSelectAction(uuid, action) { - selectedActions.set(uuid, action); + function onSelectAction(key, action) { + selectedActions.set(key, action); selectedActions = new Map(selectedActions); } diff --git a/frontend/src/routes/delete-relations-dialog/DeleteDependencyNode.svelte b/frontend/src/routes/delete-relations-dialog/DeleteDependencyNode.svelte index e9f83764..08a10448 100644 --- a/frontend/src/routes/delete-relations-dialog/DeleteDependencyNode.svelte +++ b/frontend/src/routes/delete-relations-dialog/DeleteDependencyNode.svelte @@ -34,7 +34,12 @@ node, selectedActions, onSelectAction, - availableActions = ["DELETE", "KEEP", "REMOVE_REFERENCE"], + availableActions = [ + "DELETE", + "KEEP", + "REMOVE_PACKAGE_REFERENCE", + "REMOVE_SUBCLASS_REFERENCE", + ], depth = 0, isRoot = false, disabled = false, @@ -64,21 +69,28 @@ tooltip: "Keep this resource and its reference, even if the referenced target is deleted", }, - REMOVE_REFERENCE: { + REMOVE_PACKAGE_REFERENCE: { icon: faLinkSlash, text: "Remove ref", variant: "default", width: "w-30", - tooltip: - "Keep this resource but remove its reference to the deleted target", + tooltip: "Keep this resource but remove its package reference", + }, + REMOVE_SUBCLASS_REFERENCE: { + icon: faLinkSlash, + text: "Remove ref", + variant: "default", + width: "w-30", + tooltip: "Keep this resource but remove its inheritance reference", }, }; let expanded = $state(isRoot); let hasChildren = $derived(node.children?.length > 0); + let actionKey = $derived(`${node.resourceIdentifier.uuid}::${node.reason}`); let currentAction = $derived( - selectedActions.get(node.resourceIdentifier.uuid) ?? node.actions[0], + selectedActions.get(actionKey) ?? node.actions[0], ); let typeBadge = $derived(node.type); let isAssociation = $derived(typeBadge === "ASSOCIATION"); @@ -93,7 +105,7 @@ } function selectAction(action) { - onSelectAction(node.resourceIdentifier.uuid, action); + onSelectAction(actionKey, action); } From 3ea8e4937abb7d3b0b2a7d696e209e4f079f7ad8 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Wed, 15 Apr 2026 08:58:06 +0200 Subject: [PATCH 06/31] started fixing bugs --- .../datasets/graphs/DeleteRESTController.java | 9 ++--- .../CIMResourceTypeIdentifyingUtils.java | 8 +++- .../delete/DeleteResourcesService.java | 39 ++++++++++++------- .../DeleteDependenciesDialog.svelte | 2 + 4 files changed, 38 insertions(+), 20 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeleteRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeleteRESTController.java index 7033127f..9a83c580 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeleteRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeleteRESTController.java @@ -52,10 +52,9 @@ public class DeleteRESTController { private final FindOnDeleteRelationsUseCase findOnDeleteRelationsUseCase; private final DeleteResourcesUseCase deleteResourcesUseCase; - //TODO: anpassen der beschreibung @Operation( - summary = "resolve iri identifier", - description = "Resolve iri identifier of a cim resource to its uuid.", + summary = "Get deletion impact", + description = "Returns a tree of affected resources for deleting the resource with the given UUID.", tags = {"graph"}, responses = { @ApiResponse(responseCode = "200") @@ -86,8 +85,8 @@ public AffectedResource getDeletionImpact( } @Operation( - summary = "Deletes resources", - description = "Delete a list of resources.", + summary = "Delete resources", + description = "Processes a list of delete requests, each specifying a resource UUID and the desired action.", tags = {"graph"}, responses = { @ApiResponse(responseCode = "200") diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java index ddc888c7..6ded271c 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java @@ -76,7 +76,7 @@ public Resource findUniqueSubject(Model model, UUID uuid) { return subjects.getFirst(); } - private boolean isEnumEntry(Resource subject) { + public boolean isEnumEntry(Resource subject) { var types = subject.listProperties(RDF.type).toList(); if (types.size() != 1 || !types.getFirst().getObject().isURIResource()) { return false; @@ -84,4 +84,10 @@ private boolean isEnumEntry(Resource subject) { return types.getFirst().getObject().asResource() .hasProperty(CIMS.stereotype, CIMStereotypes.enumeration); } + + public boolean isExternalResource(Resource resource) { + return resource.listProperties() + .filterDrop(stmt -> stmt.getPredicate().equals(RDFA.uuid)) + .hasNext(); + } } diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java index 8fc1e07a..ba04d4dd 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java @@ -32,6 +32,7 @@ import org.rdfarchitect.models.cim.rdf.resources.RDFA; import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils; import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils.CimResourceType; +import org.rdfarchitect.models.cim.relations.model.properties.CIMPropertyUtils; import org.rdfarchitect.rdf.graph.wrapper.GraphRewindable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -118,12 +119,12 @@ private boolean shouldSkipOrThrow(DeleteAction action, CimResourceType type, Del * Nodes that are still referenced by other resources are not fully deleted; * instead, only their {@code rdfa:uuid} triple is preserved to maintain referential integrity. * - * @param model the RDF model to remove statements from * @param resource the root resource to delete */ - private void removeResource(Model model, Resource resource) { + private void removeResource(Resource resource) { var queue = new ArrayDeque(); queue.add(resource); + var model = resource.getModel(); while (!queue.isEmpty()) { var current = queue.poll(); @@ -156,7 +157,7 @@ private void deletePackage(Model model, ResourceDeleteRequest deleteRequest) { return; } var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); - removeResource(model, resource); + removeResource(resource); } /** @@ -165,7 +166,8 @@ private void deletePackage(Model model, ResourceDeleteRequest deleteRequest) { * triple is removed, leaving the class itself intact. */ private void deleteClass(Model model, ResourceDeleteRequest deleteRequest) { - if (shouldSkipOrThrow(deleteRequest.getAction(), CimResourceType.CLASS, DeleteAction.DELETE, DeleteAction.REMOVE_SUBCLASS_REFERENCE, DeleteAction.REMOVE_PACKAGE_REFERENCE)) { + if (shouldSkipOrThrow(deleteRequest.getAction(), CimResourceType.CLASS, DeleteAction.DELETE, DeleteAction.REMOVE_SUBCLASS_REFERENCE, + DeleteAction.REMOVE_PACKAGE_REFERENCE)) { return; } var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); @@ -179,17 +181,26 @@ private void deleteClass(Model model, ResourceDeleteRequest deleteRequest) { return; } - // Delete attributes and associations (linked via rdfs:domain) + // Delete attributes model.listSubjectsWithProperty(RDFS.domain, resource) + .filterKeep(CIMPropertyUtils::isAttribute) .toList() - .forEach(owned -> removeResource(model, owned)); + .forEach(this::removeResource); - // Delete enum entries (linked via rdf:type) + //delete associations only if it references an external resource + model.listSubjectsWithProperty(RDFS.domain, resource) + .filterKeep(CIMPropertyUtils::isAssociation) + .filterKeep(assoc -> CIMResourceTypeIdentifyingUtils.isExternalResource(assoc.getProperty(RDFS.domain).getObject().asResource())) + .toList() + .forEach(this::removeResource); + + // Delete enum entries model.listSubjectsWithProperty(RDF.type, resource) + .filterKeep(CIMResourceTypeIdentifyingUtils::isEnumEntry) .toList() - .forEach(entry -> removeResource(model, entry)); + .forEach(this::removeResource); - removeResource(model, resource); + removeResource(resource); } private void deleteAttribute(Model model, ResourceDeleteRequest deleteRequest) { @@ -197,7 +208,7 @@ private void deleteAttribute(Model model, ResourceDeleteRequest deleteRequest) { return; } var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); - removeResource(model, resource); + removeResource(resource); } /** @@ -218,10 +229,10 @@ private void deleteAssociation(Model model, ResourceDeleteRequest deleteRequest) // Break circular reference before deleting model.remove(inverse, CIMS.inverseRoleName, resource); model.remove(resource, CIMS.inverseRoleName, inverse); - removeResource(model, inverse); + removeResource(inverse); } - removeResource(model, resource); + removeResource(resource); } private void deleteEnumEntry(Model model, ResourceDeleteRequest deleteRequest) { @@ -229,7 +240,7 @@ private void deleteEnumEntry(Model model, ResourceDeleteRequest deleteRequest) { return; } var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); - removeResource(model, resource); + removeResource(resource); } private void deleteOntology(Model model, ResourceDeleteRequest deleteRequest) { @@ -237,6 +248,6 @@ private void deleteOntology(Model model, ResourceDeleteRequest deleteRequest) { return; } var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); - removeResource(model, resource); + removeResource(resource); } } \ No newline at end of file diff --git a/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte b/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte index 43b9f456..c8b8ddb0 100644 --- a/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte +++ b/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte @@ -24,6 +24,7 @@ import { getDefaultAction } from "./deleteDependencyDefaults.js"; import DeleteDependencyNode from "./DeleteDependencyNode.svelte"; + import { forceReloadTrigger } from "$lib/sharedState.svelte.js"; let { showDialog = $bindable(), @@ -148,6 +149,7 @@ console.error("Failed to delete resources:", await res.text()); } else { console.log("Successfully submitted delete request"); + forceReloadTrigger.trigger(); } } From 07f2170cf84e150bf3de3d554b83eb8e3844bf49 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Wed, 15 Apr 2026 08:58:24 +0200 Subject: [PATCH 07/31] started fixing bugs --- .../delete-relations-dialog/DeleteDependenciesDialog.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte b/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte index c8b8ddb0..d4c7037c 100644 --- a/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte +++ b/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte @@ -21,10 +21,10 @@ import { BackendConnection } from "$lib/api/backend.js"; import { PUBLIC_BACKEND_URL } from "$lib/config/runtime.js"; import ActionDialog from "$lib/dialog/ActionDialog.svelte"; + import { forceReloadTrigger } from "$lib/sharedState.svelte.js"; import { getDefaultAction } from "./deleteDependencyDefaults.js"; import DeleteDependencyNode from "./DeleteDependencyNode.svelte"; - import { forceReloadTrigger } from "$lib/sharedState.svelte.js"; let { showDialog = $bindable(), From 9606a38ad92f9da2c3ec8f6fba3c3677dba29008 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Wed, 15 Apr 2026 09:53:07 +0200 Subject: [PATCH 08/31] check before rebase --- .../cim/relations/model/CIMResourceTypeIdentifyingUtils.java | 2 +- .../rdfarchitect/services/delete/DeleteResourcesService.java | 2 +- .../delete-relations-dialog/DeleteDependenciesDialog.svelte | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java index 6ded271c..f51e7880 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java @@ -86,7 +86,7 @@ public boolean isEnumEntry(Resource subject) { } public boolean isExternalResource(Resource resource) { - return resource.listProperties() + return !resource.listProperties() .filterDrop(stmt -> stmt.getPredicate().equals(RDFA.uuid)) .hasNext(); } diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java index ba04d4dd..fe6be3df 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java @@ -162,7 +162,7 @@ private void deletePackage(Model model, ResourceDeleteRequest deleteRequest) { /** * Deletes a class and all its owned resources (attributes, associations, and enum entries). - * If the action is {@link DeleteAction#REMOVE_REFERENCE}, only the {@code rdfs:subClassOf} + * If the action is {@link DeleteAction#REMOVE_SUBCLASS_REFERENCE}, only the {@code rdfs:subClassOf} * triple is removed, leaving the class itself intact. */ private void deleteClass(Model model, ResourceDeleteRequest deleteRequest) { diff --git a/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte b/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte index d4c7037c..27260fea 100644 --- a/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte +++ b/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte @@ -22,6 +22,7 @@ import { PUBLIC_BACKEND_URL } from "$lib/config/runtime.js"; import ActionDialog from "$lib/dialog/ActionDialog.svelte"; import { forceReloadTrigger } from "$lib/sharedState.svelte.js"; + import { editorState } from "$lib/sharedState.svelte.js"; import { getDefaultAction } from "./deleteDependencyDefaults.js"; import DeleteDependencyNode from "./DeleteDependencyNode.svelte"; @@ -150,6 +151,9 @@ } else { console.log("Successfully submitted delete request"); forceReloadTrigger.trigger(); + editorState.selectedClassDataset.updateValue(null); + editorState.selectedClassGraph.updateValue(null); + editorState.selectedClassUUID.updateValue(null); } } From a8b4604dfc8a4a80518123f92ca847cd440e5eed Mon Sep 17 00:00:00 2001 From: Maximilian Date: Wed, 15 Apr 2026 10:53:29 +0200 Subject: [PATCH 09/31] fixed bug, where associations to external classes caused the classeditor to not load --- .../classes/AllClassesRESTController.java | 149 +++---- .../services/select/GetClassListUseCase.java | 10 +- .../services/select/QueryGraphService.java | 363 +++++++++--------- frontend/src/lib/api/backend.js | 4 +- .../classEditor/fetch-class-editor-context.js | 2 +- 5 files changed, 241 insertions(+), 287 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/AllClassesRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/AllClassesRESTController.java index d3fbb40e..6d8f022d 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/AllClassesRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/AllClassesRESTController.java @@ -23,9 +23,7 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; - import lombok.RequiredArgsConstructor; - import org.rdfarchitect.api.dto.ClassUMLAdaptedDTO; import org.rdfarchitect.api.dto.packages.PackageDTO; import org.rdfarchitect.database.GraphIdentifier; @@ -41,6 +39,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; @@ -57,122 +56,84 @@ public class AllClassesRESTController { private final AddClassUseCase addClassUseCase; /** - * Helper record, functions as DTO for accepting the necessary information for adding a new - * class + * Helper record, functions as DTO for accepting the necessary information for adding a new class * - * @param packageDTO PackageDTO object of the package to which the new class is going to be - * added + * @param packageDTO PackageDTO object of the package to which the new class is going to be added * @param classURIPrefix URI Prefix of the new class - * @param className Label of the new class + * @param className Label of the new class */ - public record AddNewClassRequest( - PackageDTO packageDTO, String classURIPrefix, String className) {} + public record AddNewClassRequest(PackageDTO packageDTO, String classURIPrefix, String className) { + + } @Operation( - summary = "create new class", - description = - "Create a new class with default name and no attributes, stereotypes or associations. Because no concrete stereotype is added the class is abstract " - + "by default.", - tags = {"graph"}) + summary = "create new class", + description = "Create a new class with default name and no attributes, stereotypes or associations. Because no concrete stereotype is added the class is abstract " + + "by default.", + tags = {"graph"} + ) @PostMapping public String addClass( - @Parameter(description = "The name/url of the inquirer.") - @RequestHeader( - value = HttpHeaders.ORIGIN, - required = false, - defaultValue = "unknown") - String originURL, - @Parameter(description = "The literal name of the dataset.") @PathVariable - String datasetName, - @Parameter( - description = - "The url encoded uri of the graph, or \"default\" to access the default graph.") - @PathVariable - String graphURI, - @io.swagger.v3.oas.annotations.parameters.RequestBody( - required = true, - description = - "Helper record, functions as DTO for accepting the necessary information for adding a new class") - @RequestBody - AddNewClassRequest addNewClassRequest) { - logger.info( - "Received POST request: \"/api/datasets/{{}}/graphs/{{}}/classes\" from \"{}\".", - datasetName, - graphURI, - originURL); + @Parameter(description = "The name/url of the inquirer.") + @RequestHeader(value = HttpHeaders.ORIGIN, required = false, defaultValue = "unknown") + String originURL, + @Parameter(description = "The literal name of the dataset.") + @PathVariable + String datasetName, + @Parameter(description = "The url encoded uri of the graph, or \"default\" to access the default graph.") + @PathVariable + String graphURI, + @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + description = "Helper record, functions as DTO for accepting the necessary information for adding a new class" + ) + @RequestBody AddNewClassRequest addNewClassRequest) { + logger.info("Received POST request: \"/api/datasets/{{}}/graphs/{{}}/classes\" from \"{}\".", datasetName, graphURI, originURL); var extendedGraphURI = expandURIUseCase.expandUri(datasetName, graphURI); - var extendedClassURIPrefix = - expandURIUseCase.expandUri(datasetName, addNewClassRequest.classURIPrefix); + var extendedClassURIPrefix = expandURIUseCase.expandUri(datasetName, addNewClassRequest.classURIPrefix); var graphIdentifier = new GraphIdentifier(datasetName, extendedGraphURI); - var classUUID = - addClassUseCase.addClass( - graphIdentifier, - addNewClassRequest.packageDTO, - extendedClassURIPrefix, - addNewClassRequest.className); + var classUUID =addClassUseCase.addClass(graphIdentifier, addNewClassRequest.packageDTO, extendedClassURIPrefix, addNewClassRequest.className); - logger.info( - "Sending response to POST request: \"/api/datasets/{{}}/graphs/{{}}/classes\" to \"{}\".", - datasetName, - graphURI, - originURL); + logger.info("Sending response to POST request: \"/api/datasets/{{}}/graphs/{{}}/classes\" to \"{}\".", datasetName, graphURI, originURL); return classUUID.toString(); } @Operation( - summary = "list classes", - description = - "Get a list containing all classes. Doesn't include: stereotypes, attributes and associations.", - tags = {"graph"}, - responses = { - @ApiResponse( + summary = "list classes", + description = "Get a list containing all classes. Doesn't include: stereotypes, attributes and associations.", + tags = {"graph"}, + responses = {@ApiResponse( responseCode = "200", - content = - @Content( - mediaType = "application/json", - array = - @ArraySchema( - schema = - @Schema( - implementation = - ClassUMLAdaptedDTO - .class)))) - }) + content = @Content( + mediaType = "application/json", + array = @ArraySchema(schema = @Schema(implementation = ClassUMLAdaptedDTO.class)) + )) + } + ) @GetMapping public List getClassList( - @Parameter(description = "The name/url of the inquirer.") - @RequestHeader( - value = HttpHeaders.ORIGIN, - required = false, - defaultValue = "unknown") - String originURL, - @Parameter(description = "The literal name of the dataset.") @PathVariable - String datasetName, - @Parameter( - description = - "The url encoded uri of the graph, or \"default\" to access the default graph.") - @PathVariable - String graphURI) { - logger.info( - "Received GET request: \"/api/datasets/{{}}/graphs/{{}}/classes\" from \"{}\".", - datasetName, - graphURI, - originURL); + @Parameter(description = "The name/url of the inquirer.") + @RequestHeader(value = HttpHeaders.ORIGIN, required = false, defaultValue = "unknown") + String originURL, + @Parameter(description = "The literal name of the dataset.") + @PathVariable + String datasetName, + @Parameter(description = "The url encoded uri of the graph, or \"default\" to access the default graph.") + @PathVariable + String graphURI, + @Parameter(description = "Whether to include external classes.") + @RequestParam(required = false, defaultValue = "false") + boolean includeExternalClasses){ + logger.info("Received GET request: \"/api/datasets/{{}}/graphs/{{}}/classes\" from \"{}\".", datasetName, graphURI, originURL); var extendedGraphURI = expandURIUseCase.expandUri(datasetName, graphURI); - var cimClassList = - getClassListUseCase.getClassList( - new GraphIdentifier(datasetName, extendedGraphURI)); + var cimClassList = getClassListUseCase.getClassList(new GraphIdentifier(datasetName, extendedGraphURI), includeExternalClasses); - logger.info( - "Sending response to GET request: \"/api/datasets/{{}}/graphs/{{}}/classes\" to \"{}\".", - datasetName, - graphURI, - originURL); + logger.info("Sending response to GET request: \"/api/datasets/{{}}/graphs/{{}}/classes\" to \"{}\".", datasetName, graphURI, originURL); return cimClassList; } } diff --git a/backend/src/main/java/org/rdfarchitect/services/select/GetClassListUseCase.java b/backend/src/main/java/org/rdfarchitect/services/select/GetClassListUseCase.java index b7f1e69a..81597c78 100644 --- a/backend/src/main/java/org/rdfarchitect/services/select/GetClassListUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/select/GetClassListUseCase.java @@ -24,5 +24,13 @@ public interface GetClassListUseCase { - List getClassList(GraphIdentifier graphIdentifier); + /** + * Gets the list of classes in the graph. + * + * @param graphIdentifier The graph to getClassDefinition. + * @param includeExternalClasses Whether to include external classes in the result. + * + * @return The list of classes in the graph. + */ + List getClassList(GraphIdentifier graphIdentifier,boolean includeExternalClasses); } diff --git a/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java b/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java index 051737be..a0ac1c8c 100644 --- a/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java +++ b/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java @@ -17,14 +17,11 @@ package org.rdfarchitect.services.select; -import static org.rdfarchitect.models.cim.queries.select.CIMQueryBuilder.Mode.OPTIONAL; -import static org.rdfarchitect.models.cim.queries.select.CIMQueryBuilder.Mode.REQUIRED; -import static org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs.removeUUIDs; - import lombok.RequiredArgsConstructor; - import org.apache.jena.arq.querybuilder.SelectBuilder; import org.apache.jena.graph.Node; +import org.apache.jena.query.Query; +import org.apache.jena.query.QueryFactory; import org.apache.jena.query.TxnType; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.riot.RDFFormat; @@ -65,17 +62,13 @@ import java.util.UUID; import java.util.stream.Collectors; +import static org.rdfarchitect.models.cim.queries.select.CIMQueryBuilder.Mode.*; +import static org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs.*; + @Service @RequiredArgsConstructor -public class QueryGraphService - implements GetClassListUseCase, - ListDatatypesUseCase, - GetSchemaUseCase, - ListInternalPackagesUseCase, - ListExternalPackagesUseCase, - ListPrimitivesUseCase, - ListStereotypesUseCase, - ResolveIdentifierUseCase { +public class QueryGraphService implements GetClassListUseCase, ListDatatypesUseCase, GetSchemaUseCase, ListInternalPackagesUseCase, ListExternalPackagesUseCase, + ListPrimitivesUseCase, ListStereotypesUseCase, ResolveIdentifierUseCase { private static final String BLANK_PACKAGE_NAME = "default"; private static final String BLANK_PACKAGE_LANG = "en"; @@ -85,38 +78,62 @@ public class QueryGraphService private final PackageMapper packageMapper; @Override - public List getClassList(GraphIdentifier graphIdentifier) { - // build query - var baseQuery = - new CIMBaseQueryBuilder() - .setGraph(graphIdentifier.getGraphUri()) - .addPrefixes( - databasePort.getPrefixMapping(graphIdentifier.getDatasetName())) - .setOrder() - .setDistinct() - .setType(RDFS.Class) - .build(); - var query = - new CIMQueryBuilder(baseQuery) - .appendUUIDQuery(OPTIONAL) - .appendLabelQuery(OPTIONAL) - .appendPackageQuery(OPTIONAL) - .appendCommentQuery(OPTIONAL) - .appendSuperClassQuery(OPTIONAL) - .build(); - - // execute query - var queryResultSet = - InMemorySparqlExecutor.executeSingleQuery( - databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), - query, - graphIdentifier.getGraphUri()); - - // format results + public List getClassList(GraphIdentifier graphIdentifier, boolean includeExternalClasses) { + var classFilter = includeExternalClasses + ? """ + { + ?uri rdf:type rdfs:Class . + } + UNION + { + ?_any rdfs:domain ?uri . + } + """ : "?uri rdf:type rdfs:Class ."; + + var query = """ + PREFIX cims: + PREFIX rdf: + PREFIX owl: + PREFIX cim: + PREFIX xsd: + PREFIX rdfs: + PREFIX dc: + + SELECT DISTINCT ?uri ?uuid ?label ?packageURI ?packageLabel ?packageUUID ?comment ?superClassURI ?superClassLabel + WHERE + { + %s + ?uri ?uuid + OPTIONAL + { ?uri rdfs:label ?label} + OPTIONAL + { ?uri cims:belongsToCategory ?packageURI + OPTIONAL + { ?packageURI rdfs:label ?packageLabel} + OPTIONAL + { ?packageURI ?packageUUID} + } + OPTIONAL + { ?uri rdfs:comment ?comment} + OPTIONAL + { ?uri rdfs:subClassOf ?superClassURI + OPTIONAL + { ?superClassURI + rdfs:label ?superClassLabel} + } + } + ORDER BY ?uri + """.formatted(classFilter); + + //execute query + var queryResultSet = InMemorySparqlExecutor.executeSingleQuery(databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), QueryFactory.create(query), null); + + //format results var cimClassList = CIMUMLObjectFactory.createCIMClassUMLAdaptedList(queryResultSet); var referencedClassList = getReferencedClassList(graphIdentifier); - var existingUuids = - cimClassList.stream().map(CIMClassUMLAdapted::getUuid).collect(Collectors.toSet()); + var existingUuids = cimClassList.stream() + .map(CIMClassUMLAdapted::getUuid) + .collect(Collectors.toSet()); for (var referencedClass : referencedClassList) { if (!existingUuids.contains(referencedClass.getUuid())) { @@ -128,57 +145,47 @@ public List getClassList(GraphIdentifier graphIdentifier) { return classMapper.toDTOList(cimClassList); } - private List getReferencedClassList(GraphIdentifier graphIdentifier) { - var query = - new SelectBuilder() - .setDistinct(true) - .addVar("?uri") - .addVar("?uuid") - .addUnion(new SelectBuilder().addWhere("?subject", RDFS.domain, "?uri")) - .addUnion(new SelectBuilder().addWhere("?subject", CIMS.datatype, "?uri")) - .addOptional("?uri", RDFA.uuid, "?uuid") - .build(); - - // execute query - var queryResultSet = - InMemorySparqlExecutor.executeSingleQuery( - databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), - query, - graphIdentifier.getGraphUri()); - - // format results + private List getReferencedClassList(GraphIdentifier graphIdentifier) { + var query = new SelectBuilder() + .setDistinct(true) + .addVar("?uri") + .addVar("?uuid") + .addUnion(new SelectBuilder() + .addWhere("?subject", RDFS.domain, "?uri")) + .addUnion(new SelectBuilder() + .addWhere("?subject", CIMS.datatype, "?uri")) + .addOptional("?uri", RDFA.uuid, "?uuid") + .build(); + + //execute query + var queryResultSet = InMemorySparqlExecutor.executeSingleQuery(databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), query, graphIdentifier.getGraphUri()); + + //format results return CIMUMLObjectFactory.createCIMClassUMLAdaptedList(queryResultSet); } @Override public List listDatatypes(GraphIdentifier graphIdentifier) { - // build query - var baseQuery = - new CIMBaseQueryBuilder() - .addPrefixes( - databasePort.getPrefixMapping(graphIdentifier.getDatasetName())) - .setGraph(graphIdentifier.getGraphUri()) - .setOrder() - .setDistinct() - .setType(RDFS.Class) - .filterStereotypes(CIMStereotypes.enumeration.getURI(), "Entsoe") - .build(); - var query = - new CIMQueryBuilder(baseQuery) - .appendLabelQuery(OPTIONAL) - .appendPackageQuery(OPTIONAL) - .appendCommentQuery(OPTIONAL) - .appendSuperClassQuery(OPTIONAL) - .build(); - - // execute query - var queryResultSet = - InMemorySparqlExecutor.executeSingleQuery( - databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), - query, - graphIdentifier.getGraphUri()); - - // format results + //build query + var baseQuery = new CIMBaseQueryBuilder() + .addPrefixes(databasePort.getPrefixMapping(graphIdentifier.getDatasetName())) + .setGraph(graphIdentifier.getGraphUri()) + .setOrder() + .setDistinct() + .setType(RDFS.Class) + .filterStereotypes(CIMStereotypes.enumeration.getURI(), "Entsoe") + .build(); + var query = new CIMQueryBuilder(baseQuery) + .appendLabelQuery(OPTIONAL) + .appendPackageQuery(OPTIONAL) + .appendCommentQuery(OPTIONAL) + .appendSuperClassQuery(OPTIONAL) + .build(); + + //execute query + var queryResultSet = InMemorySparqlExecutor.executeSingleQuery(databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), query, graphIdentifier.getGraphUri()); + + //format results var cimClassList = CIMUMLObjectFactory.createCIMClassUMLAdaptedList(queryResultSet); cimClassList.forEach(CIMClassUMLAdapted::nullEmptyLists); @@ -192,9 +199,7 @@ public ByteArrayOutputStream getSchema(GraphIdentifier graphIdentifier, RDFForma graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); graph.begin(TxnType.READ); var copiedGraph = GraphUtils.deepCopy(graph); - copiedGraph - .getPrefixMapping() - .setNsPrefixes(databasePort.getPrefixMapping(graphIdentifier.getDatasetName())); + copiedGraph.getPrefixMapping().setNsPrefixes(databasePort.getPrefixMapping(graphIdentifier.getDatasetName())); removeUUIDs(copiedGraph); var sortedModel = new CimSortedModel(ModelFactory.createModelForGraph(copiedGraph)); sortedModel.write(out, format.getLang().getName()); @@ -210,41 +215,35 @@ public ByteArrayOutputStream getSchema(GraphIdentifier graphIdentifier, RDFForma @Override public List listInternalPackages(GraphIdentifier graphIdentifier) { - // build package query - var internalPackageBaseQuery = - new CIMBaseQueryBuilder() - .setDistinct() - .addPrefixes( - databasePort.getPrefixMapping(graphIdentifier.getDatasetName())) - .setGraph(graphIdentifier.getGraphUri()) - .setType(CIMS.classCategory) - .build(); - - var internalPackageQuery = - new CIMQueryBuilder(internalPackageBaseQuery) - .appendUUIDQuery(REQUIRED) - .appendLabelQuery(REQUIRED) - .appendPackageQuery(OPTIONAL) - .appendCommentQuery(OPTIONAL) - .build(); - - // execute package query + //build package query + var internalPackageBaseQuery = new CIMBaseQueryBuilder() + .setDistinct() + .addPrefixes(databasePort.getPrefixMapping(graphIdentifier.getDatasetName())) + .setGraph(graphIdentifier.getGraphUri()) + .setType(CIMS.classCategory) + .build(); + + var internalPackageQuery = new CIMQueryBuilder(internalPackageBaseQuery) + .appendUUIDQuery(REQUIRED) + .appendLabelQuery(REQUIRED) + .appendPackageQuery(OPTIONAL) + .appendCommentQuery(OPTIONAL) + .build(); + + //execute package query var internalPackageQueryResultSet = - InMemorySparqlExecutor.executeSingleQuery( - databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), - internalPackageQuery, - graphIdentifier.getGraphUri()); + InMemorySparqlExecutor.executeSingleQuery(databasePort.getGraphWithContext(graphIdentifier) + .getRdfGraph(), internalPackageQuery, graphIdentifier.getGraphUri()); - // format results + //format results var cimPackageList = CIMObjectFactory.createCIMPackageList(internalPackageQueryResultSet); - // add blank package + //add blank package URI uri = new URI(BLANK_PACKAGE_NAME); - var blankPackage = - CIMPackage.builder() - .uri(uri) - .label(new RDFSLabel(BLANK_PACKAGE_NAME, BLANK_PACKAGE_LANG)) - .build(); + var blankPackage = CIMPackage.builder() + .uri(uri) + .label(new RDFSLabel(BLANK_PACKAGE_NAME, BLANK_PACKAGE_LANG)) + .build(); cimPackageList.add(blankPackage); return packageMapper.toDTOList(cimPackageList); @@ -252,61 +251,50 @@ public List listInternalPackages(GraphIdentifier graphIdentifier) { @Override public List listExternalPackages(GraphIdentifier graphIdentifier) { - // build external package query - var externalPackageBaseQuery = - new CIMBaseQueryBuilder() - .setDistinct() - .addPrefixes( - databasePort.getPrefixMapping(graphIdentifier.getDatasetName())) - .setGraph(graphIdentifier.getGraphUri()) - .addWhereThisNotExists(RDF.type.getURI(), CIMS.classCategory.getURI()) - .build() - .addWhere(Node.ANY, CIMS.belongsToCategory.asNode(), CIMQueryVars.URI); - - var externalPackageQuery = - new CIMQueryBuilder(externalPackageBaseQuery).appendUUIDQuery(REQUIRED).build(); - - // execute external package query + //build external package query + var externalPackageBaseQuery = new CIMBaseQueryBuilder() + .setDistinct() + .addPrefixes(databasePort.getPrefixMapping(graphIdentifier.getDatasetName())) + .setGraph(graphIdentifier.getGraphUri()) + .addWhereThisNotExists(RDF.type.getURI(), CIMS.classCategory.getURI()) + .build() + .addWhere(Node.ANY, CIMS.belongsToCategory.asNode(), CIMQueryVars.URI); + + var externalPackageQuery = new CIMQueryBuilder(externalPackageBaseQuery) + .appendUUIDQuery(REQUIRED) + .build(); + + //execute external package query var externalPackageQueryResultSet = - InMemorySparqlExecutor.executeSingleQuery( - databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), - externalPackageQuery, - graphIdentifier.getGraphUri()); + InMemorySparqlExecutor.executeSingleQuery(databasePort.getGraphWithContext(graphIdentifier) + .getRdfGraph(), externalPackageQuery, graphIdentifier.getGraphUri()); - var cimExternalPackageList = - CIMObjectFactory.createExternalCIMPackageList(externalPackageQueryResultSet); + var cimExternalPackageList = CIMObjectFactory.createExternalCIMPackageList(externalPackageQueryResultSet); return packageMapper.toDTOList(cimExternalPackageList); } @Override public List listPrimitives(GraphIdentifier graphIdentifier) { - var baseQuery = - new CIMBaseQueryBuilder() - .setOrder() - .setDistinct() - .addPrefixes(databasePort.getPrefixMapping(graphIdentifier.getGraphUri())) - .filterStereotypes( - CIMStereotypes.primitiveString, CIMStereotypes.cimDatatypeString) - .setGraph(graphIdentifier.getGraphUri()) - .setType(RDFS.Class) - .build(); - var query = - new CIMQueryBuilder(baseQuery) - .appendLabelQuery(REQUIRED) - .appendPackageQuery(OPTIONAL) - .appendCommentQuery(OPTIONAL) - .appendSuperClassQuery(OPTIONAL) - .build(); - - // execute query - var queryResultSet = - InMemorySparqlExecutor.executeSingleQuery( - databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), - query, - graphIdentifier.getGraphUri()); - - // format results + var baseQuery = new CIMBaseQueryBuilder() + .setOrder() + .setDistinct() + .addPrefixes(databasePort.getPrefixMapping(graphIdentifier.getGraphUri())) + .filterStereotypes(CIMStereotypes.primitiveString, CIMStereotypes.cimDatatypeString) + .setGraph(graphIdentifier.getGraphUri()) + .setType(RDFS.Class) + .build(); + var query = new CIMQueryBuilder(baseQuery) + .appendLabelQuery(REQUIRED) + .appendPackageQuery(OPTIONAL) + .appendCommentQuery(OPTIONAL) + .appendSuperClassQuery(OPTIONAL) + .build(); + + //execute query + var queryResultSet = InMemorySparqlExecutor.executeSingleQuery(databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), query, graphIdentifier.getGraphUri()); + + //format results var cimClassList = CIMUMLObjectFactory.createCIMClassUMLAdaptedList(queryResultSet); cimClassList.forEach(CIMClassUMLAdapted::nullEmptyLists); @@ -315,25 +303,22 @@ public List listPrimitives(GraphIdentifier graphIdentifier) @Override public List listStereotypes(GraphIdentifier graphIdentifier) { - var baseQuery = - new CIMBaseQueryBuilder() - .setOrder() - .setDistinct() - .addPrefixes( - databasePort.getPrefixMapping(graphIdentifier.getDatasetName())) - .setGraph(graphIdentifier.getGraphUri()) - .buildWithoutUriVar(); - - var query = new CIMQueryBuilder(baseQuery).appendStereotypeQuery(REQUIRED).build(); - - // execute query - var queryResult = - InMemorySparqlExecutor.executeSingleQuery( - databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), - query, - graphIdentifier.getGraphUri()); - - // format results + var baseQuery = new CIMBaseQueryBuilder() + .setOrder() + .setDistinct() + .addPrefixes(databasePort.getPrefixMapping(graphIdentifier.getDatasetName())) + .setGraph(graphIdentifier.getGraphUri()) + .buildWithoutUriVar(); + + var query = new CIMQueryBuilder(baseQuery) + .appendStereotypeQuery(REQUIRED) + .build(); + + + //execute query + var queryResult = InMemorySparqlExecutor.executeSingleQuery(databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), query, graphIdentifier.getGraphUri()); + + //format results List resultList = new ArrayList<>(); while (queryResult.hasNext()) { var parser = new CIMQuerySolutionParser(queryResult.next()); diff --git a/frontend/src/lib/api/backend.js b/frontend/src/lib/api/backend.js index 02025d17..6d0363ff 100644 --- a/frontend/src/lib/api/backend.js +++ b/frontend/src/lib/api/backend.js @@ -95,8 +95,8 @@ export class BackendConnection { }); } - async getClasses(datasetName, graphURI) { - const url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/classes`; + async getClasses(datasetName, graphURI, includeExternalClasses = false) { + const url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/classes?includeExternalClasses=${includeExternalClasses}`; return fetch(url, { method: "GET", mode: "cors", diff --git a/frontend/src/routes/mainpage/classEditor/fetch-class-editor-context.js b/frontend/src/routes/mainpage/classEditor/fetch-class-editor-context.js index 9273ee10..67667dec 100644 --- a/frontend/src/routes/mainpage/classEditor/fetch-class-editor-context.js +++ b/frontend/src/routes/mainpage/classEditor/fetch-class-editor-context.js @@ -97,7 +97,7 @@ export async function getDataTypes(datasetName, graphUri) { } export async function getClasses(datasetName, graphUri) { - const res = await bec.getClasses(datasetName, graphUri); + const res = await bec.getClasses(datasetName, graphUri, true); let classesDto = await res.json(); let classes = classesDto.map(cls => new Class(cls)); console.log("CLASSES:", classes); From 261ca62082c26452e2d2f3af5157aeadafef8995 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Wed, 15 Apr 2026 12:05:46 +0200 Subject: [PATCH 10/31] fixed more bugs, where resources were not deleted properly --- .../delete/DeleteResourcesService.java | 10 +- .../delete/FindOnDeleteRelationsService.java | 35 ++++--- .../mapper/map-reactive-object-to-dto.js | 16 +++- frontend/src/routes/NewClassDialog.svelte | 2 +- .../DeleteDependencyNode.svelte | 93 +++++++++++++------ 5 files changed, 108 insertions(+), 48 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java index fe6be3df..a22dd4e7 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java @@ -134,6 +134,14 @@ private void removeResource(Resource resource) { .filter(stmt -> stmt.getObject().isAnon()) .forEach(stmt -> queue.add(stmt.getObject().asResource())); + // Delete inverse association + var inverseStmt = current.getProperty(CIMS.inverseRoleName); + if (inverseStmt != null && inverseStmt.getObject().isResource()) { + var inverse = inverseStmt.getObject().asResource(); + model.remove(inverse, CIMS.inverseRoleName, current); + queue.add(inverse); + } + if (isReferencedElsewhere(model, current)) { var uuidStmt = current.getProperty(RDFA.uuid); model.removeAll(current, null, null); @@ -190,7 +198,7 @@ private void deleteClass(Model model, ResourceDeleteRequest deleteRequest) { //delete associations only if it references an external resource model.listSubjectsWithProperty(RDFS.domain, resource) .filterKeep(CIMPropertyUtils::isAssociation) - .filterKeep(assoc -> CIMResourceTypeIdentifyingUtils.isExternalResource(assoc.getProperty(RDFS.domain).getObject().asResource())) + .filterKeep(assoc -> CIMResourceTypeIdentifyingUtils.isExternalResource(assoc.getProperty(RDFS.range).getObject().asResource())) .toList() .forEach(this::removeResource); diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java b/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java index b287015b..4a2a4124 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java @@ -94,7 +94,7 @@ private AffectedResource findAffectedRelationsForClass(Model model, UUID uuid, var classResourceId = createResourceIdentifier(model, uuid); var affectedResources = new ArrayList(); - affectedResources.addAll(findAffectedAttributesForClass(classResource, classResourceId)); + affectedResources.addAll(findAffectedAttributesForClass(classResource)); affectedResources.addAll(findAffectedAssociationsForClass(classResource, classResourceId)); affectedResources.addAll(findAffectedChildClassesForClass(classResource)); @@ -106,27 +106,33 @@ private AffectedResource findAffectedRelationsForClass(Model model, UUID uuid, .setChildren(affectedResources); } - private List findAffectedAttributesForClass(Resource classResource, ResourceIdentifier classResourceId) { + private List findAffectedAttributesForClass(Resource classResource) { var childActions = List.of(DeleteAction.DELETE, DeleteAction.KEEP); return listAttributesWithClassAsDatatype(classResource).stream() .map(attr -> new AffectedOwnedResource(createResourceIdentifier(attr), CimResourceType.ATTRIBUTE, AffectedResourceReason.USES_DELETED_CLASS_AS_DATATYPE, - classResourceId) + createResourceIdentifier(attr.getProperty(RDFS.domain).getObject().asResource())) .setActions(childActions)) .toList(); } private List findAffectedAssociationsForClass(Resource classResource, ResourceIdentifier classResourceId) { - var childActions = List.of(DeleteAction.DELETE, DeleteAction.KEEP); return listAssociationsReferencingClass(classResource).stream() - .map(assoc -> new AffectedAssociation(createResourceIdentifier(assoc), - CimResourceType.ASSOCIATION, - AffectedResourceReason.REFENCES_DELETED_CLASS_VIA_ASSOCIATION, - classResourceId, - getAssociationTarget(assoc) - ) - .setActions(childActions)) + .map(assoc -> { + var childActions = new ArrayList(); + childActions.add(DeleteAction.DELETE); + if(!CIMResourceTypeIdentifyingUtils.isExternalResource(assoc.getProperty(RDFS.range).getObject().asResource())){ + childActions.add(DeleteAction.KEEP); + } + return new AffectedAssociation(createResourceIdentifier(assoc), + CimResourceType.ASSOCIATION, + AffectedResourceReason.REFENCES_DELETED_CLASS_VIA_ASSOCIATION, + classResourceId, + getAssociationTarget(assoc) + ) + .setActions(childActions); + }) .toList(); } @@ -195,11 +201,12 @@ private ResourceIdentifier createResourceIdentifier(Model model, UUID uuid) { private ResourceIdentifier createResourceIdentifier(Resource resource) { var uuid = findUuidForResource(resource); - if (!resource.hasProperty(RDFS.label)) { - throw new IllegalStateException("Resource with UUID " + uuid + " does not have a label."); + var label = resource.getLocalName(); + if (resource.hasProperty(RDFS.label)) { + label = resource.getProperty(RDFS.label).getString(); } return new ResourceIdentifier().setUuid(uuid) - .setLabel(resource.getProperty(RDFS.label).getString()) + .setLabel(label) .setNamespace(resource.getNameSpace()); } diff --git a/frontend/src/lib/models/reactive/mapper/map-reactive-object-to-dto.js b/frontend/src/lib/models/reactive/mapper/map-reactive-object-to-dto.js index 887e529c..a48a5c5e 100644 --- a/frontend/src/lib/models/reactive/mapper/map-reactive-object-to-dto.js +++ b/frontend/src/lib/models/reactive/mapper/map-reactive-object-to-dto.js @@ -14,6 +14,7 @@ * limitations under the License. * */ +import { URI } from "$lib/models/dto/index.ts"; import { ReactiveAssociation } from "$lib/models/reactive/models/reactive-association.svelte.js"; import { ReactiveAttribute } from "$lib/models/reactive/models/reactive-attribute.svelte.js"; import { ReactiveClass } from "$lib/models/reactive/models/reactive-class.svelte.js"; @@ -119,17 +120,22 @@ export function mapReactiveAttributeToAttributeDto( attribute.multiplicityUpperBound, ); const datatype = getDatatypeByUri(attribute.datatype); + const uri = new URI(attribute.datatype); + + const dtoDatatype = datatype + ? { + prefix: datatype.prefix, + label: datatype.label, + type: datatype.type, + } + : { prefix: uri.prefix, label: uri.suffix, type: "UNKNOWN" }; return { uuid: attribute.uuid, label: attribute.label, prefix: attribute.namespace, multiplicity: multiplicityString, domain: domainIri, - dataType: { - prefix: datatype.prefix, - label: datatype.label, - type: datatype.type, - }, + dataType: dtoDatatype, comment: attribute.comment, fixedValue: attribute.fixedValue, defaultValue: attribute.defaultValue, diff --git a/frontend/src/routes/NewClassDialog.svelte b/frontend/src/routes/NewClassDialog.svelte index 249ade37..13f2ab02 100644 --- a/frontend/src/routes/NewClassDialog.svelte +++ b/frontend/src/routes/NewClassDialog.svelte @@ -107,7 +107,7 @@ } untrack( () => - (className = new ReactiveValueWrapper(className.value, label => + (className = new ReactiveValueWrapper(className?.value, label => isInvalidClassLabel( label, classURINamespace.value, diff --git a/frontend/src/routes/delete-relations-dialog/DeleteDependencyNode.svelte b/frontend/src/routes/delete-relations-dialog/DeleteDependencyNode.svelte index 08a10448..86ac5354 100644 --- a/frontend/src/routes/delete-relations-dialog/DeleteDependencyNode.svelte +++ b/frontend/src/routes/delete-relations-dialog/DeleteDependencyNode.svelte @@ -63,7 +63,7 @@ }, KEEP: { icon: faShieldHalved, - text: "Keep ref", + text: "Keep", variant: "default", width: "w-24", tooltip: @@ -71,16 +71,16 @@ }, REMOVE_PACKAGE_REFERENCE: { icon: faLinkSlash, - text: "Remove ref", + text: "Move to default", variant: "default", - width: "w-30", + width: "w-36", tooltip: "Keep this resource but remove its package reference", }, REMOVE_SUBCLASS_REFERENCE: { icon: faLinkSlash, - text: "Remove ref", + text: "Remove parent", variant: "default", - width: "w-30", + width: "w-36", tooltip: "Keep this resource but remove its inheritance reference", }, }; @@ -163,6 +163,11 @@ {:else} {node.resourceIdentifier.label} + {#if typeBadge === "ATTRIBUTE" && node.domain} + + ({node.domain.label}) + + {/if} {/if} {#if node.reason && !isRoot} @@ -176,28 +181,62 @@
{#each availableActions as action} {@const config = actionConfig[action]} -
- {#if node.actions.includes(action)} -
- selectAction(action)} - icon={config.icon} - text={config.text} - variant={currentAction === action - ? config.variant - : "default"} - title={config.tooltip} - disabled={disabled || isRoot} - /> -
- {/if} -
+ {#if action === "REMOVE_SUBCLASS_REFERENCE" && availableActions.includes("REMOVE_PACKAGE_REFERENCE")} + + {:else if action === "REMOVE_PACKAGE_REFERENCE"} + + {@const refAction = node.actions.find( + a => + a === "REMOVE_PACKAGE_REFERENCE" || + a === "REMOVE_SUBCLASS_REFERENCE", + )} +
+ {#if refAction} + {@const refConfig = actionConfig[refAction]} +
+ selectAction(refAction)} + icon={refConfig.icon} + text={refConfig.text} + variant={currentAction === refAction + ? refConfig.variant + : "default"} + title={refConfig.tooltip} + disabled={disabled || isRoot} + /> +
+ {/if} +
+ {:else} +
+ {#if node.actions.includes(action)} +
+ selectAction(action)} + icon={config.icon} + text={config.text} + variant={currentAction === action + ? config.variant + : "default"} + title={config.tooltip} + disabled={disabled || isRoot} + /> +
+ {/if} +
+ {/if} {/each}
From 484c234dd45701b8a3c35851bae9fbb03114b810 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Thu, 16 Apr 2026 12:33:36 +0200 Subject: [PATCH 11/31] structures some code --- .../datasets/graphs/DeleteRESTController.java | 8 +-- .../CIMResourceTypeIdentifyingUtils.java | 6 --- .../cim/relations/model/CIMResourceUtils.java | 51 +++++++++++++++++++ .../delete/DeleteResourcesService.java | 5 +- .../delete/DeleteResourcesUseCase.java | 7 ++- ...ava => FindDeleteDependenciesService.java} | 22 +++----- ...ava => FindDeleteDependenciesUseCase.java} | 10 +++- 7 files changed, 79 insertions(+), 30 deletions(-) create mode 100644 backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceUtils.java rename backend/src/main/java/org/rdfarchitect/services/delete/{FindOnDeleteRelationsService.java => FindDeleteDependenciesService.java} (92%) rename backend/src/main/java/org/rdfarchitect/services/delete/{FindOnDeleteRelationsUseCase.java => FindDeleteDependenciesUseCase.java} (64%) diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeleteRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeleteRESTController.java index 9a83c580..92cd443a 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeleteRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeleteRESTController.java @@ -26,7 +26,7 @@ import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.services.ExpandURIUseCase; import org.rdfarchitect.services.delete.DeleteResourcesUseCase; -import org.rdfarchitect.services.delete.FindOnDeleteRelationsUseCase; +import org.rdfarchitect.services.delete.FindDeleteDependenciesUseCase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; @@ -49,7 +49,7 @@ public class DeleteRESTController { private static final Logger logger = LoggerFactory.getLogger(DeleteRESTController.class); private final ExpandURIUseCase expandURIUseCase; - private final FindOnDeleteRelationsUseCase findOnDeleteRelationsUseCase; + private final FindDeleteDependenciesUseCase findDeleteDependenciesUseCase; private final DeleteResourcesUseCase deleteResourcesUseCase; @Operation( @@ -78,7 +78,7 @@ public AffectedResource getDeletionImpact( var extendedGraphURI = expandURIUseCase.expandUri(datasetName, graphURI); - var resultObj = findOnDeleteRelationsUseCase.getDeleteRelations(new GraphIdentifier(datasetName, extendedGraphURI), UUID.fromString(uuid)); + var resultObj = findDeleteDependenciesUseCase.getDeleteDependencies(new GraphIdentifier(datasetName, extendedGraphURI), UUID.fromString(uuid)); logger.info("Sending response to GET request: \"/api/datasets/{{}}/graphs/{{}}/uuid/{{}/deletion-impact\" from \"{}\".", datasetName, graphURI, uuid, originURL); return resultObj; @@ -111,7 +111,7 @@ public String deleteResources( var extendedGraphURI = expandURIUseCase.expandUri(datasetName, graphURI); - deleteResourcesUseCase.deleteResources(new GraphIdentifier(datasetName, extendedGraphURI), deleteRequests); + deleteResourcesUseCase.executeDeleteRequests(new GraphIdentifier(datasetName, extendedGraphURI), deleteRequests); logger.info("Sending response to POST request: \"/api/datasets/{{}}/graphs/{{}}/delete\" from \"{}\".", datasetName, graphURI, originURL); return "success"; diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java index f51e7880..9c76c2d6 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java @@ -84,10 +84,4 @@ public boolean isEnumEntry(Resource subject) { return types.getFirst().getObject().asResource() .hasProperty(CIMS.stereotype, CIMStereotypes.enumeration); } - - public boolean isExternalResource(Resource resource) { - return !resource.listProperties() - .filterDrop(stmt -> stmt.getPredicate().equals(RDFA.uuid)) - .hasNext(); - } } diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceUtils.java b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceUtils.java new file mode 100644 index 00000000..05f0562e --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceUtils.java @@ -0,0 +1,51 @@ +/* + * 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.models.cim.relations.model; + +import lombok.experimental.UtilityClass; +import org.apache.jena.rdf.model.Resource; +import org.rdfarchitect.models.cim.rdf.resources.RDFA; + +import java.util.UUID; + +@UtilityClass +public class CIMResourceUtils { + + /** + * Checks whether a resource is external/referenced only. In our model this would mean it only has a {@link RDFA::uuid} property, but no other properties. + * @param resource The resource to check for. + * @return True if the resource is an external resource, false otherwise. + */ + public boolean isExternalResource(Resource resource) { + return !resource.listProperties() + .filterDrop(stmt -> stmt.getPredicate().equals(RDFA.uuid)) + .hasNext(); + } + + /** + * Finds the uuid of a resource. + * @param resource The resource to finde the uuid for. + * @return The uuid of the resource. + */ + public UUID findUuidForResource(Resource resource) { + if (!resource.hasProperty(RDFA.uuid)) { + throw new IllegalStateException("Resource " + resource + " does not have a UUID."); + } + return UUID.fromString(resource.getProperty(RDFA.uuid).getString()); + } +} diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java index a22dd4e7..da030097 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java @@ -32,6 +32,7 @@ import org.rdfarchitect.models.cim.rdf.resources.RDFA; import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils; import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils.CimResourceType; +import org.rdfarchitect.models.cim.relations.model.CIMResourceUtils; import org.rdfarchitect.models.cim.relations.model.properties.CIMPropertyUtils; import org.rdfarchitect.rdf.graph.wrapper.GraphRewindable; import org.slf4j.Logger; @@ -51,7 +52,7 @@ public class DeleteResourcesService implements DeleteResourcesUseCase { private final DatabasePort databasePort; @Override - public void deleteResources(GraphIdentifier graphIdentifier, List deleteRequests) { + public void executeDeleteRequests(GraphIdentifier graphIdentifier, List deleteRequests) { GraphRewindable graph = null; try { graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); @@ -198,7 +199,7 @@ private void deleteClass(Model model, ResourceDeleteRequest deleteRequest) { //delete associations only if it references an external resource model.listSubjectsWithProperty(RDFS.domain, resource) .filterKeep(CIMPropertyUtils::isAssociation) - .filterKeep(assoc -> CIMResourceTypeIdentifyingUtils.isExternalResource(assoc.getProperty(RDFS.range).getObject().asResource())) + .filterKeep(assoc -> CIMResourceUtils.isExternalResource(assoc.getProperty(RDFS.range).getObject().asResource())) .toList() .forEach(this::removeResource); diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesUseCase.java b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesUseCase.java index 64dc2c47..7d92d2dd 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesUseCase.java @@ -24,5 +24,10 @@ public interface DeleteResourcesUseCase { - void deleteResources(GraphIdentifier graphIdentifier, List deleteRequests); + /** + * Executes a list of delete requests on a specified graph. + * @param graphIdentifier The identifier of the graph where the change occurred. + * @param deleteRequests The List of deleteRequests. + */ + void executeDeleteRequests(GraphIdentifier graphIdentifier, List deleteRequests); } diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java b/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java similarity index 92% rename from backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java rename to backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java index 4a2a4124..cdb44a3c 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java @@ -34,9 +34,9 @@ import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.models.cim.rdf.resources.CIMS; -import org.rdfarchitect.models.cim.rdf.resources.RDFA; import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils; import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils.CimResourceType; +import org.rdfarchitect.models.cim.relations.model.CIMResourceUtils; import org.rdfarchitect.models.cim.relations.model.properties.CIMPropertyUtils; import org.rdfarchitect.rdf.graph.GraphUtils; import org.rdfarchitect.rdf.graph.wrapper.GraphRewindable; @@ -48,12 +48,12 @@ @Service @RequiredArgsConstructor -public class FindOnDeleteRelationsService implements FindOnDeleteRelationsUseCase { +public class FindDeleteDependenciesService implements FindDeleteDependenciesUseCase { private final DatabasePort databasePort; @Override - public AffectedResource getDeleteRelations(GraphIdentifier graphIdentifier, UUID uuid) { + public AffectedResource getDeleteDependencies(GraphIdentifier graphIdentifier, UUID uuid) { var model = ModelFactory.createModelForGraph(getCopyOfDatabaseGraph(graphIdentifier)); var resourceType = CIMResourceTypeIdentifyingUtils.getType(model, uuid); var defaultActions = List.of(DeleteAction.DELETE); @@ -79,7 +79,7 @@ private AffectedResource findAffectedRelationsForPackage(Model model, UUID uuid, var affectedResources = new ArrayList(); var clsDeleteActions = List.of(DeleteAction.DELETE, DeleteAction.KEEP, DeleteAction.REMOVE_PACKAGE_REFERENCE); for (var cls : classesInPackage) { - var clsUuid = findUuidForResource(cls); + var clsUuid = CIMResourceUtils.findUuidForResource(cls); var affectedClassResource = findAffectedRelationsForClass(model, clsUuid, AffectedResourceReason.CONTAINED_IN_PACKAGE, clsDeleteActions); affectedResources.add(affectedClassResource); } @@ -98,9 +98,6 @@ private AffectedResource findAffectedRelationsForClass(Model model, UUID uuid, affectedResources.addAll(findAffectedAssociationsForClass(classResource, classResourceId)); affectedResources.addAll(findAffectedChildClassesForClass(classResource)); - // Attribute, die Teile der Klasse sind, werden hier nicht abgefragt, da sie immer mitgelöscht werden. - // (Vllt. sollte man die aber trotzdem mit anzeigen und keine Option oder so geben?) - return new AffectedResource(classResourceId, CimResourceType.CLASS, reason) .setActions(deleteActions) .setChildren(affectedResources); @@ -122,7 +119,7 @@ private List findAffectedAssociationsForClass(Resource classRe .map(assoc -> { var childActions = new ArrayList(); childActions.add(DeleteAction.DELETE); - if(!CIMResourceTypeIdentifyingUtils.isExternalResource(assoc.getProperty(RDFS.range).getObject().asResource())){ + if(!CIMResourceUtils.isExternalResource(assoc.getProperty(RDFS.range).getObject().asResource())){ childActions.add(DeleteAction.KEEP); } return new AffectedAssociation(createResourceIdentifier(assoc), @@ -158,12 +155,7 @@ private List findAffectedChildClassesForClass(Resource classRe .toList(); } - private UUID findUuidForResource(Resource resource) { - if (!resource.hasProperty(RDFA.uuid)) { - throw new IllegalStateException("Resource " + resource + " does not have a UUID."); - } - return UUID.fromString(resource.getProperty(RDFA.uuid).getString()); - } + private List listClassesInPackage(Model model, UUID uuid) { var packageResource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, uuid); @@ -200,7 +192,7 @@ private ResourceIdentifier createResourceIdentifier(Model model, UUID uuid) { } private ResourceIdentifier createResourceIdentifier(Resource resource) { - var uuid = findUuidForResource(resource); + var uuid = CIMResourceUtils.findUuidForResource(resource); var label = resource.getLocalName(); if (resource.hasProperty(RDFS.label)) { label = resource.getProperty(RDFS.label).getString(); diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsUseCase.java b/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesUseCase.java similarity index 64% rename from backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsUseCase.java rename to backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesUseCase.java index b2275b3f..e16b5b61 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/FindOnDeleteRelationsUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesUseCase.java @@ -22,7 +22,13 @@ import java.util.UUID; -public interface FindOnDeleteRelationsUseCase { +public interface FindDeleteDependenciesUseCase { - AffectedResource getDeleteRelations(GraphIdentifier graphIdentifier, UUID uuid); + /** + * Finds the resources that would be affected by deleting a specified resource. + * @param graphIdentifier The identifier of the graph. + * @param uuid The resource to find dependencies for. + * @return An {@link AffectedResource} containing the affected resources with and their relations. + */ + AffectedResource getDeleteDependencies(GraphIdentifier graphIdentifier, UUID uuid); } From 2f81e90883c553a509b8c7fdfb6be6a606b56fb1 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Thu, 16 Apr 2026 12:33:53 +0200 Subject: [PATCH 12/31] added tabs component --- .../lib/components/bitsui/tabs/Tabs.svelte | 44 +++ .../components/bitsui/tabs/TabsList.svelte | 28 ++ .../components/bitsui/tabs/TabsTrigger.svelte | 67 +++++ .../DeleteDependenciesDialog.svelte | 20 +- .../DeleteDependencyNode.svelte | 255 +++++++++++++----- 5 files changed, 339 insertions(+), 75 deletions(-) create mode 100644 frontend/src/lib/components/bitsui/tabs/Tabs.svelte create mode 100644 frontend/src/lib/components/bitsui/tabs/TabsList.svelte create mode 100644 frontend/src/lib/components/bitsui/tabs/TabsTrigger.svelte diff --git a/frontend/src/lib/components/bitsui/tabs/Tabs.svelte b/frontend/src/lib/components/bitsui/tabs/Tabs.svelte new file mode 100644 index 00000000..a14c0144 --- /dev/null +++ b/frontend/src/lib/components/bitsui/tabs/Tabs.svelte @@ -0,0 +1,44 @@ + + + + + {@render children?.()} + diff --git a/frontend/src/lib/components/bitsui/tabs/TabsList.svelte b/frontend/src/lib/components/bitsui/tabs/TabsList.svelte new file mode 100644 index 00000000..32adbd93 --- /dev/null +++ b/frontend/src/lib/components/bitsui/tabs/TabsList.svelte @@ -0,0 +1,28 @@ + + + + + {@render children?.()} + diff --git a/frontend/src/lib/components/bitsui/tabs/TabsTrigger.svelte b/frontend/src/lib/components/bitsui/tabs/TabsTrigger.svelte new file mode 100644 index 00000000..ccf1065a --- /dev/null +++ b/frontend/src/lib/components/bitsui/tabs/TabsTrigger.svelte @@ -0,0 +1,67 @@ + + + + + + {#if text} + {text} + {/if} + {#if icon} + + {/if} + + diff --git a/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte b/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte index 27260fea..811563e8 100644 --- a/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte +++ b/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte @@ -162,6 +162,23 @@ selectedActions = new Map(selectedActions); } + /** + * Applies the given action to all direct children of parentNode that + * actually support it. Used by the root's bulk-apply controls. + * @param {object} parentNode + * @param {string} action + */ + function onBulkApplyToChildren(parentNode, action) { + if (!parentNode.children) return; + for (const child of parentNode.children) { + if (child.actions.includes(action)) { + const key = `${child.resourceIdentifier.uuid}::${child.reason}`; + selectedActions.set(key, action); + } + } + selectedActions = new Map(selectedActions); + } + function getDialogTitle() { if (deleteDependencies) { return `Delete ${type} "${deleteDependencies.resourceIdentifier.label}"?`; @@ -174,7 +191,7 @@ bind:showDialog onOpen={onOpenInternal} {onClose} - size="w-full max-w-1/3 max-h-3/4" + size="w-1/3 max-w-1/2 max-h-3/4" primaryLabel="Delete" onPrimary={submitDeleteRequest} title={getDialogTitle()} @@ -197,6 +214,7 @@ node={deleteDependencies} {selectedActions} {onSelectAction} + {onBulkApplyToChildren} {availableActions} depth={0} isRoot={true} diff --git a/frontend/src/routes/delete-relations-dialog/DeleteDependencyNode.svelte b/frontend/src/routes/delete-relations-dialog/DeleteDependencyNode.svelte index 86ac5354..ce77c7f8 100644 --- a/frontend/src/routes/delete-relations-dialog/DeleteDependencyNode.svelte +++ b/frontend/src/routes/delete-relations-dialog/DeleteDependencyNode.svelte @@ -21,12 +21,15 @@ faShieldHalved, faLinkSlash, faArrowRight, + faArrowRightFromBracket, faChevronRight, } from "@fortawesome/free-solid-svg-icons"; import { slide } from "svelte/transition"; import { Fa } from "svelte-fa"; - import FaIconButton from "$lib/components/FaIconButton.svelte"; + import Tabs from "$lib/components/bitsui/tabs/Tabs.svelte"; + import TabsList from "$lib/components/bitsui/tabs/TabsList.svelte"; + import TabsTrigger from "$lib/components/bitsui/tabs/TabsTrigger.svelte"; import DeleteDependencyNode from "./DeleteDependencyNode.svelte"; @@ -34,17 +37,25 @@ node, selectedActions, onSelectAction, + onBulkApplyToChildren = () => {}, availableActions = [ - "DELETE", - "KEEP", - "REMOVE_PACKAGE_REFERENCE", - "REMOVE_SUBCLASS_REFERENCE", + ACTIONS.DELETE, + ACTIONS.KEEP, + ACTIONS.REMOVE_PACKAGE_REFERENCE, + ACTIONS.REMOVE_SUBCLASS_REFERENCE, ], depth = 0, isRoot = false, disabled = false, } = $props(); + const ACTIONS = { + DELETE: "DELETE", + KEEP: "KEEP", + REMOVE_PACKAGE_REFERENCE: "REMOVE_PACKAGE_REFERENCE", + REMOVE_SUBCLASS_REFERENCE: "REMOVE_SUBCLASS_REFERENCE", + }; + const reasonLabels = { DELETION_REQUESTED_BY_USER: "Requested by user", REFENCES_DELETED_CLASS_VIA_ASSOCIATION: "References deleted class", @@ -54,39 +65,47 @@ }; const actionConfig = { - DELETE: { + [ACTIONS.DELETE]: { icon: faTrash, text: "Delete", variant: "danger", width: "w-24", tooltip: "Permanently delete this resource and all its data", + bulkTooltip: "Delete all direct children", }, - KEEP: { + [ACTIONS.KEEP]: { icon: faShieldHalved, text: "Keep", variant: "default", width: "w-24", tooltip: "Keep this resource and its reference, even if the referenced target is deleted", + bulkTooltip: "Keep all direct children", }, - REMOVE_PACKAGE_REFERENCE: { - icon: faLinkSlash, + [ACTIONS.REMOVE_PACKAGE_REFERENCE]: { + icon: faArrowRightFromBracket, text: "Move to default", variant: "default", width: "w-36", tooltip: "Keep this resource but remove its package reference", + bulkTooltip: + "Move all direct children out of the package reference", }, - REMOVE_SUBCLASS_REFERENCE: { + [ACTIONS.REMOVE_SUBCLASS_REFERENCE]: { icon: faLinkSlash, text: "Remove parent", variant: "default", width: "w-36", tooltip: "Keep this resource but remove its inheritance reference", + bulkTooltip: + "Remove inheritance reference from all direct children", }, }; let expanded = $state(isRoot); + let bulkValue = $state(null); + let hasChildren = $derived(node.children?.length > 0); let actionKey = $derived(`${node.resourceIdentifier.uuid}::${node.reason}`); let currentAction = $derived( @@ -94,9 +113,32 @@ ); let typeBadge = $derived(node.type); let isAssociation = $derived(typeBadge === "ASSOCIATION"); - + let singleAction = $derived(node.actions.length === 1); // Children are disabled if this node is not set to DELETE - let childrenDisabled = $derived(disabled || currentAction !== "DELETE"); + let childrenDisabled = $derived( + disabled || currentAction !== ACTIONS.DELETE, + ); + + // Tabs are disabled when: + // - the node itself is disabled by its parent, OR + // - it is the root AND there is more than one action to choose from + // (a root node with only a single action stays active so it isn't grayed out) + let tabsDisabled = $derived(disabled || (isRoot && !singleAction)); + + /** + * For root: collect actions supported by at least one direct child, + * so the bulk toggle only shows applicable actions. + */ + let bulkApplicableActions = $derived.by(() => { + if (!isRoot || !hasChildren) return new Set(); + const set = new Set(); + for (const child of node.children) { + for (const a of child.actions) { + set.add(a); + } + } + return set; + }); function toggleExpand() { if (hasChildren) { @@ -104,8 +146,16 @@ } } - function selectAction(action) { - onSelectAction(actionKey, action); + function handleValueChange(next) { + if (next && next !== currentAction) { + onSelectAction(actionKey, next); + } + } + + function handleBulkChange(next) { + if (next) { + onBulkApplyToChildren(node, next); + } } @@ -177,75 +227,131 @@ {/if}
- -
- {#each availableActions as action} - {@const config = actionConfig[action]} - {#if action === "REMOVE_SUBCLASS_REFERENCE" && availableActions.includes("REMOVE_PACKAGE_REFERENCE")} - - {:else if action === "REMOVE_PACKAGE_REFERENCE"} - - {@const refAction = node.actions.find( - a => - a === "REMOVE_PACKAGE_REFERENCE" || - a === "REMOVE_SUBCLASS_REFERENCE", - )} -
- {#if refAction} - {@const refConfig = actionConfig[refAction]} -
- selectAction(refAction)} - icon={refConfig.icon} - text={refConfig.text} - variant={currentAction === refAction - ? refConfig.variant - : "default"} - title={refConfig.tooltip} - disabled={disabled || isRoot} - /> + +
+ + + {#each availableActions as action} + {@const config = actionConfig[action]} + {#if action === ACTIONS.REMOVE_SUBCLASS_REFERENCE && availableActions.includes(ACTIONS.REMOVE_PACKAGE_REFERENCE)} + + {:else if action === ACTIONS.REMOVE_PACKAGE_REFERENCE} + {@const refAction = node.actions.find( + a => + a === ACTIONS.REMOVE_PACKAGE_REFERENCE || + a === ACTIONS.REMOVE_SUBCLASS_REFERENCE, + )} +
+ {#if refAction} + {@const refConfig = actionConfig[refAction]} + + {/if}
- {/if} -
- {:else} -
- {#if node.actions.includes(action)} -
- selectAction(action)} - icon={config.icon} - text={config.text} - variant={currentAction === action - ? config.variant - : "default"} - title={config.tooltip} - disabled={disabled || isRoot} - /> + {:else} +
+ {#if node.actions.includes(action)} + + {/if}
{/if} -
- {/if} - {/each} + {/each} + +
- + {#if hasChildren && expanded}
+ {#if isRoot} + +
+ + Set all: + +
+ (bulkValue = null)} + {disabled} + > + + {#each availableActions as action} + {@const config = actionConfig[action]} + {#if action === ACTIONS.REMOVE_SUBCLASS_REFERENCE && availableActions.includes(ACTIONS.REMOVE_PACKAGE_REFERENCE)} + + {:else if action === ACTIONS.REMOVE_PACKAGE_REFERENCE} + {@const refAction = [ + ACTIONS.REMOVE_PACKAGE_REFERENCE, + ACTIONS.REMOVE_SUBCLASS_REFERENCE, + ].find(a => + bulkApplicableActions.has(a), + )} +
+ {#if refAction} + {@const refConfig = + actionConfig[refAction]} + + handleBulkChange( + refAction, + )} + /> + {/if} +
+ {:else} +
+ {#if bulkApplicableActions.has(action)} + + handleBulkChange( + action, + )} + /> + {/if} +
+ {/if} + {/each} +
+
+
+
+ {/if} + {#each node.children as child, i (child.resourceIdentifier.uuid)} - {#if i > 0 && (node.children[i - 1]?.children?.length > 0 || child.children?.length > 0)}
Date: Mon, 20 Apr 2026 15:17:40 +0200 Subject: [PATCH 13/31] Added: - tests - deleterequest for ontologies --- .../wrapper/GraphRewindableWithUUIDs.java | 49 ++- .../delete/DeleteResourcesService.java | 8 +- .../delete/FindDeleteDependenciesService.java | 100 ++++- .../delete/DeleteResourcesServiceTest.java | 362 ++++++++++++++++++ .../FindDeleteDependenciesServiceTest.java | 310 +++++++++++++++ .../rdfarchitect/services/delete/testdata.ttl | 153 ++++++++ frontend/src/routes/NewClassDialog.svelte | 15 +- .../packageNavigation/GraphSection.svelte | 16 +- 8 files changed, 956 insertions(+), 57 deletions(-) create mode 100644 backend/src/test/java/org/rdfarchitect/services/delete/DeleteResourcesServiceTest.java create mode 100644 backend/src/test/java/org/rdfarchitect/services/delete/FindDeleteDependenciesServiceTest.java create mode 100644 backend/src/test/java/org/rdfarchitect/services/delete/testdata.ttl diff --git a/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/GraphRewindableWithUUIDs.java b/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/GraphRewindableWithUUIDs.java index f2a16f22..f619903c 100644 --- a/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/GraphRewindableWithUUIDs.java +++ b/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/GraphRewindableWithUUIDs.java @@ -28,9 +28,9 @@ import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; import org.jetbrains.annotations.NotNull; +import org.rdfarchitect.models.cim.rdf.resources.RDFA; import org.rdfarchitect.exception.graph.GraphNotInATransactionException; import org.rdfarchitect.exception.graph.GraphTransactionException; -import org.rdfarchitect.models.cim.rdf.resources.RDFA; import org.rdfarchitect.rdf.graph.DeltaCompressible; import java.util.HashSet; @@ -39,17 +39,14 @@ public class GraphRewindableWithUUIDs extends GraphRewindable { - private static final Set RELEVANT_TYPES = - Set.of(RDF.Property.toString(), RDFS.Class.toString()); + private static final Set RELEVANT_TYPES = Set.of(RDF.Property.toString(), RDFS.Class.toString()); /** - * Accepts a {@link Graph} that serves as a base version of the {@link - * GraphRewindableWithUUIDs}. + * Accepts a {@link Graph} that serves as a base version of the {@link GraphRewindableWithUUIDs}. * - * @param base The base graph - * @param maxVersions The maximum amount of versions the graph stores. - * @param compressCount The amount of versions that are compressed to a new base when - * compressing. + * @param base The base graph + * @param maxVersions The maximum amount of versions the graph stores. + * @param compressCount The amount of versions that are compressed to a new base when compressing. */ public GraphRewindableWithUUIDs(@NotNull Graph base, int maxVersions, int compressCount) { super(enhanceWithUUIDs(base), maxVersions, compressCount); @@ -86,10 +83,9 @@ static Graph enhanceWithUUIDs(Graph graph) { } private static void addUUIDsToTypedResources(Model model) { - var subjects = - model.listResourcesWithProperty(RDF.type) - .filterKeep(r -> r.isURIResource() && !r.hasProperty(RDFA.uuid)) - .toSet(); + var subjects = model.listResourcesWithProperty(RDF.type) + .filterKeep(r -> r.isURIResource() && !r.hasProperty(RDFA.uuid)) + .toSet(); for (var subject : subjects) { subject.addProperty(RDFA.uuid, createUUID()); @@ -100,30 +96,29 @@ private static void addUUIDsToReferencedOnlyResources(Model model) { var objects = new HashSet(); model.listResourcesWithProperty(RDF.type) - .filterKeep(r -> r.isURIResource() && hasAnyType(r)) - .forEachRemaining( - subject -> - subject.listProperties() - .mapWith(Statement::getObject) - .filterKeep(GraphRewindableWithUUIDs::isReferencedOnlyURI) - .mapWith(RDFNode::asResource) - .forEachRemaining(objects::add)); + .filterKeep(r -> r.isURIResource() && hasAnyType(r)) + .forEachRemaining(subject -> + subject.listProperties() + .mapWith(Statement::getObject) + .filterKeep(GraphRewindableWithUUIDs::isReferencedOnlyURI) + .mapWith(RDFNode::asResource) + .forEachRemaining(objects::add)); objects.forEach(o -> o.addProperty(RDFA.uuid, createUUID())); } private static boolean hasAnyType(Resource resource) { return resource.listProperties(RDF.type) - .mapWith(Statement::getObject) - .filterKeep(o -> RELEVANT_TYPES.contains(o.asResource().getURI())) - .hasNext(); + .mapWith(Statement::getObject) + .filterKeep(o -> RELEVANT_TYPES.contains(o.asResource().getURI())) + .hasNext(); } private static boolean isReferencedOnlyURI(RDFNode node) { return node.isURIResource() - && !RELEVANT_TYPES.contains(node.asResource().getURI()) - && !node.asResource().hasProperty(RDFA.uuid) - && !node.asResource().listProperties().hasNext(); + && !RELEVANT_TYPES.contains(node.asResource().getURI()) + && !node.asResource().hasProperty(RDFA.uuid) + && !node.asResource().listProperties().hasNext(); } private static String createUUID() { diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java index da030097..fc627c5d 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java @@ -145,12 +145,12 @@ private void removeResource(Resource resource) { if (isReferencedElsewhere(model, current)) { var uuidStmt = current.getProperty(RDFA.uuid); - model.removeAll(current, null, null); + current.listProperties().forEach(model::remove); if (uuidStmt != null) { model.add(uuidStmt); } } else { - model.removeAll(current, null, null); + current.listProperties().forEach(model::remove); } } } @@ -181,12 +181,12 @@ private void deleteClass(Model model, ResourceDeleteRequest deleteRequest) { } var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); if (deleteRequest.getAction() == DeleteAction.REMOVE_SUBCLASS_REFERENCE) { - model.remove(resource, RDFS.subClassOf, null); + resource.listProperties(RDFS.subClassOf).forEach(model::remove); return; } if (deleteRequest.getAction() == DeleteAction.REMOVE_PACKAGE_REFERENCE) { - model.remove(resource, CIMS.belongsToCategory, null); + resource.listProperties(CIMS.belongsToCategory).forEach(model::remove); return; } diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java b/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java index cdb44a3c..8509add7 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java @@ -43,7 +43,12 @@ import org.springframework.stereotype.Service; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.UUID; @Service @@ -123,10 +128,10 @@ private List findAffectedAssociationsForClass(Resource classRe childActions.add(DeleteAction.KEEP); } return new AffectedAssociation(createResourceIdentifier(assoc), - CimResourceType.ASSOCIATION, - AffectedResourceReason.REFENCES_DELETED_CLASS_VIA_ASSOCIATION, - classResourceId, - getAssociationTarget(assoc) + CimResourceType.ASSOCIATION, + AffectedResourceReason.REFENCES_DELETED_CLASS_VIA_ASSOCIATION, + classResourceId, + getAssociationTarget(assoc) ) .setActions(childActions); }) @@ -146,16 +151,85 @@ private ResourceIdentifier getAssociationTarget(Resource associationResource) { } private List findAffectedChildClassesForClass(Resource classResource) { - var childClassActions = List.of(DeleteAction.KEEP, DeleteAction.REMOVE_SUBCLASS_REFERENCE); - return listDirectlyDescendingClasses(classResource).stream() - .map(childClass -> new AffectedResource(createResourceIdentifier(childClass), - CimResourceType.CLASS, - AffectedResourceReason.CHILD_OF) - .setActions(childClassActions)) - .toList(); + var childClassActions = List.of(DeleteAction.DELETE, DeleteAction.KEEP, DeleteAction.REMOVE_SUBCLASS_REFERENCE); + return buildAffectedChildClassTree(classResource, childClassActions); } + private List buildAffectedChildClassTree(Resource classResource, List childClassActions) { + var visited = new HashSet(); + visited.add(classResource); + var resourceMap = new HashMap(); + var rootChildren = new ArrayList(); + + var queue = initializeQueue(classResource, visited); + + while (!queue.isEmpty()) { + var entry = queue.poll(); + var current = entry.getKey(); + var parent = entry.getValue(); + + var affectedResource = createAffectedChildClass(current, childClassActions); + resourceMap.put(current, affectedResource); + + attachToParentOrRoot(affectedResource, parent, classResource, resourceMap, rootChildren); + enqueueChildren(current, visited, queue); + } + + return rootChildren; + } + + private LinkedList> initializeQueue(Resource classResource, Set visited) { + var queue = new LinkedList>(); + for (var directChild : listDirectlyDescendingClasses(classResource)) { + if (visited.add(directChild)) { + queue.add(Map.entry(directChild, classResource)); + } + } + return queue; + } + + private AffectedResource createAffectedChildClass(Resource classResource, List actions) { + var resourceId = createResourceIdentifier(classResource); + var children = new ArrayList(); + children.addAll(findAffectedAttributesForClass(classResource)); + children.addAll(findAffectedAssociationsForClass(classResource, resourceId)); + + return new AffectedResource(resourceId, CimResourceType.CLASS, AffectedResourceReason.CHILD_OF) + .setActions(actions) + .setChildren(children); + } + + private void attachToParentOrRoot(AffectedResource affectedResource, Resource parent, + Resource rootClass, Map resourceMap, + List rootChildren) { + if (parent.equals(rootClass)) { + rootChildren.add(affectedResource); + return; + } + var parentAffected = resourceMap.get(parent); + if (parentAffected != null) { + var existingChildren = parentAffected.getChildren(); + if (existingChildren == null) { + existingChildren = new ArrayList<>(); + } + existingChildren.add(affectedResource); + parentAffected.setChildren(existingChildren); + } + } + + private void enqueueChildren(Resource current, Set visited, + LinkedList> queue) { + for (var child : listDirectlyDescendingClasses(current)) { + if (visited.add(child)) { + queue.add(Map.entry(child, current)); + } + } + } + + private List listDirectlyDescendingClasses(Resource classResource) { + return classResource.getModel().listSubjectsWithProperty(RDFS.subClassOf, classResource).toList(); + } private List listClassesInPackage(Model model, UUID uuid) { var packageResource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, uuid); @@ -167,10 +241,6 @@ private List listClassesInPackage(Model model, UUID uuid) { .toList(); } - private List listDirectlyDescendingClasses(Resource classResource) { - return classResource.getModel().listSubjectsWithProperty(RDFS.subClassOf, classResource).toList(); - } - private List listAssociationsReferencingClass(Resource classResource) { return classResource.getModel().listSubjectsWithProperty(RDFS.domain, classResource) .filterKeep(CIMPropertyUtils::isAssociation) diff --git a/backend/src/test/java/org/rdfarchitect/services/delete/DeleteResourcesServiceTest.java b/backend/src/test/java/org/rdfarchitect/services/delete/DeleteResourcesServiceTest.java new file mode 100644 index 00000000..d8e5c165 --- /dev/null +++ b/backend/src/test/java/org/rdfarchitect/services/delete/DeleteResourcesServiceTest.java @@ -0,0 +1,362 @@ +/* + * 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.delete; + +import org.apache.jena.query.TxnType; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.ResourceFactory; +import org.apache.jena.riot.Lang; +import org.apache.jena.riot.RDFDataMgr; +import org.apache.jena.sparql.graph.GraphFactory; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.rdfarchitect.api.dto.delete.DeleteAction; +import org.rdfarchitect.api.dto.delete.ResourceDeleteRequest; +import org.rdfarchitect.database.DatabasePort; +import org.rdfarchitect.database.GraphIdentifier; +import org.rdfarchitect.database.inmemory.GraphWithContext; +import org.rdfarchitect.models.cim.rdf.resources.CIMS; +import org.rdfarchitect.models.cim.rdf.resources.RDFA; +import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DeleteResourcesServiceTest { + + private static final String TEST_DATA_PATH = "src/test/java/org/rdfarchitect/services/delete/testdata.ttl"; + + private static final GraphIdentifier GRAPH_IDENTIFIER = new GraphIdentifier("default", "default"); + + private static final String CIM_NS = "http://iec.ch/TC57/CIM100#"; + + // UUIDs from the TTL test data + private static final UUID PARENT_CLASS_UUID = UUID.fromString("05131eaf-a7dd-4ac4-8624-9665990985ab"); + private static final UUID CHILD_CLASS_UUID = UUID.fromString("93ee2f31-5ddd-4b25-b119-e90a5ed327b0"); + private static final UUID ASSOCIATED_CLASS_UUID = UUID.fromString("f6d92056-c469-40d4-add1-1d6adf2fa7a6"); + private static final UUID PACKAGE_UUID = UUID.fromString("0351f9b6-5e91-4059-9d8b-169d28f1b2c8"); + private static final UUID DATATYPE_CLASS_UUID = UUID.fromString("db520255-95ef-40d4-b328-e1631e4683a4"); + private static final UUID ONTOLOGY_UUID = UUID.fromString("dba8f8e3-bfb3-4e62-9ca5-b0136ed186b2"); + private static final UUID ATTR1_UUID = UUID.fromString("dc934c7b-6c4d-4177-9832-bcb955f25414"); + private static final UUID ASSOC_NONEXISTING_UUID = UUID.fromString("b2a306f1-4789-4a12-b045-04a014e7a937"); + private static final UUID ASSOC_TEMP_ASSOCIATED_UUID = UUID.fromString("c3b417f2-5890-5b23-c156-15b015e8b048"); + private static final UUID ASSOC_ASSOCIATED_CHILD_UUID = UUID.fromString("abfab117-e4bc-4814-9b5a-3aed72de8a2d"); + private static final UUID ASSOC_CHILD_ASSOCIATED_UUID = UUID.fromString("bc0bc228-f5cd-5925-ab6b-4bfe83ef9b3e"); + + @Mock + private DatabasePort databasePort; + + @InjectMocks + private DeleteResourcesService service; + + private GraphRewindableWithUUIDs wrappedGraph; + + @BeforeEach + void setUp() throws IOException { + var graph = GraphFactory.createDefaultGraph(); + InputStream in = Files.newInputStream(Path.of(TEST_DATA_PATH)); + RDFDataMgr.read(graph, in, Lang.TTL); + in.close(); + + wrappedGraph = new GraphRewindableWithUUIDs(graph, 5, 5); + var wrappedContext = new GraphWithContext(wrappedGraph); + + when(databasePort.getGraphWithContext(any(GraphIdentifier.class))).thenReturn(wrappedContext); + } + + private Model readModel() { + wrappedGraph.begin(TxnType.READ); + return ModelFactory.createModelForGraph(wrappedGraph); + } + + private void endRead() { + wrappedGraph.end(); + } + + private ResourceDeleteRequest request(UUID uuid, DeleteAction action) { + var req = new ResourceDeleteRequest(); + req.setUuid(uuid); + req.setAction(action); + return req; + } + + /** + * Asserts that the resource has at most its UUID triple left (all other properties removed). + */ + private void assertResourceRemovedOrOnlyUuid(String localName) { + var model = readModel(); + try { + var resource = ResourceFactory.createResource(CIM_NS + localName); + var nonUuidStatements = model.listStatements(resource, null, (RDFNode) null) + .filterDrop(stmt -> stmt.getPredicate().equals(RDFA.uuid)) + .toList(); + assertThat(nonUuidStatements).isEmpty(); + } finally { + endRead(); + } + } + + /** + * Asserts that the resource still has properties beyond just UUID. + */ + private void assertResourceFullyPresent(String localName) { + var model = readModel(); + try { + var resource = ResourceFactory.createResource(CIM_NS + localName); + var nonUuidStatements = model.listStatements(resource, null, (RDFNode) null) + .filterDrop(stmt -> stmt.getPredicate().equals(RDFA.uuid)) + .toList(); + assertThat(nonUuidStatements).isNotEmpty(); + } finally { + endRead(); + } + } + + // ==================== Delete ontology ==================== + + @Test + void executeDeleteRequests_deleteOntology_removesOntology() { + service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(ONTOLOGY_UUID, DeleteAction.DELETE))); + + assertResourceRemovedOrOnlyUuid("Ontology"); + } + + // ==================== Delete package ==================== + + @Test + void executeDeleteRequests_deletePackage_removesPackagePropertiesButPreservesUuid() { + // Package is still referenced by classes via belongsToCategory, so UUID is preserved + service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(PACKAGE_UUID, DeleteAction.DELETE))); + + assertResourceRemovedOrOnlyUuid("Package_Package"); + } + + @Test + void executeDeleteRequests_keepPackage_doesNotRemovePackage() { + service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(PACKAGE_UUID, DeleteAction.KEEP))); + + assertResourceFullyPresent("Package_Package"); + } + + // ==================== Delete class ==================== + + @Test + void executeDeleteRequests_deleteClass_removesClassAndOwnedAttributes() { + service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(PARENT_CLASS_UUID, DeleteAction.DELETE))); + + // ParentClass is referenced via subClassOf, so UUID may be preserved + assertResourceRemovedOrOnlyUuid("ParentClass"); + // ParentClass.attr1 is owned by ParentClass and should be removed + assertResourceRemovedOrOnlyUuid("ParentClass.attr1"); + } + + @Test + void executeDeleteRequests_deleteClass_removesClassProperties() { + // ChildClass is referenced by AssociatedClass via subClassOf + service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(CHILD_CLASS_UUID, DeleteAction.DELETE))); + + assertResourceRemovedOrOnlyUuid("ChildClass"); + } + + @Test + void executeDeleteRequests_keepClass_doesNotRemoveClass() { + service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(PARENT_CLASS_UUID, DeleteAction.KEEP))); + + assertResourceFullyPresent("ParentClass"); + } + + @Test + void executeDeleteRequests_removeSubclassReference_removesOnlySubClassOfTriple() { + service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(CHILD_CLASS_UUID, DeleteAction.REMOVE_SUBCLASS_REFERENCE))); + + var model = readModel(); + try { + var childClassResource = ResourceFactory.createResource(CIM_NS + "ChildClass"); + assertThat(model.listStatements(childClassResource, RDFS.subClassOf, (RDFNode) null).hasNext()).isFalse(); + assertThat(childClassResource.inModel(model).hasProperty(RDF.type, RDFS.Class)).isTrue(); + assertThat(childClassResource.inModel(model).hasProperty(RDFS.label)).isTrue(); + } finally { + endRead(); + } + } + + @Test + void executeDeleteRequests_removePackageReference_removesOnlyBelongsToCategoryTriple() { + service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(CHILD_CLASS_UUID, DeleteAction.REMOVE_PACKAGE_REFERENCE))); + + var model = readModel(); + try { + var childClassResource = ResourceFactory.createResource(CIM_NS + "ChildClass"); + assertThat(model.listStatements(childClassResource, CIMS.belongsToCategory, (RDFNode) null).hasNext()).isFalse(); + assertThat(childClassResource.inModel(model).hasProperty(RDF.type, RDFS.Class)).isTrue(); + assertThat(childClassResource.inModel(model).hasProperty(RDFS.label)).isTrue(); + } finally { + endRead(); + } + } + + // ==================== Delete attribute ==================== + + @Test + void executeDeleteRequests_deleteAttribute_removesAttribute() { + service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(ATTR1_UUID, DeleteAction.DELETE))); + + assertResourceRemovedOrOnlyUuid("ParentClass.attr1"); + } + + @Test + void executeDeleteRequests_keepAttribute_doesNotRemoveAttribute() { + service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(ATTR1_UUID, DeleteAction.KEEP))); + + assertResourceFullyPresent("ParentClass.attr1"); + } + + // ==================== Delete association ==================== + + @Test + void executeDeleteRequests_deleteAssociation_removesAssociationAndInverse() { + service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(ASSOC_NONEXISTING_UUID, DeleteAction.DELETE))); + + // Both the association and its inverse should have their properties removed + assertResourceRemovedOrOnlyUuid("AssociatedClass.NonExisting"); + assertResourceRemovedOrOnlyUuid("Temp.AssociatedClass"); + } + + @Test + void executeDeleteRequests_keepAssociation_doesNotRemoveAssociation() { + service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(ASSOC_NONEXISTING_UUID, DeleteAction.KEEP))); + + assertResourceFullyPresent("AssociatedClass.NonExisting"); + } + + @Test + void executeDeleteRequests_deleteAssociationOtherDirection_removesAssociationAndInverse() { + service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(ASSOC_ASSOCIATED_CHILD_UUID, DeleteAction.DELETE))); + + assertResourceRemovedOrOnlyUuid("AssociatedClass.ChildClass"); + assertResourceRemovedOrOnlyUuid("ChildClass.AssociatedClass"); + } + + // ==================== Delete class with external associations ==================== + + @Test + void executeDeleteRequests_deleteClassWithExternalAssociation_removesExternalAssociations() { + // AssociatedClass has an association to Temp (external, not in any package) + // Deleting AssociatedClass should remove associations referencing external resources + service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(ASSOCIATED_CLASS_UUID, DeleteAction.DELETE))); + + assertResourceRemovedOrOnlyUuid("AssociatedClass"); + assertResourceRemovedOrOnlyUuid("AssociatedClass.NonExisting"); + } + + @Test + void executeDeleteRequests_deleteClass_doesNotRemoveInternalAssociations() { + // AssociatedClass.ChildClass points to ChildClass (internal, in same package) + // Deleting AssociatedClass should NOT remove internal associations + service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(ASSOCIATED_CLASS_UUID, DeleteAction.DELETE))); + + assertResourceFullyPresent("AssociatedClass.ChildClass"); + } + + // ==================== Multiple delete requests ==================== + + @Test + void executeDeleteRequests_multipleRequests_executesAllInOrder() { + var requests = List.of( + request(ATTR1_UUID, DeleteAction.DELETE), + request(CHILD_CLASS_UUID, DeleteAction.REMOVE_SUBCLASS_REFERENCE), + request(ONTOLOGY_UUID, DeleteAction.DELETE) + ); + + service.executeDeleteRequests(GRAPH_IDENTIFIER, requests); + + // Attribute should be removed + assertResourceRemovedOrOnlyUuid("ParentClass.attr1"); + + // ChildClass should still exist but without subClassOf + var model = readModel(); + try { + var childClassResource = ResourceFactory.createResource(CIM_NS + "ChildClass"); + assertThat(childClassResource.inModel(model).hasProperty(RDF.type, RDFS.Class)).isTrue(); + assertThat(model.listStatements(childClassResource, RDFS.subClassOf, (RDFNode) null).hasNext()).isFalse(); + } finally { + endRead(); + } + + // Ontology should be removed + assertResourceRemovedOrOnlyUuid("Ontology"); + } + + // ==================== Invalid actions are skipped gracefully ==================== + + @Test + void executeDeleteRequests_unsupportedActionForPackage_skipsWithoutException() { + service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(PACKAGE_UUID, DeleteAction.REMOVE_SUBCLASS_REFERENCE))); + + assertResourceFullyPresent("Package_Package"); + } + + @Test + void executeDeleteRequests_nullAction_skipsWithoutException() { + service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(PARENT_CLASS_UUID, null))); + + assertResourceFullyPresent("ParentClass"); + } + + // ==================== Referenced resources preserve UUID ==================== + + @Test + void executeDeleteRequests_deleteReferencedResource_preservesUuidTriple() { + // DatatypeClass is referenced by ParentClass.attr1 via cims:dataType + service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(DATATYPE_CLASS_UUID, DeleteAction.DELETE))); + + var model = readModel(); + try { + var datatypeResource = ResourceFactory.createResource(CIM_NS + "DatatypeClass"); + + // UUID triple should be preserved since it's referenced elsewhere + var uuidStatements = model.listStatements(datatypeResource, RDFA.uuid, (RDFNode) null).toList(); + assertThat(uuidStatements).isNotEmpty(); + + // All other properties should be removed + var otherStatements = model.listStatements(datatypeResource, null, (RDFNode) null) + .filterDrop(stmt -> stmt.getPredicate().equals(RDFA.uuid)) + .toList(); + assertThat(otherStatements).isEmpty(); + } finally { + endRead(); + } + } +} diff --git a/backend/src/test/java/org/rdfarchitect/services/delete/FindDeleteDependenciesServiceTest.java b/backend/src/test/java/org/rdfarchitect/services/delete/FindDeleteDependenciesServiceTest.java new file mode 100644 index 00000000..1c032eed --- /dev/null +++ b/backend/src/test/java/org/rdfarchitect/services/delete/FindDeleteDependenciesServiceTest.java @@ -0,0 +1,310 @@ +/* + * 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.delete; + +import org.apache.jena.riot.Lang; +import org.apache.jena.riot.RDFDataMgr; +import org.apache.jena.sparql.graph.GraphFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.rdfarchitect.api.dto.delete.DeleteAction; +import org.rdfarchitect.api.dto.delete.relations.AffectedAssociation; +import org.rdfarchitect.api.dto.delete.relations.AffectedResource; +import org.rdfarchitect.api.dto.delete.relations.AffectedResource.AffectedResourceReason; +import org.rdfarchitect.database.DatabasePort; +import org.rdfarchitect.database.GraphIdentifier; +import org.rdfarchitect.database.inmemory.GraphWithContext; +import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils.CimResourceType; +import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class FindDeleteDependenciesServiceTest { + + private static final String TEST_DATA_PATH = "src/test/java/org/rdfarchitect/services/delete/testdata.ttl"; + + private static final GraphIdentifier GRAPH_IDENTIFIER = new GraphIdentifier("default", "default"); + + // UUIDs from the TTL test data + private static final UUID PARENT_CLASS_UUID = UUID.fromString("05131eaf-a7dd-4ac4-8624-9665990985ab"); + private static final UUID CHILD_CLASS_UUID = UUID.fromString("93ee2f31-5ddd-4b25-b119-e90a5ed327b0"); + private static final UUID ASSOCIATED_CLASS_UUID = UUID.fromString("f6d92056-c469-40d4-add1-1d6adf2fa7a6"); + private static final UUID PACKAGE_UUID = UUID.fromString("0351f9b6-5e91-4059-9d8b-169d28f1b2c8"); + private static final UUID DATATYPE_CLASS_UUID = UUID.fromString("db520255-95ef-40d4-b328-e1631e4683a4"); + private static final UUID ONTOLOGY_UUID = UUID.fromString("dba8f8e3-bfb3-4e62-9ca5-b0136ed186b2"); + + @Mock + private DatabasePort databasePort; + + @InjectMocks + private FindDeleteDependenciesService service; + + @BeforeEach + void setUp() throws IOException { + var graph = GraphFactory.createDefaultGraph(); + InputStream in = Files.newInputStream(Path.of(TEST_DATA_PATH)); + RDFDataMgr.read(graph, in, Lang.TTL); + in.close(); + + var wrappedGraph = new GraphRewindableWithUUIDs(graph, 5, 5); + var wrappedContext = new GraphWithContext(wrappedGraph); + + when(databasePort.getGraphWithContext(any(GraphIdentifier.class))).thenReturn(wrappedContext); + } + + // ==================== Simple resource types ==================== + + @Test + void getDeleteDependencies_ontology_returnsAffectedResourceWithDeleteAction() { + var result = service.getDeleteDependencies(GRAPH_IDENTIFIER, ONTOLOGY_UUID); + + assertThat(result.getResourceIdentifier().getUuid()).isEqualTo(ONTOLOGY_UUID); + assertThat(result.getType()).isEqualTo(CimResourceType.ONTOLOGY); + assertThat(result.getReason()).isEqualTo(AffectedResourceReason.DELETION_REQUESTED_BY_USER); + assertThat(result.getActions()).containsExactly(DeleteAction.DELETE); + } + + @Test + void getDeleteDependencies_datatypeClass_returnsClassWithAttributesDependingOnIt() { + var result = service.getDeleteDependencies(GRAPH_IDENTIFIER, DATATYPE_CLASS_UUID); + + assertThat(result.getResourceIdentifier().getUuid()).isEqualTo(DATATYPE_CLASS_UUID); + assertThat(result.getType()).isEqualTo(CimResourceType.CLASS); + assertThat(result.getActions()).containsExactly(DeleteAction.DELETE); + } + + // ==================== Class with child classes (inheritance) ==================== + + @Test + void getDeleteDependencies_classWithDirectChild_returnsChildAsAffectedResource() { + var result = service.getDeleteDependencies(GRAPH_IDENTIFIER, PARENT_CLASS_UUID); + + assertThat(result.getResourceIdentifier().getUuid()).isEqualTo(PARENT_CLASS_UUID); + assertThat(result.getType()).isEqualTo(CimResourceType.CLASS); + + var childClasses = result.getChildren().stream() + .filter(c -> c.getType() == CimResourceType.CLASS) + .toList(); + + assertThat(childClasses).isNotEmpty() + .anyMatch(c -> + c.getResourceIdentifier().getUuid().equals(CHILD_CLASS_UUID) + && c.getReason() == AffectedResourceReason.CHILD_OF); + } + + @Test + void getDeleteDependencies_classWithTransitiveChildren_returnsNestedHierarchy() { + // AssociatedClass -> ParentClass -> ChildClass (-> AssociatedClass = cycle) + var result = service.getDeleteDependencies(GRAPH_IDENTIFIER, ASSOCIATED_CLASS_UUID); + + assertThat(result.getResourceIdentifier().getUuid()).isEqualTo(ASSOCIATED_CLASS_UUID); + + // ParentClass should be a direct child + var parentClassChild = result.getChildren().stream() + .filter(c -> c.getType() == CimResourceType.CLASS + && c.getResourceIdentifier().getUuid().equals(PARENT_CLASS_UUID)) + .findFirst(); + assertThat(parentClassChild).isPresent(); + + // ChildClass should be nested under ParentClass, not flat + var childClassNested = parentClassChild.get().getChildren().stream() + .filter(c -> c.getType() == CimResourceType.CLASS + && c.getResourceIdentifier().getUuid().equals(CHILD_CLASS_UUID)) + .findFirst(); + assertThat(childClassNested).isPresent(); + } + + @Test + void getDeleteDependencies_cyclicInheritance_doesNotCauseInfiniteLoop() { + // AssociatedClass -> ChildClass -> ParentClass -> AssociatedClass = cycle + var result = service.getDeleteDependencies(GRAPH_IDENTIFIER, ASSOCIATED_CLASS_UUID); + + assertThat(result).isNotNull(); + assertThat(result.getResourceIdentifier().getUuid()).isEqualTo(ASSOCIATED_CLASS_UUID); + + // Each class must appear only once in the tree + var allChildClasses = flattenChildClasses(result); + var classUuids = allChildClasses.stream() + .map(c -> c.getResourceIdentifier().getUuid()) + .toList(); + assertThat(classUuids).doesNotHaveDuplicates(); + } + + @Test + void getDeleteDependencies_childClassActions_containDeleteKeepAndRemoveSubclassReference() { + var result = service.getDeleteDependencies(GRAPH_IDENTIFIER, ASSOCIATED_CLASS_UUID); + + var childClasses = result.getChildren().stream() + .filter(c -> c.getType() == CimResourceType.CLASS) + .toList(); + + assertThat(childClasses).isNotEmpty() + .allSatisfy(child -> + assertThat(child.getActions()).containsExactlyInAnyOrder( + DeleteAction.DELETE, DeleteAction.KEEP, DeleteAction.REMOVE_SUBCLASS_REFERENCE)); + } + + // ==================== Class with associations ==================== + + @Test + void getDeleteDependencies_classWithAssociations_returnsAssociationsAsAffectedResources() { + var result = service.getDeleteDependencies(GRAPH_IDENTIFIER, ASSOCIATED_CLASS_UUID); + + var associations = result.getChildren().stream() + .filter(c -> c.getType() == CimResourceType.ASSOCIATION) + .toList(); + + assertThat(associations).isNotEmpty() + .allSatisfy(assoc -> + assertThat(assoc.getReason()).isEqualTo(AffectedResourceReason.REFENCES_DELETED_CLASS_VIA_ASSOCIATION)); + } + + @Test + void getDeleteDependencies_associationWithTarget_returnsAffectedAssociationWithTarget() { + var result = service.getDeleteDependencies(GRAPH_IDENTIFIER, ASSOCIATED_CLASS_UUID); + + var affectedAssociations = result.getChildren().stream() + .filter(AffectedAssociation.class::isInstance) + .map(c -> (AffectedAssociation) c) + .toList(); + + assertThat(affectedAssociations).isNotEmpty() + .allSatisfy(assoc -> + assertThat(assoc.getTarget()).isNotNull()); + } + + // ==================== Child classes have their own dependencies ==================== + + @Test + void getDeleteDependencies_childClassWithAssociations_childHasAssociationsAsChildren() { + // When ParentClass is deleted, ChildClass should appear as a child + // and ChildClass should have its own associations as children + var result = service.getDeleteDependencies(GRAPH_IDENTIFIER, PARENT_CLASS_UUID); + + var childClassAffected = result.getChildren().stream() + .filter(c -> c.getType() == CimResourceType.CLASS + && c.getResourceIdentifier().getUuid().equals(CHILD_CLASS_UUID)) + .findFirst(); + assertThat(childClassAffected).isPresent(); + + var childAssociations = childClassAffected.get().getChildren().stream() + .filter(c -> c.getType() == CimResourceType.ASSOCIATION) + .toList(); + assertThat(childAssociations).isNotEmpty(); + } + + // ==================== Class used as datatype ==================== + + @Test + void getDeleteDependencies_classUsedAsDatatype_returnsAttributeAsAffectedResource() { + // DatatypeClass is used as datatype in ParentClass.attr1 + var result = service.getDeleteDependencies(GRAPH_IDENTIFIER, DATATYPE_CLASS_UUID); + + var attributes = result.getChildren().stream() + .filter(c -> c.getType() == CimResourceType.ATTRIBUTE) + .toList(); + + assertThat(attributes).isNotEmpty() + .allSatisfy(attr -> + assertThat(attr.getReason()).isEqualTo(AffectedResourceReason.USES_DELETED_CLASS_AS_DATATYPE)); + } + + // ==================== Delete package ==================== + + @Test + void getDeleteDependencies_package_returnsAllClassesInPackageAsChildren() { + var result = service.getDeleteDependencies(GRAPH_IDENTIFIER, PACKAGE_UUID); + + assertThat(result.getResourceIdentifier().getUuid()).isEqualTo(PACKAGE_UUID); + assertThat(result.getType()).isEqualTo(CimResourceType.PACKAGE); + assertThat(result.getReason()).isEqualTo(AffectedResourceReason.DELETION_REQUESTED_BY_USER); + + var childClasses = result.getChildren().stream() + .filter(c -> c.getType() == CimResourceType.CLASS) + .toList(); + + // Package_Package contains: ParentClass, ChildClass, AssociatedClass + assertThat(childClasses).hasSize(3); + + var childUuids = childClasses.stream() + .map(c -> c.getResourceIdentifier().getUuid()) + .toList(); + assertThat(childUuids).containsExactlyInAnyOrder( + PARENT_CLASS_UUID, CHILD_CLASS_UUID, ASSOCIATED_CLASS_UUID); + } + + @Test + void getDeleteDependencies_package_classesHaveCorrectReasonAndActions() { + var result = service.getDeleteDependencies(GRAPH_IDENTIFIER, PACKAGE_UUID); + + var childClasses = result.getChildren().stream() + .filter(c -> c.getType() == CimResourceType.CLASS) + .toList(); + + assertThat(childClasses).isNotEmpty() + .allSatisfy(cls -> { + assertThat(cls.getReason()).isEqualTo(AffectedResourceReason.CONTAINED_IN_PACKAGE); + assertThat(cls.getActions()).containsExactlyInAnyOrder( + DeleteAction.DELETE, DeleteAction.KEEP, DeleteAction.REMOVE_PACKAGE_REFERENCE); + }); + } + + @Test + void getDeleteDependencies_package_classesInPackageHaveTheirOwnDependencies() { + var result = service.getDeleteDependencies(GRAPH_IDENTIFIER, PACKAGE_UUID); + + var associatedClassAffected = result.getChildren().stream() + .filter(c -> c.getResourceIdentifier().getUuid().equals(ASSOCIATED_CLASS_UUID)) + .findFirst(); + assertThat(associatedClassAffected).isPresent(); + + // AssociatedClass has associations and child classes + assertThat(associatedClassAffected.get().getChildren()).isNotEmpty(); + } + + // ==================== Helper methods ==================== + + private List flattenChildClasses(AffectedResource root) { + var result = new ArrayList(); + if (root.getChildren() == null) { + return result; + } + for (var child : root.getChildren()) { + if (child.getType() == CimResourceType.CLASS) { + result.add(child); + result.addAll(flattenChildClasses(child)); + } + } + return result; + } +} diff --git a/backend/src/test/java/org/rdfarchitect/services/delete/testdata.ttl b/backend/src/test/java/org/rdfarchitect/services/delete/testdata.ttl new file mode 100644 index 00000000..a67455fa --- /dev/null +++ b/backend/src/test/java/org/rdfarchitect/services/delete/testdata.ttl @@ -0,0 +1,153 @@ + + a ; + + "Datatypes"@en; + "a5ede240-396a-44a6-91c6-4fbd588df864" . + + + a ; + + "Package"@en; + "0351f9b6-5e91-4059-9d8b-169d28f1b2c8" . + + + a ; + "dba8f8e3-bfb3-4e62-9ca5-b0136ed186b2"; + + "2026-04-20T14:19:03.9589539"^^; + + "2026-04-20"^^ . + + + a ; + + ; + + "NonExisting"@en; + + ; + "b2a306f1-4789-4a12-b045-04a014e7a937"; + + "No"; + + ; + + . + + + a ; + + ; + + "AssociatedClass"@en; + + ; + "c3b417f2-5890-5b23-c156-15b015e8b048"; + + "No"; + + ; + + . + + + "45168d29-0a45-42f1-bb51-1a8efc03bac1" . + + + a ; + + ; + + "ChildClass"@en; + + ; + "abfab117-e4bc-4814-9b5a-3aed72de8a2d"; + + "No"; + + ; + + . + + + a ; + + ; + + "AssociatedClass"@en; + + ; + "bc0bc228-f5cd-5925-ab6b-4bfe83ef9b3e"; + + "No"; + + ; + + . + + + a ; + + "AssociatedClass"@en; + + ; + "f6d92056-c469-40d4-add1-1d6adf2fa7a6"; + + . + + + a ; + + ; + + "attr1"@en; + "dc934c7b-6c4d-4177-9832-bcb955f25414"; + + ; + + ; + + . + + + "3b04c3fc-44bf-46fc-a0b2-fd3e799ac35e" . + + + "0998527c-7e84-4442-ac48-02cad69e1342" . + + + "569fb7d4-0e27-4ef8-8465-65bb9358b7be" . + + + a ; + + "DatatypeClass"@en; + "db520255-95ef-40d4-b328-e1631e4683a4"; + + ; + + "Primitive" . + + + a ; + + "ChildClass"@en; + + ; + "93ee2f31-5ddd-4b25-b119-e90a5ed327b0"; + + ; + + . + + + a ; + + "ParentClass"@en; + + ; + "05131eaf-a7dd-4ac4-8624-9665990985ab"; + + ; + + . \ No newline at end of file diff --git a/frontend/src/routes/NewClassDialog.svelte b/frontend/src/routes/NewClassDialog.svelte index 13f2ab02..5b0b84c8 100644 --- a/frontend/src/routes/NewClassDialog.svelte +++ b/frontend/src/routes/NewClassDialog.svelte @@ -110,7 +110,7 @@ (className = new ReactiveValueWrapper(className?.value, label => isInvalidClassLabel( label, - classURINamespace.value, + classURINamespace?.value, compareClasses, ), )), @@ -125,7 +125,11 @@ classURINamespace = new ReactiveValueWrapper(null); className = new ReactiveValueWrapper("", label => - isInvalidClassLabel(label, classURINamespace.value, compareClasses), + isInvalidClassLabel( + label, + classURINamespace?.value, + compareClasses, + ), ); if (!datasetName) { @@ -209,13 +213,12 @@ }), body: JSON.stringify({ packageDTO, - classURIPrefix: classURINamespaceLocal.value, - className: classNameLocal.value, + classURIPrefix: classURINamespaceLocal?.value, + className: classNameLocal?.value, }), credentials: "include", }, ); - if (res.ok) { const uuid = await res.text(); console.log("successfully added class"); @@ -283,7 +286,7 @@ - {#if className} + {#if className && classURINamespace} { - bec.deleteOntology( - datasetNavEntry.id, - graphNavEntry.id, - ); - initialize(); + showDeleteDependenciesDialog = true; }} variant="danger" faIcon={faTrash} @@ -376,3 +374,11 @@ {readonly} onSubmit={initialize} /> + + From e823eb92eeebe99276479e4661c84641c9e22f51 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Tue, 21 Apr 2026 08:46:11 +0200 Subject: [PATCH 14/31] Added: deleterequest for ontologies in header menu --- .../src/routes/layout/menu-bar/Edit.svelte | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/frontend/src/routes/layout/menu-bar/Edit.svelte b/frontend/src/routes/layout/menu-bar/Edit.svelte index 20b3edff..d5ac85a8 100644 --- a/frontend/src/routes/layout/menu-bar/Edit.svelte +++ b/frontend/src/routes/layout/menu-bar/Edit.svelte @@ -61,6 +61,7 @@ let showNewPackageDialog = $state(false); let showFilterViewDialog = $state(false); let showDeleteDependenciesDialog = $state(false); + let showOntologyDeleteDependenciesDialog = $state(false); let showPackageEditorDialog = $state(false); let showNamespaceDialog = $state(false); let showEditOntologyDialog = $state(false); @@ -337,12 +338,8 @@ { - await bec.deleteOntology( - selectedDataset, - selectedGraph, - ); - reload(); + onSelect={() => { + showOntologyDeleteDependenciesDialog = true; }} disabled={!hasGraphSelected || !graphHasOntology} faIcon={faTrash} @@ -386,6 +383,16 @@ {/if} +{#if ontology} + +{/if} + {#if showEditOntologyDialog} Date: Tue, 21 Apr 2026 13:30:50 +0200 Subject: [PATCH 15/31] Added another class delete dialog to the new in diagram context menu --- .../svelteflow/svelteFlowWrapper.svelte | 47 ++++++++++++++----- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/frontend/src/lib/rendering/svelteflow/svelteFlowWrapper.svelte b/frontend/src/lib/rendering/svelteflow/svelteFlowWrapper.svelte index b6fc5226..01381fb6 100644 --- a/frontend/src/lib/rendering/svelteflow/svelteFlowWrapper.svelte +++ b/frontend/src/lib/rendering/svelteflow/svelteFlowWrapper.svelte @@ -25,7 +25,7 @@ useSvelteFlow, } from "@xyflow/svelte"; import ElkWorkerURL from "elkjs/lib/elk-worker.js?url"; - import ELK from "elkjs/lib/elk.bundled.js"; + import ELK from "elkjs/lib/elk.bundled.js"; //keep this import! the 'elkjs' import has a bug import { onMount, untrack } from "svelte"; import { BackendConnection } from "$lib/api/backend.js"; @@ -42,6 +42,8 @@ import InheritanceEdge from "./components/InheritanceEdge.svelte"; import SvelteFlowClassContextMenu from "./components/SvelteFlowClassContextMenu.svelte"; import SvelteFlowPaneContextMenu from "./components/SvelteFlowPaneContextMenu.svelte"; + import DeleteDependenciesDialog from "../../../routes/delete-relations-dialog/DeleteDependenciesDialog.svelte"; + import NewClassDialog from "../../../routes/NewClassDialog.svelte"; let { nodes: inputNodes, @@ -68,6 +70,9 @@ let paneContextMenuRequest = $state(null); let classContextMenuRequest = $state(null); let contextMenuClass = $state(null); + let deleteClassTarget = $state(null); + let showDeleteDependenciesDialog = $state(false); + let showNewClassDialog = $state(false); let pendingNewClassPlacement = null; // Ordered list of node IDs from back (index 0) to front (index n-1). @@ -500,6 +505,9 @@ // All indices up to and including original idx shifted changedIds = next.slice(0, idx + 1); } + deleteClassTarget = contextMenuClass; + showDeleteDependenciesDialog = true; + closeContextMenus(); nodeOrder = next; nodes = applyZIndicesFromOrder(nodes); @@ -697,16 +705,31 @@ onClose={closeContextMenus} />
+ + + + + From 3ad50543b94630ab9cf958491331fa51c0c740a3 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Mon, 27 Apr 2026 09:49:49 +0200 Subject: [PATCH 16/31] backend lint --- .../datasets/graphs/DeleteRESTController.java | 118 ++++--- .../classes/AllClassesRESTController.java | 151 ++++++--- .../api/dto/delete/DeleteAction.java | 20 +- .../api/dto/delete/ResourceIdentifier.java | 2 +- .../delete/relations/AffectedAssociation.java | 12 +- .../relations/AffectedOwnedResource.java | 10 +- .../delete/relations/AffectedResource.java | 8 +- .../CIMResourceTypeIdentifyingUtils.java | 46 ++- .../cim/relations/model/CIMResourceUtils.java | 10 +- .../wrapper/GraphRewindableWithUUIDs.java | 49 +-- .../delete/DeleteResourcesService.java | 136 +++++--- .../delete/DeleteResourcesUseCase.java | 4 +- .../delete/FindDeleteDependenciesService.java | 217 ++++++++---- .../delete/FindDeleteDependenciesUseCase.java | 4 +- .../services/select/GetClassListUseCase.java | 4 +- .../services/select/QueryGraphService.java | 309 ++++++++++-------- .../delete/DeleteResourcesServiceTest.java | 175 ++++++---- .../FindDeleteDependenciesServiceTest.java | 247 ++++++++------ 18 files changed, 939 insertions(+), 583 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeleteRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeleteRESTController.java index 92cd443a..8a9f7f60 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeleteRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/DeleteRESTController.java @@ -20,7 +20,9 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; + import lombok.RequiredArgsConstructor; + import org.rdfarchitect.api.dto.delete.ResourceDeleteRequest; import org.rdfarchitect.api.dto.delete.relations.AffectedResource; import org.rdfarchitect.database.GraphIdentifier; @@ -53,67 +55,93 @@ public class DeleteRESTController { private final DeleteResourcesUseCase deleteResourcesUseCase; @Operation( - summary = "Get deletion impact", - description = "Returns a tree of affected resources for deleting the resource with the given UUID.", - tags = {"graph"}, - responses = { - @ApiResponse(responseCode = "200") - } - ) + summary = "Get deletion impact", + description = + "Returns a tree of affected resources for deleting the resource with the given UUID.", + tags = {"graph"}, + responses = {@ApiResponse(responseCode = "200")}) @GetMapping("/uuid/{uuid}/deletion-impact") public AffectedResource getDeletionImpact( - @Parameter(description = "The name/url of the inquirer.") - @RequestHeader(value = HttpHeaders.ORIGIN, required = false, defaultValue = "unknown") - String originURL, - @Parameter(description = "The literal name of the dataset.") - @PathVariable - String datasetName, - @Parameter(description = "The url encoded uri of the graph, or \"default\" to access the default graph.") - @PathVariable - String graphURI, - @Parameter(description = "The url encoded iri identifier of the cim resource.") - @PathVariable - String uuid) { - logger.info("Received GET request: \"/api/datasets/{{}}/graphs/{{}}/uuid/{{}/deletion-impact\" from \"{}\".", datasetName, graphURI, uuid, originURL); + @Parameter(description = "The name/url of the inquirer.") + @RequestHeader( + value = HttpHeaders.ORIGIN, + required = false, + defaultValue = "unknown") + String originURL, + @Parameter(description = "The literal name of the dataset.") @PathVariable + String datasetName, + @Parameter( + description = + "The url encoded uri of the graph, or \"default\" to access the default graph.") + @PathVariable + String graphURI, + @Parameter(description = "The url encoded iri identifier of the cim resource.") + @PathVariable + String uuid) { + logger.info( + "Received GET request: \"/api/datasets/{{}}/graphs/{{}}/uuid/{{}/deletion-impact\" from \"{}\".", + datasetName, + graphURI, + uuid, + originURL); var extendedGraphURI = expandURIUseCase.expandUri(datasetName, graphURI); - var resultObj = findDeleteDependenciesUseCase.getDeleteDependencies(new GraphIdentifier(datasetName, extendedGraphURI), UUID.fromString(uuid)); + var resultObj = + findDeleteDependenciesUseCase.getDeleteDependencies( + new GraphIdentifier(datasetName, extendedGraphURI), UUID.fromString(uuid)); - logger.info("Sending response to GET request: \"/api/datasets/{{}}/graphs/{{}}/uuid/{{}/deletion-impact\" from \"{}\".", datasetName, graphURI, uuid, originURL); + logger.info( + "Sending response to GET request: \"/api/datasets/{{}}/graphs/{{}}/uuid/{{}/deletion-impact\" from \"{}\".", + datasetName, + graphURI, + uuid, + originURL); return resultObj; } @Operation( - summary = "Delete resources", - description = "Processes a list of delete requests, each specifying a resource UUID and the desired action.", - tags = {"graph"}, - responses = { - @ApiResponse(responseCode = "200") - } - ) + summary = "Delete resources", + description = + "Processes a list of delete requests, each specifying a resource UUID and the desired action.", + tags = {"graph"}, + responses = {@ApiResponse(responseCode = "200")}) @PostMapping("/delete-requests") public String deleteResources( - @Parameter(description = "The name/url of the inquirer.") - @RequestHeader(value = HttpHeaders.ORIGIN, required = false, defaultValue = "unknown") - String originURL, - @Parameter(description = "The literal name of the dataset.") - @PathVariable - String datasetName, - @Parameter(description = "The url encoded uri of the graph, or \"default\" to access the default graph.") - @PathVariable - String graphURI, - @Parameter(description = "A list of resource delete requests, each containing the uuid of the resource to delete and the type of deletion") - @RequestBody - List deleteRequests - ) { - logger.info("Received POST request: \"/api/datasets/{{}}/graphs/{{}}/delete\" from \"{}\".", datasetName, graphURI, originURL); + @Parameter(description = "The name/url of the inquirer.") + @RequestHeader( + value = HttpHeaders.ORIGIN, + required = false, + defaultValue = "unknown") + String originURL, + @Parameter(description = "The literal name of the dataset.") @PathVariable + String datasetName, + @Parameter( + description = + "The url encoded uri of the graph, or \"default\" to access the default graph.") + @PathVariable + String graphURI, + @Parameter( + description = + "A list of resource delete requests, each containing the uuid of the resource to delete and the type of deletion") + @RequestBody + List deleteRequests) { + logger.info( + "Received POST request: \"/api/datasets/{{}}/graphs/{{}}/delete\" from \"{}\".", + datasetName, + graphURI, + originURL); var extendedGraphURI = expandURIUseCase.expandUri(datasetName, graphURI); - deleteResourcesUseCase.executeDeleteRequests(new GraphIdentifier(datasetName, extendedGraphURI), deleteRequests); + deleteResourcesUseCase.executeDeleteRequests( + new GraphIdentifier(datasetName, extendedGraphURI), deleteRequests); - logger.info("Sending response to POST request: \"/api/datasets/{{}}/graphs/{{}}/delete\" from \"{}\".", datasetName, graphURI, originURL); + logger.info( + "Sending response to POST request: \"/api/datasets/{{}}/graphs/{{}}/delete\" from \"{}\".", + datasetName, + graphURI, + originURL); return "success"; } } diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/AllClassesRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/AllClassesRESTController.java index 6d8f022d..6231f1bd 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/AllClassesRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/AllClassesRESTController.java @@ -23,7 +23,10 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; + import lombok.RequiredArgsConstructor; + +import org.rdfarchitect.api.controller.Response; import org.rdfarchitect.api.dto.ClassUMLAdaptedDTO; import org.rdfarchitect.api.dto.packages.PackageDTO; import org.rdfarchitect.database.GraphIdentifier; @@ -56,84 +59,124 @@ public class AllClassesRESTController { private final AddClassUseCase addClassUseCase; /** - * Helper record, functions as DTO for accepting the necessary information for adding a new class + * Helper record, functions as DTO for accepting the necessary information for adding a new + * class * - * @param packageDTO PackageDTO object of the package to which the new class is going to be added + * @param packageDTO PackageDTO object of the package to which the new class is going to be + * added * @param classURIPrefix URI Prefix of the new class - * @param className Label of the new class + * @param className Label of the new class */ - public record AddNewClassRequest(PackageDTO packageDTO, String classURIPrefix, String className) { - - } + public record AddNewClassRequest( + PackageDTO packageDTO, String classURIPrefix, String className) {} @Operation( - summary = "create new class", - description = "Create a new class with default name and no attributes, stereotypes or associations. Because no concrete stereotype is added the class is abstract " + - "by default.", - tags = {"graph"} - ) + summary = "create new class", + description = + "Create a new class with default name and no attributes, stereotypes or associations. Because no concrete stereotype is added the class is abstract " + + "by default.", + tags = {"graph"}) @PostMapping public String addClass( - @Parameter(description = "The name/url of the inquirer.") - @RequestHeader(value = HttpHeaders.ORIGIN, required = false, defaultValue = "unknown") - String originURL, - @Parameter(description = "The literal name of the dataset.") - @PathVariable - String datasetName, - @Parameter(description = "The url encoded uri of the graph, or \"default\" to access the default graph.") - @PathVariable - String graphURI, - @io.swagger.v3.oas.annotations.parameters.RequestBody( - required = true, - description = "Helper record, functions as DTO for accepting the necessary information for adding a new class" - ) - @RequestBody AddNewClassRequest addNewClassRequest) { - logger.info("Received POST request: \"/api/datasets/{{}}/graphs/{{}}/classes\" from \"{}\".", datasetName, graphURI, originURL); + @Parameter(description = "The name/url of the inquirer.") + @RequestHeader( + value = HttpHeaders.ORIGIN, + required = false, + defaultValue = "unknown") + String originURL, + @Parameter(description = "The literal name of the dataset.") @PathVariable + String datasetName, + @Parameter( + description = + "The url encoded uri of the graph, or \"default\" to access the default graph.") + @PathVariable + String graphURI, + @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + description = + "Helper record, functions as DTO for accepting the necessary information for adding a new class") + @RequestBody + AddNewClassRequest addNewClassRequest) { + logger.info( + "Received POST request: \"/api/datasets/{{}}/graphs/{{}}/classes\" from \"{}\".", + datasetName, + graphURI, + originURL); var extendedGraphURI = expandURIUseCase.expandUri(datasetName, graphURI); - var extendedClassURIPrefix = expandURIUseCase.expandUri(datasetName, addNewClassRequest.classURIPrefix); + var extendedClassURIPrefix = + expandURIUseCase.expandUri(datasetName, addNewClassRequest.classURIPrefix); var graphIdentifier = new GraphIdentifier(datasetName, extendedGraphURI); - var classUUID =addClassUseCase.addClass(graphIdentifier, addNewClassRequest.packageDTO, extendedClassURIPrefix, addNewClassRequest.className); + var classUUID =addClassUseCase.addClass( + graphIdentifier, + addNewClassRequest.packageDTO, + extendedClassURIPrefix, + addNewClassRequest.className); - logger.info("Sending response to POST request: \"/api/datasets/{{}}/graphs/{{}}/classes\" to \"{}\".", datasetName, graphURI, originURL); + logger.info( + "Sending response to POST request: \"/api/datasets/{{}}/graphs/{{}}/classes\" to \"{}\".", + datasetName, + graphURI, + originURL); return classUUID.toString(); } @Operation( - summary = "list classes", - description = "Get a list containing all classes. Doesn't include: stereotypes, attributes and associations.", - tags = {"graph"}, - responses = {@ApiResponse( + summary = "list classes", + description = + "Get a list containing all classes. Doesn't include: stereotypes, attributes and associations.", + tags = {"graph"}, + responses = { + @ApiResponse( responseCode = "200", - content = @Content( - mediaType = "application/json", - array = @ArraySchema(schema = @Schema(implementation = ClassUMLAdaptedDTO.class)) - )) - } - ) + content = + @Content( + mediaType = "application/json", + array = + @ArraySchema( + schema = + @Schema( + implementation = + ClassUMLAdaptedDTO + .class)))) + }) @GetMapping public List getClassList( - @Parameter(description = "The name/url of the inquirer.") - @RequestHeader(value = HttpHeaders.ORIGIN, required = false, defaultValue = "unknown") - String originURL, - @Parameter(description = "The literal name of the dataset.") - @PathVariable - String datasetName, - @Parameter(description = "The url encoded uri of the graph, or \"default\" to access the default graph.") - @PathVariable - String graphURI, - @Parameter(description = "Whether to include external classes.") - @RequestParam(required = false, defaultValue = "false") - boolean includeExternalClasses){ - logger.info("Received GET request: \"/api/datasets/{{}}/graphs/{{}}/classes\" from \"{}\".", datasetName, graphURI, originURL); + @Parameter(description = "The name/url of the inquirer.") + @RequestHeader( + value = HttpHeaders.ORIGIN, + required = false, + defaultValue = "unknown") + String originURL, + @Parameter(description = "The literal name of the dataset.") @PathVariable + String datasetName, + @Parameter( + description = + "The url encoded uri of the graph, or \"default\" to access the default graph.") + @PathVariable + String graphURI, + @Parameter(description = "Whether to include external classes.") + @RequestParam(required = false, defaultValue = "false") + boolean includeExternalClasses) { + logger.info( + "Received GET request: \"/api/datasets/{{}}/graphs/{{}}/classes\" from \"{}\".", + datasetName, + graphURI, + originURL); var extendedGraphURI = expandURIUseCase.expandUri(datasetName, graphURI); - var cimClassList = getClassListUseCase.getClassList(new GraphIdentifier(datasetName, extendedGraphURI), includeExternalClasses); + var cimClassList = + getClassListUseCase.getClassList( + new GraphIdentifier(datasetName, extendedGraphURI), includeExternalClasses); - logger.info("Sending response to GET request: \"/api/datasets/{{}}/graphs/{{}}/classes\" to \"{}\".", datasetName, graphURI, originURL); + logger.info( + "Sending response to GET request: \"/api/datasets/{{}}/graphs/{{}}/classes\" to \"{}\".", + datasetName, + graphURI, + originURL); return cimClassList; } } diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteAction.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteAction.java index 1f33e203..e19be9e1 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteAction.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/DeleteAction.java @@ -19,29 +19,23 @@ public enum DeleteAction { - /** - * Delete the affected resource entirely. - * Applicable to all resource types. - */ + /** Delete the affected resource entirely. Applicable to all resource types. */ DELETE, /** - * Keep the affected resource as-is, even though - * it references a deleted resource. - * E.g. a class that extends a deleted class — keep the class - * but accept that the parent reference becomes invalid. + * Keep the affected resource as-is, even though it references a deleted resource. E.g. a class + * that extends a deleted class — keep the class but accept that the parent reference becomes + * invalid. */ KEEP, /** - * Remove the {@code cims:belongsToCategory} triple from a class - * whose package is being deleted. + * Remove the {@code cims:belongsToCategory} triple from a class whose package is being deleted. */ REMOVE_PACKAGE_REFERENCE, /** - * Remove the {@code rdfs:subClassOf} triple from a class - * whose parent class is being deleted. + * Remove the {@code rdfs:subClassOf} triple from a class whose parent class is being deleted. */ REMOVE_SUBCLASS_REFERENCE; -} \ No newline at end of file +} diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/ResourceIdentifier.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/ResourceIdentifier.java index 979be1aa..08aa66a3 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/ResourceIdentifier.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/ResourceIdentifier.java @@ -28,4 +28,4 @@ public class ResourceIdentifier { private UUID uuid; private String label; private String namespace; -} \ No newline at end of file +} diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedAssociation.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedAssociation.java index 5b2704dd..2752e597 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedAssociation.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedAssociation.java @@ -21,6 +21,7 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; + import org.rdfarchitect.api.dto.delete.ResourceIdentifier; import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils; @@ -32,11 +33,12 @@ public class AffectedAssociation extends AffectedOwnedResource { private ResourceIdentifier target; - public AffectedAssociation(ResourceIdentifier resourceIdentifier, - CIMResourceTypeIdentifyingUtils.CimResourceType type, - AffectedResourceReason reason, - ResourceIdentifier domain, - ResourceIdentifier target) { + public AffectedAssociation( + ResourceIdentifier resourceIdentifier, + CIMResourceTypeIdentifyingUtils.CimResourceType type, + AffectedResourceReason reason, + ResourceIdentifier domain, + ResourceIdentifier target) { this.target = target; super(resourceIdentifier, type, reason, domain); } diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedOwnedResource.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedOwnedResource.java index e449ede8..6ec2e4cd 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedOwnedResource.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedOwnedResource.java @@ -21,6 +21,7 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; + import org.rdfarchitect.api.dto.delete.ResourceIdentifier; import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils; @@ -32,10 +33,11 @@ public class AffectedOwnedResource extends AffectedResource { private ResourceIdentifier domain; - public AffectedOwnedResource(ResourceIdentifier resourceIdentifier, - CIMResourceTypeIdentifyingUtils.CimResourceType type, - AffectedResourceReason reason, - ResourceIdentifier domain) { + public AffectedOwnedResource( + ResourceIdentifier resourceIdentifier, + CIMResourceTypeIdentifyingUtils.CimResourceType type, + AffectedResourceReason reason, + ResourceIdentifier domain) { this.domain = domain; super(resourceIdentifier, type, reason); } diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java index 104869a5..66e89e16 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java @@ -20,6 +20,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; + import org.rdfarchitect.api.dto.delete.DeleteAction; import org.rdfarchitect.api.dto.delete.ResourceIdentifier; import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils; @@ -46,9 +47,10 @@ public class AffectedResource { private Map context = new HashMap<>(); - public AffectedResource(ResourceIdentifier resourceIdentifier, - CIMResourceTypeIdentifyingUtils.CimResourceType type, - AffectedResourceReason reason) { + public AffectedResource( + ResourceIdentifier resourceIdentifier, + CIMResourceTypeIdentifyingUtils.CimResourceType type, + AffectedResourceReason reason) { this.resourceIdentifier = resourceIdentifier; this.type = type; this.reason = reason; diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java index 9c76c2d6..c7c8fe1a 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceTypeIdentifyingUtils.java @@ -18,6 +18,7 @@ package org.rdfarchitect.models.cim.relations.model; import lombok.experimental.UtilityClass; + import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Resource; import org.apache.jena.vocabulary.OWL2; @@ -45,33 +46,40 @@ public enum CimResourceType { UNKNOWN, } - private record TypeRule(Predicate matches, CimResourceType type) { - - } + private record TypeRule(Predicate matches, CimResourceType type) {} - private static final List TYPE_RULES = List.of( - new TypeRule(s -> s.hasProperty(RDF.type, CIMS.classCategory), CimResourceType.PACKAGE), - new TypeRule(s -> s.hasProperty(RDF.type, RDFS.Class), CimResourceType.CLASS), - new TypeRule(s -> s.hasProperty(RDF.type, OWL2.Ontology), CimResourceType.ONTOLOGY), - new TypeRule(CIMPropertyUtils::isAttribute, CimResourceType.ATTRIBUTE), - new TypeRule(CIMPropertyUtils::isAssociation, CimResourceType.ASSOCIATION), - new TypeRule(CIMResourceTypeIdentifyingUtils::isEnumEntry, CimResourceType.ENUM_ENTRY) - ); + private static final List TYPE_RULES = + List.of( + new TypeRule( + s -> s.hasProperty(RDF.type, CIMS.classCategory), + CimResourceType.PACKAGE), + new TypeRule(s -> s.hasProperty(RDF.type, RDFS.Class), CimResourceType.CLASS), + new TypeRule( + s -> s.hasProperty(RDF.type, OWL2.Ontology), CimResourceType.ONTOLOGY), + new TypeRule(CIMPropertyUtils::isAttribute, CimResourceType.ATTRIBUTE), + new TypeRule(CIMPropertyUtils::isAssociation, CimResourceType.ASSOCIATION), + new TypeRule( + CIMResourceTypeIdentifyingUtils::isEnumEntry, + CimResourceType.ENUM_ENTRY)); public CimResourceType getType(Model model, UUID uuid) { var subject = findUniqueSubject(model, uuid); return TYPE_RULES.stream() - .filter(rule -> rule.matches().test(subject)) - .map(TypeRule::type) - .findFirst() - .orElse(CimResourceType.UNKNOWN); + .filter(rule -> rule.matches().test(subject)) + .map(TypeRule::type) + .findFirst() + .orElse(CimResourceType.UNKNOWN); } public Resource findUniqueSubject(Model model, UUID uuid) { var subjects = model.listSubjectsWithProperty(RDFA.uuid, uuid.toString()).toList(); if (subjects.size() != 1) { - throw new IllegalArgumentException("Expected exactly one subject with UUID " + uuid + ", but found " + subjects.size()); + throw new IllegalArgumentException( + "Expected exactly one subject with UUID " + + uuid + + ", but found " + + subjects.size()); } return subjects.getFirst(); } @@ -81,7 +89,9 @@ public boolean isEnumEntry(Resource subject) { if (types.size() != 1 || !types.getFirst().getObject().isURIResource()) { return false; } - return types.getFirst().getObject().asResource() - .hasProperty(CIMS.stereotype, CIMStereotypes.enumeration); + return types.getFirst() + .getObject() + .asResource() + .hasProperty(CIMS.stereotype, CIMStereotypes.enumeration); } } diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceUtils.java b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceUtils.java index 05f0562e..9c82eff9 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceUtils.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceUtils.java @@ -18,6 +18,7 @@ package org.rdfarchitect.models.cim.relations.model; import lombok.experimental.UtilityClass; + import org.apache.jena.rdf.model.Resource; import org.rdfarchitect.models.cim.rdf.resources.RDFA; @@ -27,18 +28,21 @@ public class CIMResourceUtils { /** - * Checks whether a resource is external/referenced only. In our model this would mean it only has a {@link RDFA::uuid} property, but no other properties. + * Checks whether a resource is external/referenced only. In our model this would mean it only + * has a {@link RDFA::uuid} property, but no other properties. + * * @param resource The resource to check for. * @return True if the resource is an external resource, false otherwise. */ public boolean isExternalResource(Resource resource) { return !resource.listProperties() - .filterDrop(stmt -> stmt.getPredicate().equals(RDFA.uuid)) - .hasNext(); + .filterDrop(stmt -> stmt.getPredicate().equals(RDFA.uuid)) + .hasNext(); } /** * Finds the uuid of a resource. + * * @param resource The resource to finde the uuid for. * @return The uuid of the resource. */ diff --git a/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/GraphRewindableWithUUIDs.java b/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/GraphRewindableWithUUIDs.java index f619903c..f2a16f22 100644 --- a/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/GraphRewindableWithUUIDs.java +++ b/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/GraphRewindableWithUUIDs.java @@ -28,9 +28,9 @@ import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; import org.jetbrains.annotations.NotNull; -import org.rdfarchitect.models.cim.rdf.resources.RDFA; import org.rdfarchitect.exception.graph.GraphNotInATransactionException; import org.rdfarchitect.exception.graph.GraphTransactionException; +import org.rdfarchitect.models.cim.rdf.resources.RDFA; import org.rdfarchitect.rdf.graph.DeltaCompressible; import java.util.HashSet; @@ -39,14 +39,17 @@ public class GraphRewindableWithUUIDs extends GraphRewindable { - private static final Set RELEVANT_TYPES = Set.of(RDF.Property.toString(), RDFS.Class.toString()); + private static final Set RELEVANT_TYPES = + Set.of(RDF.Property.toString(), RDFS.Class.toString()); /** - * Accepts a {@link Graph} that serves as a base version of the {@link GraphRewindableWithUUIDs}. + * Accepts a {@link Graph} that serves as a base version of the {@link + * GraphRewindableWithUUIDs}. * - * @param base The base graph - * @param maxVersions The maximum amount of versions the graph stores. - * @param compressCount The amount of versions that are compressed to a new base when compressing. + * @param base The base graph + * @param maxVersions The maximum amount of versions the graph stores. + * @param compressCount The amount of versions that are compressed to a new base when + * compressing. */ public GraphRewindableWithUUIDs(@NotNull Graph base, int maxVersions, int compressCount) { super(enhanceWithUUIDs(base), maxVersions, compressCount); @@ -83,9 +86,10 @@ static Graph enhanceWithUUIDs(Graph graph) { } private static void addUUIDsToTypedResources(Model model) { - var subjects = model.listResourcesWithProperty(RDF.type) - .filterKeep(r -> r.isURIResource() && !r.hasProperty(RDFA.uuid)) - .toSet(); + var subjects = + model.listResourcesWithProperty(RDF.type) + .filterKeep(r -> r.isURIResource() && !r.hasProperty(RDFA.uuid)) + .toSet(); for (var subject : subjects) { subject.addProperty(RDFA.uuid, createUUID()); @@ -96,29 +100,30 @@ private static void addUUIDsToReferencedOnlyResources(Model model) { var objects = new HashSet(); model.listResourcesWithProperty(RDF.type) - .filterKeep(r -> r.isURIResource() && hasAnyType(r)) - .forEachRemaining(subject -> - subject.listProperties() - .mapWith(Statement::getObject) - .filterKeep(GraphRewindableWithUUIDs::isReferencedOnlyURI) - .mapWith(RDFNode::asResource) - .forEachRemaining(objects::add)); + .filterKeep(r -> r.isURIResource() && hasAnyType(r)) + .forEachRemaining( + subject -> + subject.listProperties() + .mapWith(Statement::getObject) + .filterKeep(GraphRewindableWithUUIDs::isReferencedOnlyURI) + .mapWith(RDFNode::asResource) + .forEachRemaining(objects::add)); objects.forEach(o -> o.addProperty(RDFA.uuid, createUUID())); } private static boolean hasAnyType(Resource resource) { return resource.listProperties(RDF.type) - .mapWith(Statement::getObject) - .filterKeep(o -> RELEVANT_TYPES.contains(o.asResource().getURI())) - .hasNext(); + .mapWith(Statement::getObject) + .filterKeep(o -> RELEVANT_TYPES.contains(o.asResource().getURI())) + .hasNext(); } private static boolean isReferencedOnlyURI(RDFNode node) { return node.isURIResource() - && !RELEVANT_TYPES.contains(node.asResource().getURI()) - && !node.asResource().hasProperty(RDFA.uuid) - && !node.asResource().listProperties().hasNext(); + && !RELEVANT_TYPES.contains(node.asResource().getURI()) + && !node.asResource().hasProperty(RDFA.uuid) + && !node.asResource().listProperties().hasNext(); } private static String createUUID() { diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java index fc627c5d..b52e9c30 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java @@ -18,6 +18,7 @@ package org.rdfarchitect.services.delete; import lombok.RequiredArgsConstructor; + import org.apache.jena.query.TxnType; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; @@ -52,7 +53,8 @@ public class DeleteResourcesService implements DeleteResourcesUseCase { private final DatabasePort databasePort; @Override - public void executeDeleteRequests(GraphIdentifier graphIdentifier, List deleteRequests) { + public void executeDeleteRequests( + GraphIdentifier graphIdentifier, List deleteRequests) { GraphRewindable graph = null; try { graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); @@ -71,9 +73,16 @@ private void deleteResources(Model model, List deleteRequ try { deleteResource(model, deleteRequest); } catch (UnsupportedOperationException | IllegalArgumentException e) { - logger.warn("Skipping deletion of resource with UUID {} due to unsupported action: {} : {}", deleteRequest.getUuid(), deleteRequest.getAction(), e.getMessage()); + logger.warn( + "Skipping deletion of resource with UUID {} due to unsupported action: {} : {}", + deleteRequest.getUuid(), + deleteRequest.getAction(), + e.getMessage()); } catch (IllegalStateException e) { - logger.warn("Skipping deletion of resource with UUID {} due to illegal state: {}", deleteRequest.getUuid(), e.getMessage()); + logger.warn( + "Skipping deletion of resource with UUID {} due to illegal state: {}", + deleteRequest.getUuid(), + e.getMessage()); } } } @@ -87,20 +96,23 @@ private void deleteResource(Model model, ResourceDeleteRequest deleteRequest) { case ASSOCIATION -> deleteAssociation(model, deleteRequest); case ENUM_ENTRY -> deleteEnumEntry(model, deleteRequest); case ONTOLOGY -> deleteOntology(model, deleteRequest); - case UNKNOWN -> throw new IllegalArgumentException("Unknown resource type for resource with UUID: " + deleteRequest.getUuid()); + case UNKNOWN -> + throw new IllegalArgumentException( + "Unknown resource type for resource with UUID: " + + deleteRequest.getUuid()); } } /** * Validates the given action against the supported actions for a resource type. * - * @return {@code true} if the action is {@link DeleteAction#KEEP} and should be skipped, - * {@code false} if the action is supported and deletion should proceed. - * - * @throws IllegalArgumentException if the action is {@code null} + * @return {@code true} if the action is {@link DeleteAction#KEEP} and should be skipped, {@code + * false} if the action is supported and deletion should proceed. + * @throws IllegalArgumentException if the action is {@code null} * @throws UnsupportedOperationException if the action is not in the supported set */ - private boolean shouldSkipOrThrow(DeleteAction action, CimResourceType type, DeleteAction... supported) { + private boolean shouldSkipOrThrow( + DeleteAction action, CimResourceType type, DeleteAction... supported) { if (action == null) { throw new IllegalArgumentException("Action must not be null"); } @@ -109,16 +121,19 @@ private boolean shouldSkipOrThrow(DeleteAction action, CimResourceType type, Del } if (!Set.of(supported).contains(action)) { throw new UnsupportedOperationException( - "Action " + action + " is not supported for " + type.name().toLowerCase().replace("_", " ") + "." - ); + "Action " + + action + + " is not supported for " + + type.name().toLowerCase().replace("_", " ") + + "."); } return false; } /** - * Removes a resource and all its transitively reachable blank nodes from the model. - * Nodes that are still referenced by other resources are not fully deleted; - * instead, only their {@code rdfa:uuid} triple is preserved to maintain referential integrity. + * Removes a resource and all its transitively reachable blank nodes from the model. Nodes that + * are still referenced by other resources are not fully deleted; instead, only their {@code + * rdfa:uuid} triple is preserved to maintain referential integrity. * * @param resource the root resource to delete */ @@ -129,11 +144,9 @@ private void removeResource(Resource resource) { while (!queue.isEmpty()) { var current = queue.poll(); - current.listProperties() - .toList() - .stream() - .filter(stmt -> stmt.getObject().isAnon()) - .forEach(stmt -> queue.add(stmt.getObject().asResource())); + current.listProperties().toList().stream() + .filter(stmt -> stmt.getObject().isAnon()) + .forEach(stmt -> queue.add(stmt.getObject().asResource())); // Delete inverse association var inverseStmt = current.getProperty(CIMS.inverseRoleName); @@ -157,29 +170,36 @@ private void removeResource(Resource resource) { private boolean isReferencedElsewhere(Model model, Resource resource) { return model.listStatements(null, null, resource) - .filterDrop(stmt -> stmt.getPredicate().equals(RDFA.uuid)) - .hasNext(); + .filterDrop(stmt -> stmt.getPredicate().equals(RDFA.uuid)) + .hasNext(); } private void deletePackage(Model model, ResourceDeleteRequest deleteRequest) { - if (shouldSkipOrThrow(deleteRequest.getAction(), CimResourceType.PACKAGE, DeleteAction.DELETE)) { + if (shouldSkipOrThrow( + deleteRequest.getAction(), CimResourceType.PACKAGE, DeleteAction.DELETE)) { return; } - var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); + var resource = + CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); removeResource(resource); } /** - * Deletes a class and all its owned resources (attributes, associations, and enum entries). - * If the action is {@link DeleteAction#REMOVE_SUBCLASS_REFERENCE}, only the {@code rdfs:subClassOf} - * triple is removed, leaving the class itself intact. + * Deletes a class and all its owned resources (attributes, associations, and enum entries). If + * the action is {@link DeleteAction#REMOVE_SUBCLASS_REFERENCE}, only the {@code + * rdfs:subClassOf} triple is removed, leaving the class itself intact. */ private void deleteClass(Model model, ResourceDeleteRequest deleteRequest) { - if (shouldSkipOrThrow(deleteRequest.getAction(), CimResourceType.CLASS, DeleteAction.DELETE, DeleteAction.REMOVE_SUBCLASS_REFERENCE, - DeleteAction.REMOVE_PACKAGE_REFERENCE)) { + if (shouldSkipOrThrow( + deleteRequest.getAction(), + CimResourceType.CLASS, + DeleteAction.DELETE, + DeleteAction.REMOVE_SUBCLASS_REFERENCE, + DeleteAction.REMOVE_PACKAGE_REFERENCE)) { return; } - var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); + var resource = + CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); if (deleteRequest.getAction() == DeleteAction.REMOVE_SUBCLASS_REFERENCE) { resource.listProperties(RDFS.subClassOf).forEach(model::remove); return; @@ -192,45 +212,51 @@ private void deleteClass(Model model, ResourceDeleteRequest deleteRequest) { // Delete attributes model.listSubjectsWithProperty(RDFS.domain, resource) - .filterKeep(CIMPropertyUtils::isAttribute) - .toList() - .forEach(this::removeResource); + .filterKeep(CIMPropertyUtils::isAttribute) + .toList() + .forEach(this::removeResource); - //delete associations only if it references an external resource + // delete associations only if it references an external resource model.listSubjectsWithProperty(RDFS.domain, resource) - .filterKeep(CIMPropertyUtils::isAssociation) - .filterKeep(assoc -> CIMResourceUtils.isExternalResource(assoc.getProperty(RDFS.range).getObject().asResource())) - .toList() - .forEach(this::removeResource); + .filterKeep(CIMPropertyUtils::isAssociation) + .filterKeep( + assoc -> + CIMResourceUtils.isExternalResource( + assoc.getProperty(RDFS.range).getObject().asResource())) + .toList() + .forEach(this::removeResource); // Delete enum entries model.listSubjectsWithProperty(RDF.type, resource) - .filterKeep(CIMResourceTypeIdentifyingUtils::isEnumEntry) - .toList() - .forEach(this::removeResource); + .filterKeep(CIMResourceTypeIdentifyingUtils::isEnumEntry) + .toList() + .forEach(this::removeResource); removeResource(resource); } private void deleteAttribute(Model model, ResourceDeleteRequest deleteRequest) { - if (shouldSkipOrThrow(deleteRequest.getAction(), CimResourceType.ATTRIBUTE, DeleteAction.DELETE)) { + if (shouldSkipOrThrow( + deleteRequest.getAction(), CimResourceType.ATTRIBUTE, DeleteAction.DELETE)) { return; } - var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); + var resource = + CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); removeResource(resource); } /** - * Deletes an association and its inverse. Since associations reference each other - * via {@code cims:inverseRoleName}, the mutual references are removed first to - * prevent {@link #removeResource} from preserving stale UUID triples due to - * the circular reference. + * Deletes an association and its inverse. Since associations reference each other via {@code + * cims:inverseRoleName}, the mutual references are removed first to prevent {@link + * #removeResource} from preserving stale UUID triples due to the circular reference. */ private void deleteAssociation(Model model, ResourceDeleteRequest deleteRequest) { - if (shouldSkipOrThrow(deleteRequest.getAction(), CimResourceType.ASSOCIATION, DeleteAction.DELETE)) { + if (shouldSkipOrThrow( + deleteRequest.getAction(), CimResourceType.ASSOCIATION, DeleteAction.DELETE)) { return; } - var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); + var resource = + CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); var inverseStmt = resource.getProperty(CIMS.inverseRoleName); if (inverseStmt != null && inverseStmt.getObject().isResource()) { @@ -245,18 +271,22 @@ private void deleteAssociation(Model model, ResourceDeleteRequest deleteRequest) } private void deleteEnumEntry(Model model, ResourceDeleteRequest deleteRequest) { - if (shouldSkipOrThrow(deleteRequest.getAction(), CimResourceType.ENUM_ENTRY, DeleteAction.DELETE)) { + if (shouldSkipOrThrow( + deleteRequest.getAction(), CimResourceType.ENUM_ENTRY, DeleteAction.DELETE)) { return; } - var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); + var resource = + CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); removeResource(resource); } private void deleteOntology(Model model, ResourceDeleteRequest deleteRequest) { - if (shouldSkipOrThrow(deleteRequest.getAction(), CimResourceType.ONTOLOGY, DeleteAction.DELETE)) { + if (shouldSkipOrThrow( + deleteRequest.getAction(), CimResourceType.ONTOLOGY, DeleteAction.DELETE)) { return; } - var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); + var resource = + CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); removeResource(resource); } -} \ No newline at end of file +} diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesUseCase.java b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesUseCase.java index 7d92d2dd..7bfaa6cd 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesUseCase.java @@ -26,8 +26,10 @@ public interface DeleteResourcesUseCase { /** * Executes a list of delete requests on a specified graph. + * * @param graphIdentifier The identifier of the graph where the change occurred. * @param deleteRequests The List of deleteRequests. */ - void executeDeleteRequests(GraphIdentifier graphIdentifier, List deleteRequests); + void executeDeleteRequests( + GraphIdentifier graphIdentifier, List deleteRequests); } diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java b/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java index 8509add7..284492f7 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java @@ -18,6 +18,7 @@ package org.rdfarchitect.services.delete; import lombok.RequiredArgsConstructor; + import org.apache.jena.graph.Graph; import org.apache.jena.query.TxnType; import org.apache.jena.rdf.model.Model; @@ -66,35 +67,72 @@ public AffectedResource getDeleteDependencies(GraphIdentifier graphIdentifier, U return switch (resourceType) { case PACKAGE -> findAffectedRelationsForPackage(model, uuid, reason, defaultActions); case CLASS -> findAffectedRelationsForClass(model, uuid, reason, defaultActions); - case ATTRIBUTE -> new AffectedResource(createResourceIdentifier(model, uuid), CimResourceType.ATTRIBUTE, reason) - .setActions(defaultActions); - case ASSOCIATION -> new AffectedResource(createResourceIdentifier(model, uuid), CimResourceType.ASSOCIATION, reason) - .setActions(defaultActions); - case ENUM_ENTRY -> new AffectedResource(createResourceIdentifier(model, uuid), CimResourceType.ENUM_ENTRY, reason) - .setActions(defaultActions); - case ONTOLOGY -> new AffectedResource(createResourceIdentifier(model, uuid), CimResourceType.ONTOLOGY, reason) - .setActions(defaultActions); - case UNKNOWN -> new AffectedResource(createResourceIdentifier(model, uuid), CimResourceType.UNKNOWN, reason) - .setActions(defaultActions); + case ATTRIBUTE -> + new AffectedResource( + createResourceIdentifier(model, uuid), + CimResourceType.ATTRIBUTE, + reason) + .setActions(defaultActions); + case ASSOCIATION -> + new AffectedResource( + createResourceIdentifier(model, uuid), + CimResourceType.ASSOCIATION, + reason) + .setActions(defaultActions); + case ENUM_ENTRY -> + new AffectedResource( + createResourceIdentifier(model, uuid), + CimResourceType.ENUM_ENTRY, + reason) + .setActions(defaultActions); + case ONTOLOGY -> + new AffectedResource( + createResourceIdentifier(model, uuid), + CimResourceType.ONTOLOGY, + reason) + .setActions(defaultActions); + case UNKNOWN -> + new AffectedResource( + createResourceIdentifier(model, uuid), + CimResourceType.UNKNOWN, + reason) + .setActions(defaultActions); }; } - private AffectedResource findAffectedRelationsForPackage(Model model, UUID uuid, AffectedResourceReason reason, List deleteActions) { + private AffectedResource findAffectedRelationsForPackage( + Model model, + UUID uuid, + AffectedResourceReason reason, + List deleteActions) { var classesInPackage = listClassesInPackage(model, uuid); var affectedResources = new ArrayList(); - var clsDeleteActions = List.of(DeleteAction.DELETE, DeleteAction.KEEP, DeleteAction.REMOVE_PACKAGE_REFERENCE); + var clsDeleteActions = + List.of( + DeleteAction.DELETE, + DeleteAction.KEEP, + DeleteAction.REMOVE_PACKAGE_REFERENCE); for (var cls : classesInPackage) { var clsUuid = CIMResourceUtils.findUuidForResource(cls); - var affectedClassResource = findAffectedRelationsForClass(model, clsUuid, AffectedResourceReason.CONTAINED_IN_PACKAGE, clsDeleteActions); + var affectedClassResource = + findAffectedRelationsForClass( + model, + clsUuid, + AffectedResourceReason.CONTAINED_IN_PACKAGE, + clsDeleteActions); affectedResources.add(affectedClassResource); } - return new AffectedResource(createResourceIdentifier(model, uuid), CimResourceType.PACKAGE, reason) - .setActions(deleteActions) - .setChildren(affectedResources); + return new AffectedResource( + createResourceIdentifier(model, uuid), CimResourceType.PACKAGE, reason) + .setActions(deleteActions) + .setChildren(affectedResources); } - private AffectedResource findAffectedRelationsForClass(Model model, UUID uuid, - AffectedResourceReason reason, List deleteActions) { + private AffectedResource findAffectedRelationsForClass( + Model model, + UUID uuid, + AffectedResourceReason reason, + List deleteActions) { var classResource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, uuid); var classResourceId = createResourceIdentifier(model, uuid); @@ -104,58 +142,78 @@ private AffectedResource findAffectedRelationsForClass(Model model, UUID uuid, affectedResources.addAll(findAffectedChildClassesForClass(classResource)); return new AffectedResource(classResourceId, CimResourceType.CLASS, reason) - .setActions(deleteActions) - .setChildren(affectedResources); + .setActions(deleteActions) + .setChildren(affectedResources); } private List findAffectedAttributesForClass(Resource classResource) { var childActions = List.of(DeleteAction.DELETE, DeleteAction.KEEP); return listAttributesWithClassAsDatatype(classResource).stream() - .map(attr -> new AffectedOwnedResource(createResourceIdentifier(attr), - CimResourceType.ATTRIBUTE, - AffectedResourceReason.USES_DELETED_CLASS_AS_DATATYPE, - createResourceIdentifier(attr.getProperty(RDFS.domain).getObject().asResource())) - .setActions(childActions)) - .toList(); + .map( + attr -> + new AffectedOwnedResource( + createResourceIdentifier(attr), + CimResourceType.ATTRIBUTE, + AffectedResourceReason + .USES_DELETED_CLASS_AS_DATATYPE, + createResourceIdentifier( + attr.getProperty(RDFS.domain) + .getObject() + .asResource())) + .setActions(childActions)) + .toList(); } - private List findAffectedAssociationsForClass(Resource classResource, ResourceIdentifier classResourceId) { + private List findAffectedAssociationsForClass( + Resource classResource, ResourceIdentifier classResourceId) { return listAssociationsReferencingClass(classResource).stream() - .map(assoc -> { - var childActions = new ArrayList(); - childActions.add(DeleteAction.DELETE); - if(!CIMResourceUtils.isExternalResource(assoc.getProperty(RDFS.range).getObject().asResource())){ - childActions.add(DeleteAction.KEEP); - } - return new AffectedAssociation(createResourceIdentifier(assoc), - CimResourceType.ASSOCIATION, - AffectedResourceReason.REFENCES_DELETED_CLASS_VIA_ASSOCIATION, - classResourceId, - getAssociationTarget(assoc) - ) - .setActions(childActions); - }) - .toList(); + .map( + assoc -> { + var childActions = new ArrayList(); + childActions.add(DeleteAction.DELETE); + if (!CIMResourceUtils.isExternalResource( + assoc.getProperty(RDFS.range).getObject().asResource())) { + childActions.add(DeleteAction.KEEP); + } + return new AffectedAssociation( + createResourceIdentifier(assoc), + CimResourceType.ASSOCIATION, + AffectedResourceReason + .REFENCES_DELETED_CLASS_VIA_ASSOCIATION, + classResourceId, + getAssociationTarget(assoc)) + .setActions(childActions); + }) + .toList(); } private ResourceIdentifier getAssociationTarget(Resource associationResource) { var rangeStatement = associationResource.getProperty(RDFS.range); if (rangeStatement == null) { - throw new IllegalStateException("Association " + associationResource + " does not have a range."); + throw new IllegalStateException( + "Association " + associationResource + " does not have a range."); } if (rangeStatement.getObject().isLiteral()) { - throw new IllegalStateException("Association " + associationResource + " has a literal as range, which is not supported."); + throw new IllegalStateException( + "Association " + + associationResource + + " has a literal as range, which is not supported."); } var rangeResource = rangeStatement.getObject().asResource(); return createResourceIdentifier(rangeResource); } private List findAffectedChildClassesForClass(Resource classResource) { - var childClassActions = List.of(DeleteAction.DELETE, DeleteAction.KEEP, DeleteAction.REMOVE_SUBCLASS_REFERENCE); + var childClassActions = + List.of( + DeleteAction.DELETE, + DeleteAction.KEEP, + DeleteAction.REMOVE_SUBCLASS_REFERENCE); return buildAffectedChildClassTree(classResource, childClassActions); } - private List buildAffectedChildClassTree(Resource classResource, List childClassActions) { + private List buildAffectedChildClassTree( + Resource classResource, List childClassActions) { var visited = new HashSet(); visited.add(classResource); @@ -172,14 +230,16 @@ private List buildAffectedChildClassTree(Resource classResourc var affectedResource = createAffectedChildClass(current, childClassActions); resourceMap.put(current, affectedResource); - attachToParentOrRoot(affectedResource, parent, classResource, resourceMap, rootChildren); + attachToParentOrRoot( + affectedResource, parent, classResource, resourceMap, rootChildren); enqueueChildren(current, visited, queue); } return rootChildren; } - private LinkedList> initializeQueue(Resource classResource, Set visited) { + private LinkedList> initializeQueue( + Resource classResource, Set visited) { var queue = new LinkedList>(); for (var directChild : listDirectlyDescendingClasses(classResource)) { if (visited.add(directChild)) { @@ -189,20 +249,25 @@ private LinkedList> initializeQueue(Resource class return queue; } - private AffectedResource createAffectedChildClass(Resource classResource, List actions) { + private AffectedResource createAffectedChildClass( + Resource classResource, List actions) { var resourceId = createResourceIdentifier(classResource); var children = new ArrayList(); children.addAll(findAffectedAttributesForClass(classResource)); children.addAll(findAffectedAssociationsForClass(classResource, resourceId)); - return new AffectedResource(resourceId, CimResourceType.CLASS, AffectedResourceReason.CHILD_OF) - .setActions(actions) - .setChildren(children); + return new AffectedResource( + resourceId, CimResourceType.CLASS, AffectedResourceReason.CHILD_OF) + .setActions(actions) + .setChildren(children); } - private void attachToParentOrRoot(AffectedResource affectedResource, Resource parent, - Resource rootClass, Map resourceMap, - List rootChildren) { + private void attachToParentOrRoot( + AffectedResource affectedResource, + Resource parent, + Resource rootClass, + Map resourceMap, + List rootChildren) { if (parent.equals(rootClass)) { rootChildren.add(affectedResource); return; @@ -218,8 +283,10 @@ private void attachToParentOrRoot(AffectedResource affectedResource, Resource pa } } - private void enqueueChildren(Resource current, Set visited, - LinkedList> queue) { + private void enqueueChildren( + Resource current, + Set visited, + LinkedList> queue) { for (var child : listDirectlyDescendingClasses(current)) { if (visited.add(child)) { queue.add(Map.entry(child, current)); @@ -228,7 +295,10 @@ private void enqueueChildren(Resource current, Set visited, } private List listDirectlyDescendingClasses(Resource classResource) { - return classResource.getModel().listSubjectsWithProperty(RDFS.subClassOf, classResource).toList(); + return classResource + .getModel() + .listSubjectsWithProperty(RDFS.subClassOf, classResource) + .toList(); } private List listClassesInPackage(Model model, UUID uuid) { @@ -237,23 +307,29 @@ private List listClassesInPackage(Model model, UUID uuid) { throw new IllegalStateException("Resource with UUID " + uuid + " is not a package."); } return model.listSubjectsWithProperty(CIMS.belongsToCategory, packageResource) - .filterKeep(cls -> cls.hasProperty(RDF.type, RDFS.Class)) - .toList(); + .filterKeep(cls -> cls.hasProperty(RDF.type, RDFS.Class)) + .toList(); } private List listAssociationsReferencingClass(Resource classResource) { - return classResource.getModel().listSubjectsWithProperty(RDFS.domain, classResource) - .filterKeep(CIMPropertyUtils::isAssociation) - .toList(); + return classResource + .getModel() + .listSubjectsWithProperty(RDFS.domain, classResource) + .filterKeep(CIMPropertyUtils::isAssociation) + .toList(); } private List listAttributesWithClassAsDatatype(Resource classResource) { var model = classResource.getModel(); var byDatatype = model.listSubjectsWithProperty(CIMS.datatype, classResource); var byRange = model.listSubjectsWithProperty(RDFS.range, classResource); - return byDatatype.andThen(byRange) - .filterKeep(CIMPropertyUtils::isAttribute) - .toList().stream().distinct().toList(); + return byDatatype + .andThen(byRange) + .filterKeep(CIMPropertyUtils::isAttribute) + .toList() + .stream() + .distinct() + .toList(); } private ResourceIdentifier createResourceIdentifier(Model model, UUID uuid) { @@ -267,9 +343,10 @@ private ResourceIdentifier createResourceIdentifier(Resource resource) { if (resource.hasProperty(RDFS.label)) { label = resource.getProperty(RDFS.label).getString(); } - return new ResourceIdentifier().setUuid(uuid) - .setLabel(label) - .setNamespace(resource.getNameSpace()); + return new ResourceIdentifier() + .setUuid(uuid) + .setLabel(label) + .setNamespace(resource.getNameSpace()); } private Graph getCopyOfDatabaseGraph(GraphIdentifier graphIdentifier) { @@ -284,4 +361,4 @@ private Graph getCopyOfDatabaseGraph(GraphIdentifier graphIdentifier) { } } } -} \ No newline at end of file +} diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesUseCase.java b/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesUseCase.java index e16b5b61..5b393b33 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesUseCase.java @@ -26,9 +26,11 @@ public interface FindDeleteDependenciesUseCase { /** * Finds the resources that would be affected by deleting a specified resource. + * * @param graphIdentifier The identifier of the graph. * @param uuid The resource to find dependencies for. - * @return An {@link AffectedResource} containing the affected resources with and their relations. + * @return An {@link AffectedResource} containing the affected resources with and their + * relations. */ AffectedResource getDeleteDependencies(GraphIdentifier graphIdentifier, UUID uuid); } diff --git a/backend/src/main/java/org/rdfarchitect/services/select/GetClassListUseCase.java b/backend/src/main/java/org/rdfarchitect/services/select/GetClassListUseCase.java index 81597c78..ebb6d421 100644 --- a/backend/src/main/java/org/rdfarchitect/services/select/GetClassListUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/select/GetClassListUseCase.java @@ -29,8 +29,8 @@ public interface GetClassListUseCase { * * @param graphIdentifier The graph to getClassDefinition. * @param includeExternalClasses Whether to include external classes in the result. - * * @return The list of classes in the graph. */ - List getClassList(GraphIdentifier graphIdentifier,boolean includeExternalClasses); + List getClassList( + GraphIdentifier graphIdentifier, boolean includeExternalClasses); } diff --git a/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java b/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java index a0ac1c8c..37e63100 100644 --- a/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java +++ b/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java @@ -17,10 +17,13 @@ package org.rdfarchitect.services.select; +import static org.rdfarchitect.models.cim.queries.select.CIMQueryBuilder.Mode.*; +import static org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs.*; + import lombok.RequiredArgsConstructor; + import org.apache.jena.arq.querybuilder.SelectBuilder; import org.apache.jena.graph.Node; -import org.apache.jena.query.Query; import org.apache.jena.query.QueryFactory; import org.apache.jena.query.TxnType; import org.apache.jena.rdf.model.ModelFactory; @@ -62,13 +65,17 @@ import java.util.UUID; import java.util.stream.Collectors; -import static org.rdfarchitect.models.cim.queries.select.CIMQueryBuilder.Mode.*; -import static org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs.*; - @Service @RequiredArgsConstructor -public class QueryGraphService implements GetClassListUseCase, ListDatatypesUseCase, GetSchemaUseCase, ListInternalPackagesUseCase, ListExternalPackagesUseCase, - ListPrimitivesUseCase, ListStereotypesUseCase, ResolveIdentifierUseCase { +public class QueryGraphService + implements GetClassListUseCase, + ListDatatypesUseCase, + GetSchemaUseCase, + ListInternalPackagesUseCase, + ListExternalPackagesUseCase, + ListPrimitivesUseCase, + ListStereotypesUseCase, + ResolveIdentifierUseCase { private static final String BLANK_PACKAGE_NAME = "default"; private static final String BLANK_PACKAGE_LANG = "en"; @@ -78,9 +85,11 @@ public class QueryGraphService implements GetClassListUseCase, ListDatatypesUseC private final PackageMapper packageMapper; @Override - public List getClassList(GraphIdentifier graphIdentifier, boolean includeExternalClasses) { - var classFilter = includeExternalClasses - ? """ + public List getClassList( + GraphIdentifier graphIdentifier, boolean includeExternalClasses) { + var classFilter = + includeExternalClasses + ? """ { ?uri rdf:type rdfs:Class . } @@ -88,9 +97,11 @@ public List getClassList(GraphIdentifier graphIdentifier, bo { ?_any rdfs:domain ?uri . } - """ : "?uri rdf:type rdfs:Class ."; + """ + : "?uri rdf:type rdfs:Class ."; - var query = """ + var query = + """ PREFIX cims: PREFIX rdf: PREFIX owl: @@ -123,17 +134,21 @@ public List getClassList(GraphIdentifier graphIdentifier, bo } } ORDER BY ?uri - """.formatted(classFilter); + """ + .formatted(classFilter); - //execute query - var queryResultSet = InMemorySparqlExecutor.executeSingleQuery(databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), QueryFactory.create(query), null); + // execute query + var queryResultSet = + InMemorySparqlExecutor.executeSingleQuery( + databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), + QueryFactory.create(query), + null); - //format results + // format results var cimClassList = CIMUMLObjectFactory.createCIMClassUMLAdaptedList(queryResultSet); var referencedClassList = getReferencedClassList(graphIdentifier); - var existingUuids = cimClassList.stream() - .map(CIMClassUMLAdapted::getUuid) - .collect(Collectors.toSet()); + var existingUuids = + cimClassList.stream().map(CIMClassUMLAdapted::getUuid).collect(Collectors.toSet()); for (var referencedClass : referencedClassList) { if (!existingUuids.contains(referencedClass.getUuid())) { @@ -145,47 +160,57 @@ public List getClassList(GraphIdentifier graphIdentifier, bo return classMapper.toDTOList(cimClassList); } - private List getReferencedClassList(GraphIdentifier graphIdentifier) { - var query = new SelectBuilder() - .setDistinct(true) - .addVar("?uri") - .addVar("?uuid") - .addUnion(new SelectBuilder() - .addWhere("?subject", RDFS.domain, "?uri")) - .addUnion(new SelectBuilder() - .addWhere("?subject", CIMS.datatype, "?uri")) - .addOptional("?uri", RDFA.uuid, "?uuid") - .build(); - - //execute query - var queryResultSet = InMemorySparqlExecutor.executeSingleQuery(databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), query, graphIdentifier.getGraphUri()); - - //format results + private List getReferencedClassList(GraphIdentifier graphIdentifier) { + var query = + new SelectBuilder() + .setDistinct(true) + .addVar("?uri") + .addVar("?uuid") + .addUnion(new SelectBuilder().addWhere("?subject", RDFS.domain, "?uri")) + .addUnion(new SelectBuilder().addWhere("?subject", CIMS.datatype, "?uri")) + .addOptional("?uri", RDFA.uuid, "?uuid") + .build(); + + // execute query + var queryResultSet = + InMemorySparqlExecutor.executeSingleQuery( + databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), + query, + graphIdentifier.getGraphUri()); + + // format results return CIMUMLObjectFactory.createCIMClassUMLAdaptedList(queryResultSet); } @Override public List listDatatypes(GraphIdentifier graphIdentifier) { - //build query - var baseQuery = new CIMBaseQueryBuilder() - .addPrefixes(databasePort.getPrefixMapping(graphIdentifier.getDatasetName())) - .setGraph(graphIdentifier.getGraphUri()) - .setOrder() - .setDistinct() - .setType(RDFS.Class) - .filterStereotypes(CIMStereotypes.enumeration.getURI(), "Entsoe") - .build(); - var query = new CIMQueryBuilder(baseQuery) - .appendLabelQuery(OPTIONAL) - .appendPackageQuery(OPTIONAL) - .appendCommentQuery(OPTIONAL) - .appendSuperClassQuery(OPTIONAL) - .build(); - - //execute query - var queryResultSet = InMemorySparqlExecutor.executeSingleQuery(databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), query, graphIdentifier.getGraphUri()); - - //format results + // build query + var baseQuery = + new CIMBaseQueryBuilder() + .addPrefixes( + databasePort.getPrefixMapping(graphIdentifier.getDatasetName())) + .setGraph(graphIdentifier.getGraphUri()) + .setOrder() + .setDistinct() + .setType(RDFS.Class) + .filterStereotypes(CIMStereotypes.enumeration.getURI(), "Entsoe") + .build(); + var query = + new CIMQueryBuilder(baseQuery) + .appendLabelQuery(OPTIONAL) + .appendPackageQuery(OPTIONAL) + .appendCommentQuery(OPTIONAL) + .appendSuperClassQuery(OPTIONAL) + .build(); + + // execute query + var queryResultSet = + InMemorySparqlExecutor.executeSingleQuery( + databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), + query, + graphIdentifier.getGraphUri()); + + // format results var cimClassList = CIMUMLObjectFactory.createCIMClassUMLAdaptedList(queryResultSet); cimClassList.forEach(CIMClassUMLAdapted::nullEmptyLists); @@ -199,7 +224,9 @@ public ByteArrayOutputStream getSchema(GraphIdentifier graphIdentifier, RDFForma graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); graph.begin(TxnType.READ); var copiedGraph = GraphUtils.deepCopy(graph); - copiedGraph.getPrefixMapping().setNsPrefixes(databasePort.getPrefixMapping(graphIdentifier.getDatasetName())); + copiedGraph + .getPrefixMapping() + .setNsPrefixes(databasePort.getPrefixMapping(graphIdentifier.getDatasetName())); removeUUIDs(copiedGraph); var sortedModel = new CimSortedModel(ModelFactory.createModelForGraph(copiedGraph)); sortedModel.write(out, format.getLang().getName()); @@ -215,35 +242,41 @@ public ByteArrayOutputStream getSchema(GraphIdentifier graphIdentifier, RDFForma @Override public List listInternalPackages(GraphIdentifier graphIdentifier) { - //build package query - var internalPackageBaseQuery = new CIMBaseQueryBuilder() - .setDistinct() - .addPrefixes(databasePort.getPrefixMapping(graphIdentifier.getDatasetName())) - .setGraph(graphIdentifier.getGraphUri()) - .setType(CIMS.classCategory) - .build(); - - var internalPackageQuery = new CIMQueryBuilder(internalPackageBaseQuery) - .appendUUIDQuery(REQUIRED) - .appendLabelQuery(REQUIRED) - .appendPackageQuery(OPTIONAL) - .appendCommentQuery(OPTIONAL) - .build(); - - //execute package query + // build package query + var internalPackageBaseQuery = + new CIMBaseQueryBuilder() + .setDistinct() + .addPrefixes( + databasePort.getPrefixMapping(graphIdentifier.getDatasetName())) + .setGraph(graphIdentifier.getGraphUri()) + .setType(CIMS.classCategory) + .build(); + + var internalPackageQuery = + new CIMQueryBuilder(internalPackageBaseQuery) + .appendUUIDQuery(REQUIRED) + .appendLabelQuery(REQUIRED) + .appendPackageQuery(OPTIONAL) + .appendCommentQuery(OPTIONAL) + .build(); + + // execute package query var internalPackageQueryResultSet = - InMemorySparqlExecutor.executeSingleQuery(databasePort.getGraphWithContext(graphIdentifier) - .getRdfGraph(), internalPackageQuery, graphIdentifier.getGraphUri()); + InMemorySparqlExecutor.executeSingleQuery( + databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), + internalPackageQuery, + graphIdentifier.getGraphUri()); - //format results + // format results var cimPackageList = CIMObjectFactory.createCIMPackageList(internalPackageQueryResultSet); - //add blank package + // add blank package URI uri = new URI(BLANK_PACKAGE_NAME); - var blankPackage = CIMPackage.builder() - .uri(uri) - .label(new RDFSLabel(BLANK_PACKAGE_NAME, BLANK_PACKAGE_LANG)) - .build(); + var blankPackage = + CIMPackage.builder() + .uri(uri) + .label(new RDFSLabel(BLANK_PACKAGE_NAME, BLANK_PACKAGE_LANG)) + .build(); cimPackageList.add(blankPackage); return packageMapper.toDTOList(cimPackageList); @@ -251,50 +284,61 @@ public List listInternalPackages(GraphIdentifier graphIdentifier) { @Override public List listExternalPackages(GraphIdentifier graphIdentifier) { - //build external package query - var externalPackageBaseQuery = new CIMBaseQueryBuilder() - .setDistinct() - .addPrefixes(databasePort.getPrefixMapping(graphIdentifier.getDatasetName())) - .setGraph(graphIdentifier.getGraphUri()) - .addWhereThisNotExists(RDF.type.getURI(), CIMS.classCategory.getURI()) - .build() - .addWhere(Node.ANY, CIMS.belongsToCategory.asNode(), CIMQueryVars.URI); - - var externalPackageQuery = new CIMQueryBuilder(externalPackageBaseQuery) - .appendUUIDQuery(REQUIRED) - .build(); - - //execute external package query + // build external package query + var externalPackageBaseQuery = + new CIMBaseQueryBuilder() + .setDistinct() + .addPrefixes( + databasePort.getPrefixMapping(graphIdentifier.getDatasetName())) + .setGraph(graphIdentifier.getGraphUri()) + .addWhereThisNotExists(RDF.type.getURI(), CIMS.classCategory.getURI()) + .build() + .addWhere(Node.ANY, CIMS.belongsToCategory.asNode(), CIMQueryVars.URI); + + var externalPackageQuery = + new CIMQueryBuilder(externalPackageBaseQuery).appendUUIDQuery(REQUIRED).build(); + + // execute external package query var externalPackageQueryResultSet = - InMemorySparqlExecutor.executeSingleQuery(databasePort.getGraphWithContext(graphIdentifier) - .getRdfGraph(), externalPackageQuery, graphIdentifier.getGraphUri()); + InMemorySparqlExecutor.executeSingleQuery( + databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), + externalPackageQuery, + graphIdentifier.getGraphUri()); - var cimExternalPackageList = CIMObjectFactory.createExternalCIMPackageList(externalPackageQueryResultSet); + var cimExternalPackageList = + CIMObjectFactory.createExternalCIMPackageList(externalPackageQueryResultSet); return packageMapper.toDTOList(cimExternalPackageList); } @Override public List listPrimitives(GraphIdentifier graphIdentifier) { - var baseQuery = new CIMBaseQueryBuilder() - .setOrder() - .setDistinct() - .addPrefixes(databasePort.getPrefixMapping(graphIdentifier.getGraphUri())) - .filterStereotypes(CIMStereotypes.primitiveString, CIMStereotypes.cimDatatypeString) - .setGraph(graphIdentifier.getGraphUri()) - .setType(RDFS.Class) - .build(); - var query = new CIMQueryBuilder(baseQuery) - .appendLabelQuery(REQUIRED) - .appendPackageQuery(OPTIONAL) - .appendCommentQuery(OPTIONAL) - .appendSuperClassQuery(OPTIONAL) - .build(); - - //execute query - var queryResultSet = InMemorySparqlExecutor.executeSingleQuery(databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), query, graphIdentifier.getGraphUri()); - - //format results + var baseQuery = + new CIMBaseQueryBuilder() + .setOrder() + .setDistinct() + .addPrefixes(databasePort.getPrefixMapping(graphIdentifier.getGraphUri())) + .filterStereotypes( + CIMStereotypes.primitiveString, CIMStereotypes.cimDatatypeString) + .setGraph(graphIdentifier.getGraphUri()) + .setType(RDFS.Class) + .build(); + var query = + new CIMQueryBuilder(baseQuery) + .appendLabelQuery(REQUIRED) + .appendPackageQuery(OPTIONAL) + .appendCommentQuery(OPTIONAL) + .appendSuperClassQuery(OPTIONAL) + .build(); + + // execute query + var queryResultSet = + InMemorySparqlExecutor.executeSingleQuery( + databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), + query, + graphIdentifier.getGraphUri()); + + // format results var cimClassList = CIMUMLObjectFactory.createCIMClassUMLAdaptedList(queryResultSet); cimClassList.forEach(CIMClassUMLAdapted::nullEmptyLists); @@ -303,22 +347,25 @@ public List listPrimitives(GraphIdentifier graphIdentifier) @Override public List listStereotypes(GraphIdentifier graphIdentifier) { - var baseQuery = new CIMBaseQueryBuilder() - .setOrder() - .setDistinct() - .addPrefixes(databasePort.getPrefixMapping(graphIdentifier.getDatasetName())) - .setGraph(graphIdentifier.getGraphUri()) - .buildWithoutUriVar(); - - var query = new CIMQueryBuilder(baseQuery) - .appendStereotypeQuery(REQUIRED) - .build(); - - - //execute query - var queryResult = InMemorySparqlExecutor.executeSingleQuery(databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), query, graphIdentifier.getGraphUri()); - - //format results + var baseQuery = + new CIMBaseQueryBuilder() + .setOrder() + .setDistinct() + .addPrefixes( + databasePort.getPrefixMapping(graphIdentifier.getDatasetName())) + .setGraph(graphIdentifier.getGraphUri()) + .buildWithoutUriVar(); + + var query = new CIMQueryBuilder(baseQuery).appendStereotypeQuery(REQUIRED).build(); + + // execute query + var queryResult = + InMemorySparqlExecutor.executeSingleQuery( + databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), + query, + graphIdentifier.getGraphUri()); + + // format results List resultList = new ArrayList<>(); while (queryResult.hasNext()) { var parser = new CIMQuerySolutionParser(queryResult.next()); diff --git a/backend/src/test/java/org/rdfarchitect/services/delete/DeleteResourcesServiceTest.java b/backend/src/test/java/org/rdfarchitect/services/delete/DeleteResourcesServiceTest.java index d8e5c165..c2e7af86 100644 --- a/backend/src/test/java/org/rdfarchitect/services/delete/DeleteResourcesServiceTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/delete/DeleteResourcesServiceTest.java @@ -17,6 +17,10 @@ package org.rdfarchitect.services.delete; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + import org.apache.jena.query.TxnType; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; @@ -49,37 +53,43 @@ import java.util.List; import java.util.UUID; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) class DeleteResourcesServiceTest { - private static final String TEST_DATA_PATH = "src/test/java/org/rdfarchitect/services/delete/testdata.ttl"; + private static final String TEST_DATA_PATH = + "src/test/java/org/rdfarchitect/services/delete/testdata.ttl"; - private static final GraphIdentifier GRAPH_IDENTIFIER = new GraphIdentifier("default", "default"); + private static final GraphIdentifier GRAPH_IDENTIFIER = + new GraphIdentifier("default", "default"); private static final String CIM_NS = "http://iec.ch/TC57/CIM100#"; // UUIDs from the TTL test data - private static final UUID PARENT_CLASS_UUID = UUID.fromString("05131eaf-a7dd-4ac4-8624-9665990985ab"); - private static final UUID CHILD_CLASS_UUID = UUID.fromString("93ee2f31-5ddd-4b25-b119-e90a5ed327b0"); - private static final UUID ASSOCIATED_CLASS_UUID = UUID.fromString("f6d92056-c469-40d4-add1-1d6adf2fa7a6"); - private static final UUID PACKAGE_UUID = UUID.fromString("0351f9b6-5e91-4059-9d8b-169d28f1b2c8"); - private static final UUID DATATYPE_CLASS_UUID = UUID.fromString("db520255-95ef-40d4-b328-e1631e4683a4"); - private static final UUID ONTOLOGY_UUID = UUID.fromString("dba8f8e3-bfb3-4e62-9ca5-b0136ed186b2"); + private static final UUID PARENT_CLASS_UUID = + UUID.fromString("05131eaf-a7dd-4ac4-8624-9665990985ab"); + private static final UUID CHILD_CLASS_UUID = + UUID.fromString("93ee2f31-5ddd-4b25-b119-e90a5ed327b0"); + private static final UUID ASSOCIATED_CLASS_UUID = + UUID.fromString("f6d92056-c469-40d4-add1-1d6adf2fa7a6"); + private static final UUID PACKAGE_UUID = + UUID.fromString("0351f9b6-5e91-4059-9d8b-169d28f1b2c8"); + private static final UUID DATATYPE_CLASS_UUID = + UUID.fromString("db520255-95ef-40d4-b328-e1631e4683a4"); + private static final UUID ONTOLOGY_UUID = + UUID.fromString("dba8f8e3-bfb3-4e62-9ca5-b0136ed186b2"); private static final UUID ATTR1_UUID = UUID.fromString("dc934c7b-6c4d-4177-9832-bcb955f25414"); - private static final UUID ASSOC_NONEXISTING_UUID = UUID.fromString("b2a306f1-4789-4a12-b045-04a014e7a937"); - private static final UUID ASSOC_TEMP_ASSOCIATED_UUID = UUID.fromString("c3b417f2-5890-5b23-c156-15b015e8b048"); - private static final UUID ASSOC_ASSOCIATED_CHILD_UUID = UUID.fromString("abfab117-e4bc-4814-9b5a-3aed72de8a2d"); - private static final UUID ASSOC_CHILD_ASSOCIATED_UUID = UUID.fromString("bc0bc228-f5cd-5925-ab6b-4bfe83ef9b3e"); + private static final UUID ASSOC_NONEXISTING_UUID = + UUID.fromString("b2a306f1-4789-4a12-b045-04a014e7a937"); + private static final UUID ASSOC_TEMP_ASSOCIATED_UUID = + UUID.fromString("c3b417f2-5890-5b23-c156-15b015e8b048"); + private static final UUID ASSOC_ASSOCIATED_CHILD_UUID = + UUID.fromString("abfab117-e4bc-4814-9b5a-3aed72de8a2d"); + private static final UUID ASSOC_CHILD_ASSOCIATED_UUID = + UUID.fromString("bc0bc228-f5cd-5925-ab6b-4bfe83ef9b3e"); - @Mock - private DatabasePort databasePort; + @Mock private DatabasePort databasePort; - @InjectMocks - private DeleteResourcesService service; + @InjectMocks private DeleteResourcesService service; private GraphRewindableWithUUIDs wrappedGraph; @@ -93,7 +103,8 @@ void setUp() throws IOException { wrappedGraph = new GraphRewindableWithUUIDs(graph, 5, 5); var wrappedContext = new GraphWithContext(wrappedGraph); - when(databasePort.getGraphWithContext(any(GraphIdentifier.class))).thenReturn(wrappedContext); + when(databasePort.getGraphWithContext(any(GraphIdentifier.class))) + .thenReturn(wrappedContext); } private Model readModel() { @@ -119,25 +130,25 @@ private void assertResourceRemovedOrOnlyUuid(String localName) { var model = readModel(); try { var resource = ResourceFactory.createResource(CIM_NS + localName); - var nonUuidStatements = model.listStatements(resource, null, (RDFNode) null) - .filterDrop(stmt -> stmt.getPredicate().equals(RDFA.uuid)) - .toList(); + var nonUuidStatements = + model.listStatements(resource, null, (RDFNode) null) + .filterDrop(stmt -> stmt.getPredicate().equals(RDFA.uuid)) + .toList(); assertThat(nonUuidStatements).isEmpty(); } finally { endRead(); } } - /** - * Asserts that the resource still has properties beyond just UUID. - */ + /** Asserts that the resource still has properties beyond just UUID. */ private void assertResourceFullyPresent(String localName) { var model = readModel(); try { var resource = ResourceFactory.createResource(CIM_NS + localName); - var nonUuidStatements = model.listStatements(resource, null, (RDFNode) null) - .filterDrop(stmt -> stmt.getPredicate().equals(RDFA.uuid)) - .toList(); + var nonUuidStatements = + model.listStatements(resource, null, (RDFNode) null) + .filterDrop(stmt -> stmt.getPredicate().equals(RDFA.uuid)) + .toList(); assertThat(nonUuidStatements).isNotEmpty(); } finally { endRead(); @@ -148,7 +159,8 @@ private void assertResourceFullyPresent(String localName) { @Test void executeDeleteRequests_deleteOntology_removesOntology() { - service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(ONTOLOGY_UUID, DeleteAction.DELETE))); + service.executeDeleteRequests( + GRAPH_IDENTIFIER, List.of(request(ONTOLOGY_UUID, DeleteAction.DELETE))); assertResourceRemovedOrOnlyUuid("Ontology"); } @@ -158,14 +170,16 @@ void executeDeleteRequests_deleteOntology_removesOntology() { @Test void executeDeleteRequests_deletePackage_removesPackagePropertiesButPreservesUuid() { // Package is still referenced by classes via belongsToCategory, so UUID is preserved - service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(PACKAGE_UUID, DeleteAction.DELETE))); + service.executeDeleteRequests( + GRAPH_IDENTIFIER, List.of(request(PACKAGE_UUID, DeleteAction.DELETE))); assertResourceRemovedOrOnlyUuid("Package_Package"); } @Test void executeDeleteRequests_keepPackage_doesNotRemovePackage() { - service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(PACKAGE_UUID, DeleteAction.KEEP))); + service.executeDeleteRequests( + GRAPH_IDENTIFIER, List.of(request(PACKAGE_UUID, DeleteAction.KEEP))); assertResourceFullyPresent("Package_Package"); } @@ -174,7 +188,8 @@ void executeDeleteRequests_keepPackage_doesNotRemovePackage() { @Test void executeDeleteRequests_deleteClass_removesClassAndOwnedAttributes() { - service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(PARENT_CLASS_UUID, DeleteAction.DELETE))); + service.executeDeleteRequests( + GRAPH_IDENTIFIER, List.of(request(PARENT_CLASS_UUID, DeleteAction.DELETE))); // ParentClass is referenced via subClassOf, so UUID may be preserved assertResourceRemovedOrOnlyUuid("ParentClass"); @@ -185,27 +200,36 @@ void executeDeleteRequests_deleteClass_removesClassAndOwnedAttributes() { @Test void executeDeleteRequests_deleteClass_removesClassProperties() { // ChildClass is referenced by AssociatedClass via subClassOf - service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(CHILD_CLASS_UUID, DeleteAction.DELETE))); + service.executeDeleteRequests( + GRAPH_IDENTIFIER, List.of(request(CHILD_CLASS_UUID, DeleteAction.DELETE))); assertResourceRemovedOrOnlyUuid("ChildClass"); } @Test void executeDeleteRequests_keepClass_doesNotRemoveClass() { - service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(PARENT_CLASS_UUID, DeleteAction.KEEP))); + service.executeDeleteRequests( + GRAPH_IDENTIFIER, List.of(request(PARENT_CLASS_UUID, DeleteAction.KEEP))); assertResourceFullyPresent("ParentClass"); } @Test void executeDeleteRequests_removeSubclassReference_removesOnlySubClassOfTriple() { - service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(CHILD_CLASS_UUID, DeleteAction.REMOVE_SUBCLASS_REFERENCE))); + service.executeDeleteRequests( + GRAPH_IDENTIFIER, + List.of(request(CHILD_CLASS_UUID, DeleteAction.REMOVE_SUBCLASS_REFERENCE))); var model = readModel(); try { var childClassResource = ResourceFactory.createResource(CIM_NS + "ChildClass"); - assertThat(model.listStatements(childClassResource, RDFS.subClassOf, (RDFNode) null).hasNext()).isFalse(); - assertThat(childClassResource.inModel(model).hasProperty(RDF.type, RDFS.Class)).isTrue(); + assertThat( + model.listStatements( + childClassResource, RDFS.subClassOf, (RDFNode) null) + .hasNext()) + .isFalse(); + assertThat(childClassResource.inModel(model).hasProperty(RDF.type, RDFS.Class)) + .isTrue(); assertThat(childClassResource.inModel(model).hasProperty(RDFS.label)).isTrue(); } finally { endRead(); @@ -214,13 +238,22 @@ void executeDeleteRequests_removeSubclassReference_removesOnlySubClassOfTriple() @Test void executeDeleteRequests_removePackageReference_removesOnlyBelongsToCategoryTriple() { - service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(CHILD_CLASS_UUID, DeleteAction.REMOVE_PACKAGE_REFERENCE))); + service.executeDeleteRequests( + GRAPH_IDENTIFIER, + List.of(request(CHILD_CLASS_UUID, DeleteAction.REMOVE_PACKAGE_REFERENCE))); var model = readModel(); try { var childClassResource = ResourceFactory.createResource(CIM_NS + "ChildClass"); - assertThat(model.listStatements(childClassResource, CIMS.belongsToCategory, (RDFNode) null).hasNext()).isFalse(); - assertThat(childClassResource.inModel(model).hasProperty(RDF.type, RDFS.Class)).isTrue(); + assertThat( + model.listStatements( + childClassResource, + CIMS.belongsToCategory, + (RDFNode) null) + .hasNext()) + .isFalse(); + assertThat(childClassResource.inModel(model).hasProperty(RDF.type, RDFS.Class)) + .isTrue(); assertThat(childClassResource.inModel(model).hasProperty(RDFS.label)).isTrue(); } finally { endRead(); @@ -231,14 +264,16 @@ void executeDeleteRequests_removePackageReference_removesOnlyBelongsToCategoryTr @Test void executeDeleteRequests_deleteAttribute_removesAttribute() { - service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(ATTR1_UUID, DeleteAction.DELETE))); + service.executeDeleteRequests( + GRAPH_IDENTIFIER, List.of(request(ATTR1_UUID, DeleteAction.DELETE))); assertResourceRemovedOrOnlyUuid("ParentClass.attr1"); } @Test void executeDeleteRequests_keepAttribute_doesNotRemoveAttribute() { - service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(ATTR1_UUID, DeleteAction.KEEP))); + service.executeDeleteRequests( + GRAPH_IDENTIFIER, List.of(request(ATTR1_UUID, DeleteAction.KEEP))); assertResourceFullyPresent("ParentClass.attr1"); } @@ -247,7 +282,8 @@ void executeDeleteRequests_keepAttribute_doesNotRemoveAttribute() { @Test void executeDeleteRequests_deleteAssociation_removesAssociationAndInverse() { - service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(ASSOC_NONEXISTING_UUID, DeleteAction.DELETE))); + service.executeDeleteRequests( + GRAPH_IDENTIFIER, List.of(request(ASSOC_NONEXISTING_UUID, DeleteAction.DELETE))); // Both the association and its inverse should have their properties removed assertResourceRemovedOrOnlyUuid("AssociatedClass.NonExisting"); @@ -256,14 +292,17 @@ void executeDeleteRequests_deleteAssociation_removesAssociationAndInverse() { @Test void executeDeleteRequests_keepAssociation_doesNotRemoveAssociation() { - service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(ASSOC_NONEXISTING_UUID, DeleteAction.KEEP))); + service.executeDeleteRequests( + GRAPH_IDENTIFIER, List.of(request(ASSOC_NONEXISTING_UUID, DeleteAction.KEEP))); assertResourceFullyPresent("AssociatedClass.NonExisting"); } @Test void executeDeleteRequests_deleteAssociationOtherDirection_removesAssociationAndInverse() { - service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(ASSOC_ASSOCIATED_CHILD_UUID, DeleteAction.DELETE))); + service.executeDeleteRequests( + GRAPH_IDENTIFIER, + List.of(request(ASSOC_ASSOCIATED_CHILD_UUID, DeleteAction.DELETE))); assertResourceRemovedOrOnlyUuid("AssociatedClass.ChildClass"); assertResourceRemovedOrOnlyUuid("ChildClass.AssociatedClass"); @@ -275,7 +314,8 @@ void executeDeleteRequests_deleteAssociationOtherDirection_removesAssociationAnd void executeDeleteRequests_deleteClassWithExternalAssociation_removesExternalAssociations() { // AssociatedClass has an association to Temp (external, not in any package) // Deleting AssociatedClass should remove associations referencing external resources - service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(ASSOCIATED_CLASS_UUID, DeleteAction.DELETE))); + service.executeDeleteRequests( + GRAPH_IDENTIFIER, List.of(request(ASSOCIATED_CLASS_UUID, DeleteAction.DELETE))); assertResourceRemovedOrOnlyUuid("AssociatedClass"); assertResourceRemovedOrOnlyUuid("AssociatedClass.NonExisting"); @@ -285,7 +325,8 @@ void executeDeleteRequests_deleteClassWithExternalAssociation_removesExternalAss void executeDeleteRequests_deleteClass_doesNotRemoveInternalAssociations() { // AssociatedClass.ChildClass points to ChildClass (internal, in same package) // Deleting AssociatedClass should NOT remove internal associations - service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(ASSOCIATED_CLASS_UUID, DeleteAction.DELETE))); + service.executeDeleteRequests( + GRAPH_IDENTIFIER, List.of(request(ASSOCIATED_CLASS_UUID, DeleteAction.DELETE))); assertResourceFullyPresent("AssociatedClass.ChildClass"); } @@ -294,11 +335,11 @@ void executeDeleteRequests_deleteClass_doesNotRemoveInternalAssociations() { @Test void executeDeleteRequests_multipleRequests_executesAllInOrder() { - var requests = List.of( - request(ATTR1_UUID, DeleteAction.DELETE), - request(CHILD_CLASS_UUID, DeleteAction.REMOVE_SUBCLASS_REFERENCE), - request(ONTOLOGY_UUID, DeleteAction.DELETE) - ); + var requests = + List.of( + request(ATTR1_UUID, DeleteAction.DELETE), + request(CHILD_CLASS_UUID, DeleteAction.REMOVE_SUBCLASS_REFERENCE), + request(ONTOLOGY_UUID, DeleteAction.DELETE)); service.executeDeleteRequests(GRAPH_IDENTIFIER, requests); @@ -309,8 +350,13 @@ void executeDeleteRequests_multipleRequests_executesAllInOrder() { var model = readModel(); try { var childClassResource = ResourceFactory.createResource(CIM_NS + "ChildClass"); - assertThat(childClassResource.inModel(model).hasProperty(RDF.type, RDFS.Class)).isTrue(); - assertThat(model.listStatements(childClassResource, RDFS.subClassOf, (RDFNode) null).hasNext()).isFalse(); + assertThat(childClassResource.inModel(model).hasProperty(RDF.type, RDFS.Class)) + .isTrue(); + assertThat( + model.listStatements( + childClassResource, RDFS.subClassOf, (RDFNode) null) + .hasNext()) + .isFalse(); } finally { endRead(); } @@ -323,7 +369,9 @@ void executeDeleteRequests_multipleRequests_executesAllInOrder() { @Test void executeDeleteRequests_unsupportedActionForPackage_skipsWithoutException() { - service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(PACKAGE_UUID, DeleteAction.REMOVE_SUBCLASS_REFERENCE))); + service.executeDeleteRequests( + GRAPH_IDENTIFIER, + List.of(request(PACKAGE_UUID, DeleteAction.REMOVE_SUBCLASS_REFERENCE))); assertResourceFullyPresent("Package_Package"); } @@ -340,20 +388,23 @@ void executeDeleteRequests_nullAction_skipsWithoutException() { @Test void executeDeleteRequests_deleteReferencedResource_preservesUuidTriple() { // DatatypeClass is referenced by ParentClass.attr1 via cims:dataType - service.executeDeleteRequests(GRAPH_IDENTIFIER, List.of(request(DATATYPE_CLASS_UUID, DeleteAction.DELETE))); + service.executeDeleteRequests( + GRAPH_IDENTIFIER, List.of(request(DATATYPE_CLASS_UUID, DeleteAction.DELETE))); var model = readModel(); try { var datatypeResource = ResourceFactory.createResource(CIM_NS + "DatatypeClass"); // UUID triple should be preserved since it's referenced elsewhere - var uuidStatements = model.listStatements(datatypeResource, RDFA.uuid, (RDFNode) null).toList(); + var uuidStatements = + model.listStatements(datatypeResource, RDFA.uuid, (RDFNode) null).toList(); assertThat(uuidStatements).isNotEmpty(); // All other properties should be removed - var otherStatements = model.listStatements(datatypeResource, null, (RDFNode) null) - .filterDrop(stmt -> stmt.getPredicate().equals(RDFA.uuid)) - .toList(); + var otherStatements = + model.listStatements(datatypeResource, null, (RDFNode) null) + .filterDrop(stmt -> stmt.getPredicate().equals(RDFA.uuid)) + .toList(); assertThat(otherStatements).isEmpty(); } finally { endRead(); diff --git a/backend/src/test/java/org/rdfarchitect/services/delete/FindDeleteDependenciesServiceTest.java b/backend/src/test/java/org/rdfarchitect/services/delete/FindDeleteDependenciesServiceTest.java index 1c032eed..af496552 100644 --- a/backend/src/test/java/org/rdfarchitect/services/delete/FindDeleteDependenciesServiceTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/delete/FindDeleteDependenciesServiceTest.java @@ -17,6 +17,10 @@ package org.rdfarchitect.services.delete; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + import org.apache.jena.riot.Lang; import org.apache.jena.riot.RDFDataMgr; import org.apache.jena.sparql.graph.GraphFactory; @@ -44,30 +48,32 @@ import java.util.List; import java.util.UUID; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) class FindDeleteDependenciesServiceTest { - private static final String TEST_DATA_PATH = "src/test/java/org/rdfarchitect/services/delete/testdata.ttl"; + private static final String TEST_DATA_PATH = + "src/test/java/org/rdfarchitect/services/delete/testdata.ttl"; - private static final GraphIdentifier GRAPH_IDENTIFIER = new GraphIdentifier("default", "default"); + private static final GraphIdentifier GRAPH_IDENTIFIER = + new GraphIdentifier("default", "default"); // UUIDs from the TTL test data - private static final UUID PARENT_CLASS_UUID = UUID.fromString("05131eaf-a7dd-4ac4-8624-9665990985ab"); - private static final UUID CHILD_CLASS_UUID = UUID.fromString("93ee2f31-5ddd-4b25-b119-e90a5ed327b0"); - private static final UUID ASSOCIATED_CLASS_UUID = UUID.fromString("f6d92056-c469-40d4-add1-1d6adf2fa7a6"); - private static final UUID PACKAGE_UUID = UUID.fromString("0351f9b6-5e91-4059-9d8b-169d28f1b2c8"); - private static final UUID DATATYPE_CLASS_UUID = UUID.fromString("db520255-95ef-40d4-b328-e1631e4683a4"); - private static final UUID ONTOLOGY_UUID = UUID.fromString("dba8f8e3-bfb3-4e62-9ca5-b0136ed186b2"); - - @Mock - private DatabasePort databasePort; - - @InjectMocks - private FindDeleteDependenciesService service; + private static final UUID PARENT_CLASS_UUID = + UUID.fromString("05131eaf-a7dd-4ac4-8624-9665990985ab"); + private static final UUID CHILD_CLASS_UUID = + UUID.fromString("93ee2f31-5ddd-4b25-b119-e90a5ed327b0"); + private static final UUID ASSOCIATED_CLASS_UUID = + UUID.fromString("f6d92056-c469-40d4-add1-1d6adf2fa7a6"); + private static final UUID PACKAGE_UUID = + UUID.fromString("0351f9b6-5e91-4059-9d8b-169d28f1b2c8"); + private static final UUID DATATYPE_CLASS_UUID = + UUID.fromString("db520255-95ef-40d4-b328-e1631e4683a4"); + private static final UUID ONTOLOGY_UUID = + UUID.fromString("dba8f8e3-bfb3-4e62-9ca5-b0136ed186b2"); + + @Mock private DatabasePort databasePort; + + @InjectMocks private FindDeleteDependenciesService service; @BeforeEach void setUp() throws IOException { @@ -79,7 +85,8 @@ void setUp() throws IOException { var wrappedGraph = new GraphRewindableWithUUIDs(graph, 5, 5); var wrappedContext = new GraphWithContext(wrappedGraph); - when(databasePort.getGraphWithContext(any(GraphIdentifier.class))).thenReturn(wrappedContext); + when(databasePort.getGraphWithContext(any(GraphIdentifier.class))) + .thenReturn(wrappedContext); } // ==================== Simple resource types ==================== @@ -112,14 +119,17 @@ void getDeleteDependencies_classWithDirectChild_returnsChildAsAffectedResource() assertThat(result.getResourceIdentifier().getUuid()).isEqualTo(PARENT_CLASS_UUID); assertThat(result.getType()).isEqualTo(CimResourceType.CLASS); - var childClasses = result.getChildren().stream() - .filter(c -> c.getType() == CimResourceType.CLASS) - .toList(); - - assertThat(childClasses).isNotEmpty() - .anyMatch(c -> - c.getResourceIdentifier().getUuid().equals(CHILD_CLASS_UUID) - && c.getReason() == AffectedResourceReason.CHILD_OF); + var childClasses = + result.getChildren().stream() + .filter(c -> c.getType() == CimResourceType.CLASS) + .toList(); + + assertThat(childClasses) + .isNotEmpty() + .anyMatch( + c -> + c.getResourceIdentifier().getUuid().equals(CHILD_CLASS_UUID) + && c.getReason() == AffectedResourceReason.CHILD_OF); } @Test @@ -130,17 +140,27 @@ void getDeleteDependencies_classWithTransitiveChildren_returnsNestedHierarchy() assertThat(result.getResourceIdentifier().getUuid()).isEqualTo(ASSOCIATED_CLASS_UUID); // ParentClass should be a direct child - var parentClassChild = result.getChildren().stream() - .filter(c -> c.getType() == CimResourceType.CLASS - && c.getResourceIdentifier().getUuid().equals(PARENT_CLASS_UUID)) - .findFirst(); + var parentClassChild = + result.getChildren().stream() + .filter( + c -> + c.getType() == CimResourceType.CLASS + && c.getResourceIdentifier() + .getUuid() + .equals(PARENT_CLASS_UUID)) + .findFirst(); assertThat(parentClassChild).isPresent(); // ChildClass should be nested under ParentClass, not flat - var childClassNested = parentClassChild.get().getChildren().stream() - .filter(c -> c.getType() == CimResourceType.CLASS - && c.getResourceIdentifier().getUuid().equals(CHILD_CLASS_UUID)) - .findFirst(); + var childClassNested = + parentClassChild.get().getChildren().stream() + .filter( + c -> + c.getType() == CimResourceType.CLASS + && c.getResourceIdentifier() + .getUuid() + .equals(CHILD_CLASS_UUID)) + .findFirst(); assertThat(childClassNested).isPresent(); } @@ -154,9 +174,8 @@ void getDeleteDependencies_cyclicInheritance_doesNotCauseInfiniteLoop() { // Each class must appear only once in the tree var allChildClasses = flattenChildClasses(result); - var classUuids = allChildClasses.stream() - .map(c -> c.getResourceIdentifier().getUuid()) - .toList(); + var classUuids = + allChildClasses.stream().map(c -> c.getResourceIdentifier().getUuid()).toList(); assertThat(classUuids).doesNotHaveDuplicates(); } @@ -164,14 +183,20 @@ void getDeleteDependencies_cyclicInheritance_doesNotCauseInfiniteLoop() { void getDeleteDependencies_childClassActions_containDeleteKeepAndRemoveSubclassReference() { var result = service.getDeleteDependencies(GRAPH_IDENTIFIER, ASSOCIATED_CLASS_UUID); - var childClasses = result.getChildren().stream() - .filter(c -> c.getType() == CimResourceType.CLASS) - .toList(); - - assertThat(childClasses).isNotEmpty() - .allSatisfy(child -> - assertThat(child.getActions()).containsExactlyInAnyOrder( - DeleteAction.DELETE, DeleteAction.KEEP, DeleteAction.REMOVE_SUBCLASS_REFERENCE)); + var childClasses = + result.getChildren().stream() + .filter(c -> c.getType() == CimResourceType.CLASS) + .toList(); + + assertThat(childClasses) + .isNotEmpty() + .allSatisfy( + child -> + assertThat(child.getActions()) + .containsExactlyInAnyOrder( + DeleteAction.DELETE, + DeleteAction.KEEP, + DeleteAction.REMOVE_SUBCLASS_REFERENCE)); } // ==================== Class with associations ==================== @@ -180,27 +205,34 @@ void getDeleteDependencies_childClassActions_containDeleteKeepAndRemoveSubclassR void getDeleteDependencies_classWithAssociations_returnsAssociationsAsAffectedResources() { var result = service.getDeleteDependencies(GRAPH_IDENTIFIER, ASSOCIATED_CLASS_UUID); - var associations = result.getChildren().stream() - .filter(c -> c.getType() == CimResourceType.ASSOCIATION) - .toList(); - - assertThat(associations).isNotEmpty() - .allSatisfy(assoc -> - assertThat(assoc.getReason()).isEqualTo(AffectedResourceReason.REFENCES_DELETED_CLASS_VIA_ASSOCIATION)); + var associations = + result.getChildren().stream() + .filter(c -> c.getType() == CimResourceType.ASSOCIATION) + .toList(); + + assertThat(associations) + .isNotEmpty() + .allSatisfy( + assoc -> + assertThat(assoc.getReason()) + .isEqualTo( + AffectedResourceReason + .REFENCES_DELETED_CLASS_VIA_ASSOCIATION)); } @Test void getDeleteDependencies_associationWithTarget_returnsAffectedAssociationWithTarget() { var result = service.getDeleteDependencies(GRAPH_IDENTIFIER, ASSOCIATED_CLASS_UUID); - var affectedAssociations = result.getChildren().stream() - .filter(AffectedAssociation.class::isInstance) - .map(c -> (AffectedAssociation) c) - .toList(); + var affectedAssociations = + result.getChildren().stream() + .filter(AffectedAssociation.class::isInstance) + .map(c -> (AffectedAssociation) c) + .toList(); - assertThat(affectedAssociations).isNotEmpty() - .allSatisfy(assoc -> - assertThat(assoc.getTarget()).isNotNull()); + assertThat(affectedAssociations) + .isNotEmpty() + .allSatisfy(assoc -> assertThat(assoc.getTarget()).isNotNull()); } // ==================== Child classes have their own dependencies ==================== @@ -211,15 +243,21 @@ void getDeleteDependencies_childClassWithAssociations_childHasAssociationsAsChil // and ChildClass should have its own associations as children var result = service.getDeleteDependencies(GRAPH_IDENTIFIER, PARENT_CLASS_UUID); - var childClassAffected = result.getChildren().stream() - .filter(c -> c.getType() == CimResourceType.CLASS - && c.getResourceIdentifier().getUuid().equals(CHILD_CLASS_UUID)) - .findFirst(); + var childClassAffected = + result.getChildren().stream() + .filter( + c -> + c.getType() == CimResourceType.CLASS + && c.getResourceIdentifier() + .getUuid() + .equals(CHILD_CLASS_UUID)) + .findFirst(); assertThat(childClassAffected).isPresent(); - var childAssociations = childClassAffected.get().getChildren().stream() - .filter(c -> c.getType() == CimResourceType.ASSOCIATION) - .toList(); + var childAssociations = + childClassAffected.get().getChildren().stream() + .filter(c -> c.getType() == CimResourceType.ASSOCIATION) + .toList(); assertThat(childAssociations).isNotEmpty(); } @@ -230,13 +268,19 @@ void getDeleteDependencies_classUsedAsDatatype_returnsAttributeAsAffectedResourc // DatatypeClass is used as datatype in ParentClass.attr1 var result = service.getDeleteDependencies(GRAPH_IDENTIFIER, DATATYPE_CLASS_UUID); - var attributes = result.getChildren().stream() - .filter(c -> c.getType() == CimResourceType.ATTRIBUTE) - .toList(); - - assertThat(attributes).isNotEmpty() - .allSatisfy(attr -> - assertThat(attr.getReason()).isEqualTo(AffectedResourceReason.USES_DELETED_CLASS_AS_DATATYPE)); + var attributes = + result.getChildren().stream() + .filter(c -> c.getType() == CimResourceType.ATTRIBUTE) + .toList(); + + assertThat(attributes) + .isNotEmpty() + .allSatisfy( + attr -> + assertThat(attr.getReason()) + .isEqualTo( + AffectedResourceReason + .USES_DELETED_CLASS_AS_DATATYPE)); } // ==================== Delete package ==================== @@ -249,43 +293,56 @@ void getDeleteDependencies_package_returnsAllClassesInPackageAsChildren() { assertThat(result.getType()).isEqualTo(CimResourceType.PACKAGE); assertThat(result.getReason()).isEqualTo(AffectedResourceReason.DELETION_REQUESTED_BY_USER); - var childClasses = result.getChildren().stream() - .filter(c -> c.getType() == CimResourceType.CLASS) - .toList(); + var childClasses = + result.getChildren().stream() + .filter(c -> c.getType() == CimResourceType.CLASS) + .toList(); // Package_Package contains: ParentClass, ChildClass, AssociatedClass assertThat(childClasses).hasSize(3); - var childUuids = childClasses.stream() - .map(c -> c.getResourceIdentifier().getUuid()) - .toList(); - assertThat(childUuids).containsExactlyInAnyOrder( - PARENT_CLASS_UUID, CHILD_CLASS_UUID, ASSOCIATED_CLASS_UUID); + var childUuids = + childClasses.stream().map(c -> c.getResourceIdentifier().getUuid()).toList(); + assertThat(childUuids) + .containsExactlyInAnyOrder( + PARENT_CLASS_UUID, CHILD_CLASS_UUID, ASSOCIATED_CLASS_UUID); } @Test void getDeleteDependencies_package_classesHaveCorrectReasonAndActions() { var result = service.getDeleteDependencies(GRAPH_IDENTIFIER, PACKAGE_UUID); - var childClasses = result.getChildren().stream() - .filter(c -> c.getType() == CimResourceType.CLASS) - .toList(); - - assertThat(childClasses).isNotEmpty() - .allSatisfy(cls -> { - assertThat(cls.getReason()).isEqualTo(AffectedResourceReason.CONTAINED_IN_PACKAGE); - assertThat(cls.getActions()).containsExactlyInAnyOrder( - DeleteAction.DELETE, DeleteAction.KEEP, DeleteAction.REMOVE_PACKAGE_REFERENCE); - }); + var childClasses = + result.getChildren().stream() + .filter(c -> c.getType() == CimResourceType.CLASS) + .toList(); + + assertThat(childClasses) + .isNotEmpty() + .allSatisfy( + cls -> { + assertThat(cls.getReason()) + .isEqualTo(AffectedResourceReason.CONTAINED_IN_PACKAGE); + assertThat(cls.getActions()) + .containsExactlyInAnyOrder( + DeleteAction.DELETE, + DeleteAction.KEEP, + DeleteAction.REMOVE_PACKAGE_REFERENCE); + }); } @Test void getDeleteDependencies_package_classesInPackageHaveTheirOwnDependencies() { var result = service.getDeleteDependencies(GRAPH_IDENTIFIER, PACKAGE_UUID); - var associatedClassAffected = result.getChildren().stream() - .filter(c -> c.getResourceIdentifier().getUuid().equals(ASSOCIATED_CLASS_UUID)) - .findFirst(); + var associatedClassAffected = + result.getChildren().stream() + .filter( + c -> + c.getResourceIdentifier() + .getUuid() + .equals(ASSOCIATED_CLASS_UUID)) + .findFirst(); assertThat(associatedClassAffected).isPresent(); // AssociatedClass has associations and child classes From 53a5a14ebeb78c6a02dd86b9de9d842a6b830b9e Mon Sep 17 00:00:00 2001 From: Maximilian Date: Mon, 27 Apr 2026 10:23:34 +0200 Subject: [PATCH 17/31] removed comments --- .../delete/relations/AffectedResource.java | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java index 66e89e16..9476ef6e 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java @@ -66,40 +66,3 @@ public enum AffectedResourceReason { DELETION_REQUESTED_BY_USER, } } - - /* - Eine Assoziation kann nur von der inverse gelöscht referenziert werden, da aber immer alles zusammen gelöscht wird gibt es ein trickle effect über den man entscheiden muss - */ - - /* - Ein Attribut zu löschen hat keine Auswirkung auf irgend etwas anderes - */ - - /* - other classes - Another class is extending this one - - Class A (Keep refence to deleted Class) (delete reference) (Delete Class? (würde ich erstmal als overkill ansehen)) - - Attributes (sowohl enum als auch normal) - This class is used as a Datatype in the following attributes: - How would you like to proceed - - Attr1 von Klasse A (unreferenced Datatype) (Delete Attribute) - ... - - This class is Referenced Via an association as a Target: - - Association1 von Klasse A (unreferenced Target) (Delete Association) - ... - */ - - /* - Ein enum entry kann von theoretisch als default wert referenziert werden. - Problem ist, dass man dann beim löschen eines enum entries den put request analysieren muss, welche operation ausgeführt wird. - Aber wenn man das macht: - - Deleting a used enum entry in a default value. (delete Attribute) (delete default value) - */ - - /* - Do you want to delete the contents of this package? - - class A (Delete Class (extend into classDropdown)) (keep reference) (remove reference) - ... - */ From 7c014606502aabae837835eff7725e4d89c29d60 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Mon, 27 Apr 2026 10:23:44 +0200 Subject: [PATCH 18/31] renamed variable --- frontend/src/routes/layout/menu-bar/Edit.svelte | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/routes/layout/menu-bar/Edit.svelte b/frontend/src/routes/layout/menu-bar/Edit.svelte index d5ac85a8..cccff363 100644 --- a/frontend/src/routes/layout/menu-bar/Edit.svelte +++ b/frontend/src/routes/layout/menu-bar/Edit.svelte @@ -60,7 +60,7 @@ let showNewGraphDialog = $state(false); let showNewPackageDialog = $state(false); let showFilterViewDialog = $state(false); - let showDeleteDependenciesDialog = $state(false); + let ShowPackageDeleteDependenciesDialog = $state(false); let showOntologyDeleteDependenciesDialog = $state(false); let showPackageEditorDialog = $state(false); let showNamespaceDialog = $state(false); @@ -155,7 +155,7 @@ packageDialogTarget = { ...selectedPackageDetails }; packageDialogDataset = selectedDataset; packageDialogGraph = selectedGraph; - showDeleteDependenciesDialog = true; + ShowPackageDeleteDependenciesDialog = true; } async function getPackages() { @@ -373,9 +373,9 @@ readonly={isDatasetReadOnly} /> {/if} -{#if packageDialogTarget && showDeleteDependenciesDialog} +{#if packageDialogTarget && ShowPackageDeleteDependenciesDialog} Date: Mon, 27 Apr 2026 10:25:32 +0200 Subject: [PATCH 19/31] removed repeating checks for resource type --- .../delete/DeleteResourcesService.java | 135 +++++++++++------- 1 file changed, 82 insertions(+), 53 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java index b52e9c30..029ae868 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java @@ -42,6 +42,7 @@ import java.util.ArrayDeque; import java.util.List; +import java.util.Objects; import java.util.Set; @Service @@ -52,6 +53,14 @@ public class DeleteResourcesService implements DeleteResourcesUseCase { private final DatabasePort databasePort; + /** + * Internal record that holds the pre-resolved resource, its CIM type, and the requested action. + * This avoids redundant model lookups during deletion and allows for upfront validation of + * unsupported actions. + */ + private record ResolvedDeleteRequest( + Resource resource, CimResourceType type, DeleteAction action) {} + @Override public void executeDeleteRequests( GraphIdentifier graphIdentifier, List deleteRequests) { @@ -69,37 +78,68 @@ public void executeDeleteRequests( } private void deleteResources(Model model, List deleteRequests) { - for (var deleteRequest : deleteRequests) { + var resolvedRequests = resolveAll(model, deleteRequests); + for (var resolved : resolvedRequests) { try { - deleteResource(model, deleteRequest); + deleteResource(resolved); } catch (UnsupportedOperationException | IllegalArgumentException e) { logger.warn( - "Skipping deletion of resource with UUID {} due to unsupported action: {} : {}", - deleteRequest.getUuid(), - deleteRequest.getAction(), + "Skipping deletion of resource {} due to unsupported action: {} : {}", + resolved.resource(), + resolved.action(), e.getMessage()); } catch (IllegalStateException e) { logger.warn( - "Skipping deletion of resource with UUID {} due to illegal state: {}", - deleteRequest.getUuid(), + "Skipping deletion of resource {} due to illegal state: {}", + resolved.resource(), e.getMessage()); } } } - private void deleteResource(Model model, ResourceDeleteRequest deleteRequest) { - var resourceType = CIMResourceTypeIdentifyingUtils.getType(model, deleteRequest.getUuid()); - switch (resourceType) { - case PACKAGE -> deletePackage(model, deleteRequest); - case CLASS -> deleteClass(model, deleteRequest); - case ATTRIBUTE -> deleteAttribute(model, deleteRequest); - case ASSOCIATION -> deleteAssociation(model, deleteRequest); - case ENUM_ENTRY -> deleteEnumEntry(model, deleteRequest); - case ONTOLOGY -> deleteOntology(model, deleteRequest); + /** + * Resolves all delete requests up front: looks up the resource and its CIM type once per UUID. + * Requests that cannot be resolved (unknown type, missing resource) are logged and skipped. + */ + private List resolveAll( + Model model, List deleteRequests) { + return deleteRequests.stream() + .map(req -> resolve(model, req)) + .filter(Objects::nonNull) + .toList(); + } + + private ResolvedDeleteRequest resolve(Model model, ResourceDeleteRequest req) { + try { + var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, req.getUuid()); + var type = CIMResourceTypeIdentifyingUtils.getType(model, req.getUuid()); + if (type == CimResourceType.UNKNOWN) { + logger.warn( + "Skipping deletion of resource with UUID {} : unknown resource type", + req.getUuid()); + return null; + } + return new ResolvedDeleteRequest(resource, type, req.getAction()); + } catch (Exception e) { + logger.warn( + "Skipping deletion of resource with UUID {} : could not resolve: {}", + req.getUuid(), + e.getMessage()); + return null; + } + } + + private void deleteResource(ResolvedDeleteRequest resolved) { + switch (resolved.type()) { + case PACKAGE -> deletePackage(resolved); + case CLASS -> deleteClass(resolved); + case ATTRIBUTE -> deleteAttribute(resolved); + case ASSOCIATION -> deleteAssociation(resolved); + case ENUM_ENTRY -> deleteEnumEntry(resolved); + case ONTOLOGY -> deleteOntology(resolved); case UNKNOWN -> throw new IllegalArgumentException( - "Unknown resource type for resource with UUID: " - + deleteRequest.getUuid()); + "Unknown resource type for resource: " + resolved.resource()); } } @@ -174,14 +214,11 @@ private boolean isReferencedElsewhere(Model model, Resource resource) { .hasNext(); } - private void deletePackage(Model model, ResourceDeleteRequest deleteRequest) { - if (shouldSkipOrThrow( - deleteRequest.getAction(), CimResourceType.PACKAGE, DeleteAction.DELETE)) { + private void deletePackage(ResolvedDeleteRequest resolved) { + if (shouldSkipOrThrow(resolved.action(), CimResourceType.PACKAGE, DeleteAction.DELETE)) { return; } - var resource = - CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); - removeResource(resource); + removeResource(resolved.resource()); } /** @@ -189,23 +226,24 @@ private void deletePackage(Model model, ResourceDeleteRequest deleteRequest) { * the action is {@link DeleteAction#REMOVE_SUBCLASS_REFERENCE}, only the {@code * rdfs:subClassOf} triple is removed, leaving the class itself intact. */ - private void deleteClass(Model model, ResourceDeleteRequest deleteRequest) { + private void deleteClass(ResolvedDeleteRequest resolved) { if (shouldSkipOrThrow( - deleteRequest.getAction(), + resolved.action(), CimResourceType.CLASS, DeleteAction.DELETE, DeleteAction.REMOVE_SUBCLASS_REFERENCE, DeleteAction.REMOVE_PACKAGE_REFERENCE)) { return; } - var resource = - CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); - if (deleteRequest.getAction() == DeleteAction.REMOVE_SUBCLASS_REFERENCE) { + var resource = resolved.resource(); + var model = resource.getModel(); + + if (resolved.action() == DeleteAction.REMOVE_SUBCLASS_REFERENCE) { resource.listProperties(RDFS.subClassOf).forEach(model::remove); return; } - if (deleteRequest.getAction() == DeleteAction.REMOVE_PACKAGE_REFERENCE) { + if (resolved.action() == DeleteAction.REMOVE_PACKAGE_REFERENCE) { resource.listProperties(CIMS.belongsToCategory).forEach(model::remove); return; } @@ -216,7 +254,7 @@ private void deleteClass(Model model, ResourceDeleteRequest deleteRequest) { .toList() .forEach(this::removeResource); - // delete associations only if it references an external resource + // Delete associations only if they reference an external resource model.listSubjectsWithProperty(RDFS.domain, resource) .filterKeep(CIMPropertyUtils::isAssociation) .filterKeep( @@ -235,14 +273,11 @@ private void deleteClass(Model model, ResourceDeleteRequest deleteRequest) { removeResource(resource); } - private void deleteAttribute(Model model, ResourceDeleteRequest deleteRequest) { - if (shouldSkipOrThrow( - deleteRequest.getAction(), CimResourceType.ATTRIBUTE, DeleteAction.DELETE)) { + private void deleteAttribute(ResolvedDeleteRequest resolved) { + if (shouldSkipOrThrow(resolved.action(), CimResourceType.ATTRIBUTE, DeleteAction.DELETE)) { return; } - var resource = - CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); - removeResource(resource); + removeResource(resolved.resource()); } /** @@ -250,13 +285,13 @@ private void deleteAttribute(Model model, ResourceDeleteRequest deleteRequest) { * cims:inverseRoleName}, the mutual references are removed first to prevent {@link * #removeResource} from preserving stale UUID triples due to the circular reference. */ - private void deleteAssociation(Model model, ResourceDeleteRequest deleteRequest) { + private void deleteAssociation(ResolvedDeleteRequest resolved) { if (shouldSkipOrThrow( - deleteRequest.getAction(), CimResourceType.ASSOCIATION, DeleteAction.DELETE)) { + resolved.action(), CimResourceType.ASSOCIATION, DeleteAction.DELETE)) { return; } - var resource = - CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); + var resource = resolved.resource(); + var model = resource.getModel(); var inverseStmt = resource.getProperty(CIMS.inverseRoleName); if (inverseStmt != null && inverseStmt.getObject().isResource()) { @@ -270,23 +305,17 @@ private void deleteAssociation(Model model, ResourceDeleteRequest deleteRequest) removeResource(resource); } - private void deleteEnumEntry(Model model, ResourceDeleteRequest deleteRequest) { - if (shouldSkipOrThrow( - deleteRequest.getAction(), CimResourceType.ENUM_ENTRY, DeleteAction.DELETE)) { + private void deleteEnumEntry(ResolvedDeleteRequest resolved) { + if (shouldSkipOrThrow(resolved.action(), CimResourceType.ENUM_ENTRY, DeleteAction.DELETE)) { return; } - var resource = - CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); - removeResource(resource); + removeResource(resolved.resource()); } - private void deleteOntology(Model model, ResourceDeleteRequest deleteRequest) { - if (shouldSkipOrThrow( - deleteRequest.getAction(), CimResourceType.ONTOLOGY, DeleteAction.DELETE)) { + private void deleteOntology(ResolvedDeleteRequest resolved) { + if (shouldSkipOrThrow(resolved.action(), CimResourceType.ONTOLOGY, DeleteAction.DELETE)) { return; } - var resource = - CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, deleteRequest.getUuid()); - removeResource(resource); + removeResource(resolved.resource()); } } From 3f01c90027d1c5155e9dfc3dcaac0f63e5d1c0c2 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Mon, 27 Apr 2026 11:27:15 +0200 Subject: [PATCH 20/31] refactored code into util classes, for future reusability --- .../cim/relations/model/CIMClassUtils.java | 16 +++- .../cim/relations/model/CIMPackageUtils.java | 52 +++++++++++++ .../model/properties/CIMAssociationUtils.java | 37 ++++++++++ .../model/properties/CIMAttributeUtils.java | 21 ++++++ .../delete/FindDeleteDependenciesService.java | 74 +++---------------- 5 files changed, 136 insertions(+), 64 deletions(-) create mode 100644 backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMPackageUtils.java diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMClassUtils.java b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMClassUtils.java index 1719468b..b7509ece 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMClassUtils.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMClassUtils.java @@ -28,6 +28,7 @@ import java.util.HashSet; import java.util.LinkedList; +import java.util.List; import java.util.Set; @UtilityClass @@ -61,13 +62,26 @@ public Set findDerivingClasses(Resource classResource) { * @param classResource class resource to find deriving classes for * @return a set of directly deriving classes */ - private Set findDirectlyDerivingClasses(Resource classResource) { + public Set findDirectlyDerivingClasses(Resource classResource) { var ontology = classResource.getModel(); return ontology.listResourcesWithProperty(RDFS.subClassOf, classResource) .mapWith(RDFNode::asResource) .toSet(); } + /** + * Lists all directly deriving classes as a list. + * + * @param classResource class resource to find deriving classes for + * @return a list of directly deriving classes + */ + public List listDirectlyDerivingClasses(Resource classResource) { + return classResource + .getModel() + .listSubjectsWithProperty(RDFS.subClassOf, classResource) + .toList(); + } + /** * returns a list of all superClasses of a specified class. * diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMPackageUtils.java b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMPackageUtils.java new file mode 100644 index 00000000..669e7e4f --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMPackageUtils.java @@ -0,0 +1,52 @@ +/* + * 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.models.cim.relations.model; + +import lombok.experimental.UtilityClass; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import org.rdfarchitect.models.cim.rdf.resources.CIMS; + +import java.util.List; +import java.util.UUID; + +@UtilityClass +public class CIMPackageUtils { + + /** + * Lists all classes contained in a given package. + * + * @param model the RDF model + * @param packageUuid the UUID of the package + * @return a list of class resources belonging to the package + * @throws IllegalStateException if the resource with the given UUID is not a package + */ + public List listClassesInPackage(Model model, UUID packageUuid) { + var packageResource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, packageUuid); + if (!packageResource.hasProperty(RDF.type, CIMS.classCategory)) { + throw new IllegalStateException( + "Resource with UUID " + packageUuid + " is not a package."); + } + return model.listSubjectsWithProperty(CIMS.belongsToCategory, packageResource) + .filterKeep(cls -> cls.hasProperty(RDF.type, RDFS.Class)) + .toList(); + } +} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/properties/CIMAssociationUtils.java b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/properties/CIMAssociationUtils.java index 80935eeb..c26b88a6 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/properties/CIMAssociationUtils.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/properties/CIMAssociationUtils.java @@ -24,6 +24,7 @@ import org.rdfarchitect.models.cim.rdf.resources.CIMS; import org.rdfarchitect.models.cim.relations.model.CIMClassUtils; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -55,4 +56,40 @@ public Set listAssociationDatatypes(Resource property) { instantiableDerivingClasses.add(targetClass); // include the class itself return instantiableDerivingClasses; } + + /** + * Lists all associations that reference a given class via {@code rdfs:domain}. + * + * @param classResource the class resource to find referencing associations for + * @return a list of association resources referencing the class + */ + public List listAssociationsReferencingClass(Resource classResource) { + return classResource + .getModel() + .listSubjectsWithProperty(RDFS.domain, classResource) + .filterKeep(CIMPropertyUtils::isAssociation) + .toList(); + } + + /** + * Returns the target (range) resource of an association. + * + * @param associationResource the association resource + * @return the range resource of the association + * @throws IllegalStateException if the association has no range or has a literal as range + */ + public Resource getAssociationTarget(Resource associationResource) { + var rangeStatement = associationResource.getProperty(RDFS.range); + if (rangeStatement == null) { + throw new IllegalStateException( + "Association " + associationResource + " does not have a range."); + } + if (rangeStatement.getObject().isLiteral()) { + throw new IllegalStateException( + "Association " + + associationResource + + " has a literal as range, which is not supported."); + } + return rangeStatement.getObject().asResource(); + } } diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/properties/CIMAttributeUtils.java b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/properties/CIMAttributeUtils.java index 0eda6c21..1473d2e5 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/properties/CIMAttributeUtils.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/properties/CIMAttributeUtils.java @@ -30,6 +30,7 @@ import org.rdfarchitect.models.cim.rdf.resources.CIMStereotypes; import org.rdfarchitect.shacl.XSDDatatypeMapper; +import java.util.List; import java.util.Set; @UtilityClass @@ -175,4 +176,24 @@ public Set listEnumDatatypeEntries(Resource attribute) { var enumClass = attribute.getProperty(RDFS.range).getResource(); return ontology.listResourcesWithProperty(RDF.type, enumClass).toSet(); } + + /** + * Lists all attributes that use a given class as their datatype (via {@code cims:datatype} or + * {@code rdfs:range}). + * + * @param classResource the class resource used as datatype + * @return a list of attribute resources that reference the class as their datatype + */ + public List listAttributesWithClassAsDatatype(Resource classResource) { + var model = classResource.getModel(); + var byDatatype = model.listSubjectsWithProperty(CIMS.datatype, classResource); + var byRange = model.listSubjectsWithProperty(RDFS.range, classResource); + return byDatatype + .andThen(byRange) + .filterKeep(CIMPropertyUtils::isAttribute) + .toList() + .stream() + .distinct() + .toList(); + } } diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java b/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java index 284492f7..9142007c 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java @@ -24,7 +24,6 @@ import org.apache.jena.rdf.model.Model; 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.api.dto.delete.DeleteAction; import org.rdfarchitect.api.dto.delete.ResourceIdentifier; @@ -34,11 +33,13 @@ import org.rdfarchitect.api.dto.delete.relations.AffectedResource.AffectedResourceReason; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; -import org.rdfarchitect.models.cim.rdf.resources.CIMS; +import org.rdfarchitect.models.cim.relations.model.CIMClassUtils; +import org.rdfarchitect.models.cim.relations.model.CIMPackageUtils; import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils; import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils.CimResourceType; import org.rdfarchitect.models.cim.relations.model.CIMResourceUtils; -import org.rdfarchitect.models.cim.relations.model.properties.CIMPropertyUtils; +import org.rdfarchitect.models.cim.relations.model.properties.CIMAssociationUtils; +import org.rdfarchitect.models.cim.relations.model.properties.CIMAttributeUtils; import org.rdfarchitect.rdf.graph.GraphUtils; import org.rdfarchitect.rdf.graph.wrapper.GraphRewindable; import org.springframework.stereotype.Service; @@ -105,7 +106,7 @@ private AffectedResource findAffectedRelationsForPackage( UUID uuid, AffectedResourceReason reason, List deleteActions) { - var classesInPackage = listClassesInPackage(model, uuid); + var classesInPackage = CIMPackageUtils.listClassesInPackage(model, uuid); var affectedResources = new ArrayList(); var clsDeleteActions = List.of( @@ -148,7 +149,7 @@ private AffectedResource findAffectedRelationsForClass( private List findAffectedAttributesForClass(Resource classResource) { var childActions = List.of(DeleteAction.DELETE, DeleteAction.KEEP); - return listAttributesWithClassAsDatatype(classResource).stream() + return CIMAttributeUtils.listAttributesWithClassAsDatatype(classResource).stream() .map( attr -> new AffectedOwnedResource( @@ -166,7 +167,7 @@ private List findAffectedAttributesForClass(Resource classReso private List findAffectedAssociationsForClass( Resource classResource, ResourceIdentifier classResourceId) { - return listAssociationsReferencingClass(classResource).stream() + return CIMAssociationUtils.listAssociationsReferencingClass(classResource).stream() .map( assoc -> { var childActions = new ArrayList(); @@ -175,34 +176,19 @@ private List findAffectedAssociationsForClass( assoc.getProperty(RDFS.range).getObject().asResource())) { childActions.add(DeleteAction.KEEP); } + var targetResource = CIMAssociationUtils.getAssociationTarget(assoc); return new AffectedAssociation( createResourceIdentifier(assoc), CimResourceType.ASSOCIATION, AffectedResourceReason .REFENCES_DELETED_CLASS_VIA_ASSOCIATION, classResourceId, - getAssociationTarget(assoc)) + createResourceIdentifier(targetResource)) .setActions(childActions); }) .toList(); } - private ResourceIdentifier getAssociationTarget(Resource associationResource) { - var rangeStatement = associationResource.getProperty(RDFS.range); - if (rangeStatement == null) { - throw new IllegalStateException( - "Association " + associationResource + " does not have a range."); - } - if (rangeStatement.getObject().isLiteral()) { - throw new IllegalStateException( - "Association " - + associationResource - + " has a literal as range, which is not supported."); - } - var rangeResource = rangeStatement.getObject().asResource(); - return createResourceIdentifier(rangeResource); - } - private List findAffectedChildClassesForClass(Resource classResource) { var childClassActions = List.of( @@ -241,7 +227,7 @@ private List buildAffectedChildClassTree( private LinkedList> initializeQueue( Resource classResource, Set visited) { var queue = new LinkedList>(); - for (var directChild : listDirectlyDescendingClasses(classResource)) { + for (var directChild : CIMClassUtils.listDirectlyDerivingClasses(classResource)) { if (visited.add(directChild)) { queue.add(Map.entry(directChild, classResource)); } @@ -287,51 +273,13 @@ private void enqueueChildren( Resource current, Set visited, LinkedList> queue) { - for (var child : listDirectlyDescendingClasses(current)) { + for (var child : CIMClassUtils.listDirectlyDerivingClasses(current)) { if (visited.add(child)) { queue.add(Map.entry(child, current)); } } } - private List listDirectlyDescendingClasses(Resource classResource) { - return classResource - .getModel() - .listSubjectsWithProperty(RDFS.subClassOf, classResource) - .toList(); - } - - private List listClassesInPackage(Model model, UUID uuid) { - var packageResource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, uuid); - if (!packageResource.hasProperty(RDF.type, CIMS.classCategory)) { - throw new IllegalStateException("Resource with UUID " + uuid + " is not a package."); - } - return model.listSubjectsWithProperty(CIMS.belongsToCategory, packageResource) - .filterKeep(cls -> cls.hasProperty(RDF.type, RDFS.Class)) - .toList(); - } - - private List listAssociationsReferencingClass(Resource classResource) { - return classResource - .getModel() - .listSubjectsWithProperty(RDFS.domain, classResource) - .filterKeep(CIMPropertyUtils::isAssociation) - .toList(); - } - - private List listAttributesWithClassAsDatatype(Resource classResource) { - var model = classResource.getModel(); - var byDatatype = model.listSubjectsWithProperty(CIMS.datatype, classResource); - var byRange = model.listSubjectsWithProperty(RDFS.range, classResource); - return byDatatype - .andThen(byRange) - .filterKeep(CIMPropertyUtils::isAttribute) - .toList() - .stream() - .distinct() - .toList(); - } - private ResourceIdentifier createResourceIdentifier(Model model, UUID uuid) { var resource = CIMResourceTypeIdentifyingUtils.findUniqueSubject(model, uuid); return createResourceIdentifier(resource); From c00203b894a9c7bbc1e5fd8276fb1274c23964e0 Mon Sep 17 00:00:00 2001 From: Maximilian Reisch Date: Mon, 27 Apr 2026 13:12:56 +0200 Subject: [PATCH 21/31] Potential fix for pull request finding 'Missing Override annotation' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> Signed-off-by: Maximilian Reisch --- .../api/dto/delete/relations/AffectedAssociation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedAssociation.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedAssociation.java index 2752e597..0c443b17 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedAssociation.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedAssociation.java @@ -27,7 +27,7 @@ @Data @Accessors(chain = true) -@EqualsAndHashCode(callSuper = true) +@EqualsAndHashCode(callSuper = true, onParam_ = {@Override}) @NoArgsConstructor public class AffectedAssociation extends AffectedOwnedResource { From 48c7cbbc227a96f7b10c808d0fd988867a7813a4 Mon Sep 17 00:00:00 2001 From: Maximilian Reisch Date: Mon, 27 Apr 2026 13:13:07 +0200 Subject: [PATCH 22/31] Potential fix for pull request finding 'Missing Override annotation' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> Signed-off-by: Maximilian Reisch --- .../api/dto/delete/relations/AffectedOwnedResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedOwnedResource.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedOwnedResource.java index 6ec2e4cd..27fbb20a 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedOwnedResource.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedOwnedResource.java @@ -27,7 +27,7 @@ @Data @Accessors(chain = true) -@EqualsAndHashCode(callSuper = true) +@EqualsAndHashCode(callSuper = true, onParam_ = {@Override}) @NoArgsConstructor public class AffectedOwnedResource extends AffectedResource { From 02c0eb1640820c1490b0d026b477313427cde286 Mon Sep 17 00:00:00 2001 From: Maximilian Reisch Date: Mon, 27 Apr 2026 13:13:21 +0200 Subject: [PATCH 23/31] Potential fix for pull request finding 'Exposing internal representation' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> Signed-off-by: Maximilian Reisch --- .../api/dto/delete/relations/AffectedResource.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java index 9476ef6e..7e8d8cc1 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java @@ -17,8 +17,11 @@ package org.rdfarchitect.api.dto.delete.relations; +import lombok.AccessLevel; import lombok.Data; +import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import lombok.experimental.Accessors; import org.rdfarchitect.api.dto.delete.DeleteAction; @@ -43,6 +46,8 @@ public class AffectedResource { private List actions = new ArrayList<>(); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) private List children = new ArrayList<>(); private Map context = new HashMap<>(); @@ -56,6 +61,14 @@ public AffectedResource( this.reason = reason; } + public List getChildren() { + return new ArrayList<>(children); + } + + public void setChildren(List children) { + this.children = (children == null) ? new ArrayList<>() : new ArrayList<>(children); + } + public enum AffectedResourceReason { CONTAINED_IN_PACKAGE, USES_DELETED_CLASS_AS_DATATYPE, From 2309805a896f19184540822242e71cfaf00c456e Mon Sep 17 00:00:00 2001 From: Maximilian Date: Mon, 27 Apr 2026 13:17:57 +0200 Subject: [PATCH 24/31] rebase fixes --- .../api/dto/delete/relations/AffectedAssociation.java | 4 +++- .../api/dto/delete/relations/AffectedOwnedResource.java | 4 +++- .../api/dto/delete/relations/AffectedResource.java | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedAssociation.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedAssociation.java index 0c443b17..deb393f6 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedAssociation.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedAssociation.java @@ -27,7 +27,9 @@ @Data @Accessors(chain = true) -@EqualsAndHashCode(callSuper = true, onParam_ = {@Override}) +@EqualsAndHashCode( + callSuper = true, + onParam_ = {@Override}) @NoArgsConstructor public class AffectedAssociation extends AffectedOwnedResource { diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedOwnedResource.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedOwnedResource.java index 27fbb20a..00121801 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedOwnedResource.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedOwnedResource.java @@ -27,7 +27,9 @@ @Data @Accessors(chain = true) -@EqualsAndHashCode(callSuper = true, onParam_ = {@Override}) +@EqualsAndHashCode( + callSuper = true, + onParam_ = {@Override}) @NoArgsConstructor public class AffectedOwnedResource extends AffectedResource { diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java index 7e8d8cc1..c34ff513 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedResource.java @@ -65,8 +65,9 @@ public List getChildren() { return new ArrayList<>(children); } - public void setChildren(List children) { + public AffectedResource setChildren(List children) { this.children = (children == null) ? new ArrayList<>() : new ArrayList<>(children); + return this; } public enum AffectedResourceReason { From 19caee3abc70ab3a396eebd1e91b5ecdfadd4bf0 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Mon, 27 Apr 2026 13:36:14 +0200 Subject: [PATCH 25/31] rebase fixes --- .../api/dto/delete/relations/AffectedAssociation.java | 6 ++---- .../api/dto/delete/relations/AffectedOwnedResource.java | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedAssociation.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedAssociation.java index deb393f6..efbfb8f4 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedAssociation.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedAssociation.java @@ -27,9 +27,7 @@ @Data @Accessors(chain = true) -@EqualsAndHashCode( - callSuper = true, - onParam_ = {@Override}) +@EqualsAndHashCode(callSuper = true) @NoArgsConstructor public class AffectedAssociation extends AffectedOwnedResource { @@ -41,7 +39,7 @@ public AffectedAssociation( AffectedResourceReason reason, ResourceIdentifier domain, ResourceIdentifier target) { - this.target = target; super(resourceIdentifier, type, reason, domain); + this.target = target; } } diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedOwnedResource.java b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedOwnedResource.java index 00121801..bc637a19 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedOwnedResource.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/delete/relations/AffectedOwnedResource.java @@ -27,9 +27,7 @@ @Data @Accessors(chain = true) -@EqualsAndHashCode( - callSuper = true, - onParam_ = {@Override}) +@EqualsAndHashCode(callSuper = true) @NoArgsConstructor public class AffectedOwnedResource extends AffectedResource { @@ -40,7 +38,7 @@ public AffectedOwnedResource( CIMResourceTypeIdentifyingUtils.CimResourceType type, AffectedResourceReason reason, ResourceIdentifier domain) { - this.domain = domain; super(resourceIdentifier, type, reason); + this.domain = domain; } } From 9ce6572ffb3b8c98038cf58b66db898ab6c8835d Mon Sep 17 00:00:00 2001 From: Maximilian Date: Mon, 27 Apr 2026 13:53:00 +0200 Subject: [PATCH 26/31] more rebase fixes --- .../models/cim/relations/model/CIMResourceUtils.java | 2 +- .../org/rdfarchitect/services/select/QueryGraphService.java | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceUtils.java b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceUtils.java index 9c82eff9..012d8412 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceUtils.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/relations/model/CIMResourceUtils.java @@ -29,7 +29,7 @@ public class CIMResourceUtils { /** * Checks whether a resource is external/referenced only. In our model this would mean it only - * has a {@link RDFA::uuid} property, but no other properties. + * has a {@link RDFA uuid} property, but no other properties. * * @param resource The resource to check for. * @return True if the resource is an external resource, false otherwise. diff --git a/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java b/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java index 37e63100..208deb3e 100644 --- a/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java +++ b/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java @@ -17,8 +17,9 @@ package org.rdfarchitect.services.select; -import static org.rdfarchitect.models.cim.queries.select.CIMQueryBuilder.Mode.*; -import static org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs.*; +import static org.rdfarchitect.models.cim.queries.select.CIMQueryBuilder.Mode.OPTIONAL; +import static org.rdfarchitect.models.cim.queries.select.CIMQueryBuilder.Mode.REQUIRED; +import static org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs.removeUUIDs; import lombok.RequiredArgsConstructor; From c62c4308b98b2fcee316efa7cd72197a5941a22d Mon Sep 17 00:00:00 2001 From: Maximilian Date: Mon, 27 Apr 2026 14:01:42 +0200 Subject: [PATCH 27/31] fixed backend lint issues --- .../delete/DeleteResourcesService.java | 7 +-- .../delete/FindDeleteDependenciesService.java | 3 -- .../services/select/QueryGraphService.java | 45 ++++++++++--------- 3 files changed, 25 insertions(+), 30 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java index 029ae868..da6d6167 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java @@ -160,12 +160,9 @@ private boolean shouldSkipOrThrow( return true; } if (!Set.of(supported).contains(action)) { + var typeString = type.name().toLowerCase().replace("_", " "); throw new UnsupportedOperationException( - "Action " - + action - + " is not supported for " - + type.name().toLowerCase().replace("_", " ") - + "."); + "Action " + action + " is not supported for " + typeString + "."); } return false; } diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java b/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java index 9142007c..8243b55b 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java @@ -261,9 +261,6 @@ private void attachToParentOrRoot( var parentAffected = resourceMap.get(parent); if (parentAffected != null) { var existingChildren = parentAffected.getChildren(); - if (existingChildren == null) { - existingChildren = new ArrayList<>(); - } existingChildren.add(affectedResource); parentAffected.setChildren(existingChildren); } diff --git a/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java b/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java index 208deb3e..93791a96 100644 --- a/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java +++ b/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java @@ -114,29 +114,30 @@ public List getClassList( SELECT DISTINCT ?uri ?uuid ?label ?packageURI ?packageLabel ?packageUUID ?comment ?superClassURI ?superClassLabel WHERE { - %s - ?uri ?uuid - OPTIONAL - { ?uri rdfs:label ?label} - OPTIONAL - { ?uri cims:belongsToCategory ?packageURI - OPTIONAL - { ?packageURI rdfs:label ?packageLabel} - OPTIONAL - { ?packageURI ?packageUUID} - } - OPTIONAL - { ?uri rdfs:comment ?comment} - OPTIONAL - { ?uri rdfs:subClassOf ?superClassURI - OPTIONAL - { ?superClassURI - rdfs:label ?superClassLabel} - } - } - ORDER BY ?uri """ - .formatted(classFilter); + + classFilter + + """ + ?uri ?uuid + OPTIONAL + { ?uri rdfs:label ?label} + OPTIONAL + { ?uri cims:belongsToCategory ?packageURI + OPTIONAL + { ?packageURI rdfs:label ?packageLabel} + OPTIONAL + { ?packageURI ?packageUUID} + } + OPTIONAL + { ?uri rdfs:comment ?comment} + OPTIONAL + { ?uri rdfs:subClassOf ?superClassURI + OPTIONAL + { ?superClassURI + rdfs:label ?superClassLabel} + } + } + ORDER BY ?uri + """; // execute query var queryResultSet = From ba6806ce81f995018d6467c28effbbc9e50757df Mon Sep 17 00:00:00 2001 From: Maximilian Date: Mon, 27 Apr 2026 14:04:18 +0200 Subject: [PATCH 28/31] fixed backend lint issues --- .../rdfarchitect/services/delete/DeleteResourcesService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java index da6d6167..1333189c 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java @@ -42,6 +42,7 @@ import java.util.ArrayDeque; import java.util.List; +import java.util.Locale; import java.util.Objects; import java.util.Set; @@ -160,7 +161,7 @@ private boolean shouldSkipOrThrow( return true; } if (!Set.of(supported).contains(action)) { - var typeString = type.name().toLowerCase().replace("_", " "); + var typeString = type.name().toLowerCase(Locale.ROOT).replace("_", " "); throw new UnsupportedOperationException( "Action " + action + " is not supported for " + typeString + "."); } From 5c5c4c90b7b709ccfa3c489619a99be29b4c2c94 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Thu, 30 Apr 2026 12:15:40 +0200 Subject: [PATCH 29/31] backend lint --- .../graphs/classes/AllClassesRESTController.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/AllClassesRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/AllClassesRESTController.java index 6231f1bd..bcede28f 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/AllClassesRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/AllClassesRESTController.java @@ -26,7 +26,6 @@ import lombok.RequiredArgsConstructor; -import org.rdfarchitect.api.controller.Response; import org.rdfarchitect.api.dto.ClassUMLAdaptedDTO; import org.rdfarchitect.api.dto.packages.PackageDTO; import org.rdfarchitect.database.GraphIdentifier; @@ -108,11 +107,12 @@ public String addClass( expandURIUseCase.expandUri(datasetName, addNewClassRequest.classURIPrefix); var graphIdentifier = new GraphIdentifier(datasetName, extendedGraphURI); - var classUUID =addClassUseCase.addClass( - graphIdentifier, - addNewClassRequest.packageDTO, - extendedClassURIPrefix, - addNewClassRequest.className); + var classUUID = + addClassUseCase.addClass( + graphIdentifier, + addNewClassRequest.packageDTO, + extendedClassURIPrefix, + addNewClassRequest.className); logger.info( "Sending response to POST request: \"/api/datasets/{{}}/graphs/{{}}/classes\" to \"{}\".", From 123b14860fce4c5ef480f5015cf7ca0347bcc117 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Thu, 30 Apr 2026 15:13:24 +0200 Subject: [PATCH 30/31] rebase bugs --- .../SvelteFlowClassContextMenu.svelte | 13 +++++------ .../svelteflow/svelteFlowWrapper.svelte | 23 +++++++++---------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/frontend/src/lib/rendering/svelteflow/components/SvelteFlowClassContextMenu.svelte b/frontend/src/lib/rendering/svelteflow/components/SvelteFlowClassContextMenu.svelte index f904fa14..cbf0d91b 100644 --- a/frontend/src/lib/rendering/svelteflow/components/SvelteFlowClassContextMenu.svelte +++ b/frontend/src/lib/rendering/svelteflow/components/SvelteFlowClassContextMenu.svelte @@ -32,7 +32,7 @@ handleContextMenuOpenChange, syncContextMenuTrigger, } from "./contextMenuUtils.js"; - import DeleteClassConfirmDialog from "../../../../routes/DeleteClassConfirmDialog.svelte"; + import DeleteDependenciesDialog from "../../../../routes/delete-relations-dialog/DeleteDependenciesDialog.svelte"; let { request = null, @@ -51,7 +51,7 @@ let triggerRef = $state(null); let open = $state(false); let deleteClassTarget = $state(null); - let showDeleteClassDialog = $state(false); + let showDeleteDependenciesDialog = $state(false); let triggerStyle = $derived(getContextMenuTriggerStyle(request)); @@ -79,7 +79,7 @@ return; } deleteClassTarget = contextMenuClass; - showDeleteClassDialog = true; + showDeleteDependenciesDialog = true; onClose(); } @@ -194,10 +194,9 @@ - diff --git a/frontend/src/lib/rendering/svelteflow/svelteFlowWrapper.svelte b/frontend/src/lib/rendering/svelteflow/svelteFlowWrapper.svelte index 01381fb6..76f69b96 100644 --- a/frontend/src/lib/rendering/svelteflow/svelteFlowWrapper.svelte +++ b/frontend/src/lib/rendering/svelteflow/svelteFlowWrapper.svelte @@ -705,17 +705,17 @@ onClose={closeContextMenus} />
@@ -732,4 +732,3 @@ resourceUuid={deleteClassTarget?.uuid} bind:showDialog={showDeleteDependenciesDialog} /> - From b6c7c049898ac8a1343ab0c055fd9d4098546bc9 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Thu, 30 Apr 2026 16:12:58 +0200 Subject: [PATCH 31/31] fixed bugs found in test: delete graph dialog overflows incorrectly delete package is not disabled for external packages when deleting via edit-menu --- frontend/src/lib/dialog/ActionDialog.svelte | 11 +++++++---- frontend/src/routes/layout/menu-bar/Edit.svelte | 11 +++++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/frontend/src/lib/dialog/ActionDialog.svelte b/frontend/src/lib/dialog/ActionDialog.svelte index 9e09f445..ab388758 100644 --- a/frontend/src/lib/dialog/ActionDialog.svelte +++ b/frontend/src/lib/dialog/ActionDialog.svelte @@ -103,13 +103,16 @@
-
-
+
+

{#if titleIcon} - + {/if} {#if title} {title} diff --git a/frontend/src/routes/layout/menu-bar/Edit.svelte b/frontend/src/routes/layout/menu-bar/Edit.svelte index cccff363..8fe20f5f 100644 --- a/frontend/src/routes/layout/menu-bar/Edit.svelte +++ b/frontend/src/routes/layout/menu-bar/Edit.svelte @@ -172,8 +172,14 @@ } const packagesJSON = await response.json(); return [ - ...(packagesJSON.internalPackageList ?? []), - ...(packagesJSON.externalPackageList ?? []), + ...(packagesJSON.internalPackageList ?? []).map(p => ({ + ...p, + external: false, + })), + ...(packagesJSON.externalPackageList ?? []).map(p => ({ + ...p, + external: true, + })), ]; } catch (error) { console.error("Failed to fetch packages", error); @@ -229,6 +235,7 @@ async function disableEditing(datasetName) { await bec.disableEditing(datasetName); } + $inspect("selectedPackageDetails: ", selectedPackageDetails);