diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CustomDatasetDiagramAllClassesRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CustomDatasetDiagramAllClassesRESTController.java index 6645c3f1..358438f8 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CustomDatasetDiagramAllClassesRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CustomDatasetDiagramAllClassesRESTController.java @@ -23,7 +23,7 @@ import org.rdfarchitect.api.controller.Response; import org.rdfarchitect.database.inmemory.diagrams.ClassInDiagram; -import org.rdfarchitect.services.diagrams.AddToDiagramUseCase; +import org.rdfarchitect.services.dl.update.classlayout.CustomDiagramLayoutUseCase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; @@ -35,6 +35,7 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; +import java.util.UUID; @RestController @RequestMapping("/api/datasets/{datasetName}/diagrams/{diagramId}/classes") @@ -44,7 +45,7 @@ public class CustomDatasetDiagramAllClassesRESTController { private static final Logger logger = LoggerFactory.getLogger(CustomDatasetDiagramAllClassesRESTController.class); - private final AddToDiagramUseCase addToDiagramUseCase; + private final CustomDiagramLayoutUseCase customDiagramLayoutUseCase; @PostMapping public String addToDiagram( @@ -66,7 +67,8 @@ public String addToDiagram( diagramId, originURL); - addToDiagramUseCase.addToDiagram(datasetName, diagramId, classes); + customDiagramLayoutUseCase.addClassesToCustomDatasetDiagram( + datasetName, UUID.fromString(diagramId), classes); logger.info( "Sending response to DELETE request: \"/api/datasets/{{}}/diagrams/{{}}/classes\" from \"{}\"", diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CustomDatasetDiagramClassRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CustomDatasetDiagramClassRESTController.java index 29bf5f2c..141b6b62 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CustomDatasetDiagramClassRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CustomDatasetDiagramClassRESTController.java @@ -22,7 +22,7 @@ import lombok.RequiredArgsConstructor; import org.rdfarchitect.api.controller.Response; -import org.rdfarchitect.services.diagrams.RemoveFromDiagramUseCase; +import org.rdfarchitect.services.dl.update.classlayout.CustomDiagramLayoutUseCase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; @@ -42,7 +42,7 @@ public class CustomDatasetDiagramClassRESTController { private static final Logger logger = LoggerFactory.getLogger(CustomDatasetDiagramClassRESTController.class); - private final RemoveFromDiagramUseCase removeFromDiagramUseCase; + private final CustomDiagramLayoutUseCase customDiagramLayoutUseCase; @DeleteMapping public String removeFromDiagram( @@ -65,7 +65,8 @@ public String removeFromDiagram( classId, originURL); - removeFromDiagramUseCase.removeFromDiagram(datasetName, diagramId, classId); + customDiagramLayoutUseCase.removeClassFromCustomDatasetDiagram( + datasetName, UUID.fromString(diagramId), classId); logger.info( "Sending response to DELETE request: \"/api/datasets/{{}}/diagrams/{{}}/classes/{{}}\" from \"{}\"", diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/ChangelogRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/ChangelogRESTController.java index 3786e284..12f7b6f6 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/ChangelogRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/ChangelogRESTController.java @@ -27,7 +27,7 @@ import org.rdfarchitect.api.dto.ChangeLogEntryDTO; import org.rdfarchitect.database.GraphIdentifier; -import org.rdfarchitect.models.changelog.GraphChangeLog; +import org.rdfarchitect.models.changelog.ChangeLog; import org.rdfarchitect.services.ChangeLogUseCase; import org.rdfarchitect.services.ExpandURIUseCase; import org.slf4j.Logger; @@ -61,7 +61,7 @@ public class ChangelogRESTController { content = @Content( mediaType = "application/json", - schema = @Schema(implementation = GraphChangeLog.class))) + schema = @Schema(implementation = ChangeLog.class))) }) @GetMapping public List getChangeLog( diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/ClassAllAssociationsRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/ClassAllAssociationsRESTController.java index 42ad9042..2aa462d6 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/ClassAllAssociationsRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/ClassAllAssociationsRESTController.java @@ -44,6 +44,7 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; +import java.util.UUID; @RestController @RequestMapping("api/datasets/{datasetName}/graphs/{graphURI}/classes/{classUUID}/associations") @@ -102,7 +103,7 @@ public String replaceAllAssociations( var graphIdentifier = new GraphIdentifier(datasetName, extendedGraphURI); updateAssociationsUseCase.replaceAllAssociations( - graphIdentifier, classUUID, associationPairList); + graphIdentifier, UUID.fromString(classUUID), associationPairList); logger.info( "Sending response to PUT request: \"/api/datasets/{{}}/graphs/{{}}/classes/{{}}/associations\" to \"{}\".", diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/ClassAllAttributesRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/ClassAllAttributesRESTController.java index 11607745..78a47aa9 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/ClassAllAttributesRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/ClassAllAttributesRESTController.java @@ -150,7 +150,8 @@ public String replaceAllAttributes( var extendedGraphURI = expandURIUseCase.expandUri(datasetName, graphURI); var graphIdentifier = new GraphIdentifier(datasetName, extendedGraphURI); - updateAttributesUseCase.replaceAllAttributes(graphIdentifier, classUUID, attributeList); + updateAttributesUseCase.replaceAllAttributes( + graphIdentifier, UUID.fromString(classUUID), attributeList); logger.info( "Sending response to PUT request: \"/api/datasets/{{}}/graphs/{{}}/classes/{{}}/attributes\" to \"{}\".", diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/ClassRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/ClassRESTController.java index 733fe67a..4d7d86d0 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/ClassRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/classes/ClassRESTController.java @@ -44,6 +44,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.UUID; + @RestController @RequestMapping("/api/datasets/{datasetName}/graphs/{graphURI}/classes/{classUUID}") @RequiredArgsConstructor @@ -190,7 +192,7 @@ public String deleteClass( var graphIdentifier = new GraphIdentifier(datasetName, graphURI); - deleteClassUseCase.deleteClass(graphIdentifier, classUUID); + deleteClassUseCase.deleteClass(graphIdentifier, UUID.fromString(classUUID)); logger.info( "Sending response to DELETE request: \"/api/datasets/{{}}/graphs/{{}}/classes/{{}}\" to \"{}\".", diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/diagrams/CustomDiagramAllClassesRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/diagrams/CustomDiagramAllClassesRESTController.java index d93e034a..de04d46d 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/diagrams/CustomDiagramAllClassesRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/diagrams/CustomDiagramAllClassesRESTController.java @@ -25,7 +25,7 @@ import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.database.inmemory.diagrams.ClassInDiagram; import org.rdfarchitect.services.ExpandURIUseCase; -import org.rdfarchitect.services.diagrams.AddToDiagramUseCase; +import org.rdfarchitect.services.dl.update.classlayout.CustomDiagramLayoutUseCase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; @@ -37,6 +37,7 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; +import java.util.UUID; @RestController @RequestMapping("/api/datasets/{datasetName}/graphs/{graphURI}/diagrams/{diagramId}/classes") @@ -48,7 +49,7 @@ public class CustomDiagramAllClassesRESTController { private final ExpandURIUseCase expandURIUseCase; - private final AddToDiagramUseCase addToDiagramUseCase; + private final CustomDiagramLayoutUseCase customDiagramLayoutUseCase; @PostMapping public String addToDiagram( @@ -77,8 +78,10 @@ public String addToDiagram( originURL); var extendedGraphURI = expandURIUseCase.expandUri(datasetName, graphURI); - addToDiagramUseCase.addToDiagram( - new GraphIdentifier(datasetName, extendedGraphURI), diagramId, classes); + customDiagramLayoutUseCase.addClassesToCustomDiagram( + new GraphIdentifier(datasetName, extendedGraphURI), + UUID.fromString(diagramId), + classes); logger.info( "Sending response to DELETE request: \"/api/datasets/{{}}/graphs/{{}}/diagrams/{{}}/classes\" from \"{}\"", diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/diagrams/CustomDiagramClassRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/diagrams/CustomDiagramClassRESTController.java index 96c938bf..4ddfc12a 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/diagrams/CustomDiagramClassRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/diagrams/CustomDiagramClassRESTController.java @@ -24,7 +24,7 @@ import org.rdfarchitect.api.controller.Response; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.services.ExpandURIUseCase; -import org.rdfarchitect.services.diagrams.RemoveFromDiagramUseCase; +import org.rdfarchitect.services.dl.update.classlayout.CustomDiagramLayoutUseCase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; @@ -47,7 +47,7 @@ public class CustomDiagramClassRESTController { private final ExpandURIUseCase expandURIUseCase; - private final RemoveFromDiagramUseCase removeFromDiagramUseCase; + private final CustomDiagramLayoutUseCase customDiagramLayoutUseCase; @DeleteMapping public String removeFromDiagram( @@ -77,8 +77,10 @@ public String removeFromDiagram( originURL); var extendedGraphURI = expandURIUseCase.expandUri(datasetName, graphURI); - removeFromDiagramUseCase.removeFromDiagram( - new GraphIdentifier(datasetName, extendedGraphURI), diagramId, classId); + customDiagramLayoutUseCase.removeClassFromCustomDiagram( + new GraphIdentifier(datasetName, extendedGraphURI), + UUID.fromString(diagramId), + classId); logger.info( "Sending response to DELETE request: \"/api/datasets/{{}}/graphs/{{}}/diagrams/{{}}/classes/{{}}\" from \"{}\"", diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/diagrams/CustomDiagramsRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/diagrams/CustomDiagramsRESTController.java index 889c8a0e..12197f7d 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/diagrams/CustomDiagramsRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/diagrams/CustomDiagramsRESTController.java @@ -29,6 +29,7 @@ import org.rdfarchitect.services.ExpandURIUseCase; import org.rdfarchitect.services.diagrams.DeleteCustomDiagramUseCase; import org.rdfarchitect.services.diagrams.ReplaceCustomDiagramUseCase; +import org.rdfarchitect.services.dl.select.FetchRenderingLayoutDataUseCase; import org.rdfarchitect.services.rendering.DiagramToCIMCollectionConverterUseCase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,6 +57,8 @@ public class CustomDiagramsRESTController { private final RenderCIMCollectionUseCase renderer; + private final FetchRenderingLayoutDataUseCase fetchRenderingLayoutDataUseCase; + private final DeleteCustomDiagramUseCase deleteCustomDiagram; private final ReplaceCustomDiagramUseCase replaceCustomDiagram; @@ -90,11 +93,11 @@ public RenderingDataDTO getDiagramRenderingData( var cimCollection = converter.convert(new GraphIdentifier(datasetName, extendedGraphURI), diagramId); - var result = - renderer.renderUML( - cimCollection, + var layoutData = + fetchRenderingLayoutDataUseCase.fetchRenderingLayoutData( new GraphIdentifier(datasetName, extendedGraphURI), UUID.fromString(diagramId)); + var result = renderer.renderUML(cimCollection, layoutData); logger.info( "Sending response to GET request: \"/api/datasets/{{}}/graphs/{{}}/diagrams/{{}}\" from \"{}\"", diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/rendering/RenderingRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/rendering/RenderingRESTController.java index 7564f78b..24f8e80e 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/rendering/RenderingRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/rendering/RenderingRESTController.java @@ -27,11 +27,9 @@ import org.rdfarchitect.api.dto.rendering.RenderingDataDTO; import org.rdfarchitect.database.GraphIdentifier; -import org.rdfarchitect.models.cim.data.dto.CIMCollection; import org.rdfarchitect.models.cim.rendering.GraphFilter; -import org.rdfarchitect.models.cim.rendering.RenderCIMCollectionUseCase; import org.rdfarchitect.services.ExpandURIUseCase; -import org.rdfarchitect.services.rendering.GraphToCIMCollectionConverterUseCase; +import org.rdfarchitect.services.GetRenderingDataUseCase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; @@ -51,9 +49,7 @@ public class RenderingRESTController { private static final Logger logger = LoggerFactory.getLogger(RenderingRESTController.class); - private final GraphToCIMCollectionConverterUseCase converter; - - private final RenderCIMCollectionUseCase renderer; + private final GetRenderingDataUseCase getRenderingDataUseCase; private final ExpandURIUseCase expandURIUseCase; @@ -101,9 +97,8 @@ public RenderingDataDTO getRenderingDataParameterized( ? UUID.fromString(filter.getPackageUUID()) : null; - CIMCollection cimCollection = converter.convert(graphIdentifier, filter); RenderingDataDTO renderingData = - renderer.renderUML(cimCollection, graphIdentifier, packageUUID); + getRenderingDataUseCase.getRenderingData(graphIdentifier, filter, packageUUID); logger.info( "Sending response to GET request \"/api/datasets/{{}}/graphs/{{}}/rendering\" to \"{}\".", diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/ChangeLogEntryDTO.java b/backend/src/main/java/org/rdfarchitect/api/dto/ChangeLogEntryDTO.java index 76e5ae0f..5669fbdf 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/ChangeLogEntryDTO.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/ChangeLogEntryDTO.java @@ -28,6 +28,5 @@ public class ChangeLogEntryDTO { private String changeId; private String timestamp; private String message; - private List additions; - private List deletions; + private List contextDeltas; } diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/ChangeLogEntryMapper.java b/backend/src/main/java/org/rdfarchitect/api/dto/ChangeLogEntryMapper.java index 93748918..491e6cc3 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/ChangeLogEntryMapper.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/ChangeLogEntryMapper.java @@ -20,27 +20,32 @@ import org.apache.jena.graph.Graph; import org.apache.jena.graph.Triple; import org.mapstruct.Mapper; -import org.mapstruct.factory.Mappers; import org.rdfarchitect.models.changelog.ChangeLogEntry; +import org.rdfarchitect.models.changelog.ContextDelta; import org.rdfarchitect.models.cim.rdf.resources.RDFA; import java.lang.ref.WeakReference; -import java.util.ArrayList; import java.util.List; @Mapper(componentModel = "spring") public interface ChangeLogEntryMapper { - ChangeLogEntryMapper INSTANCE = Mappers.getMapper(ChangeLogEntryMapper.class); - ChangeLogEntryDTO toDTO(ChangeLogEntry entry); List toDTOList(List entries); + default ContextDeltaDTO toContextDeltaDTO(ContextDelta contextDelta) { + var dto = new ContextDeltaDTO(); + dto.setContextName(contextDelta.contextName()); + dto.setAdditions(mapTriples(contextDelta.additions())); + dto.setDeletions(mapTriples(contextDelta.deletions())); + return dto; + } + default List mapTriples(WeakReference graphReference) { var graph = graphReference.get(); if (graph == null) { - return new ArrayList<>(); + return null; } return graph.stream() .filter(triple -> !triple.getPredicate().equals(RDFA.uuid.asNode())) diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/ContextDeltaDTO.java b/backend/src/main/java/org/rdfarchitect/api/dto/ContextDeltaDTO.java new file mode 100644 index 00000000..e5b1d832 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/dto/ContextDeltaDTO.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; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +public class ContextDeltaDTO { + private String contextName; + private List additions; + private List deletions; +} diff --git a/backend/src/main/java/org/rdfarchitect/config/InMemoryDatabaseConfig.java b/backend/src/main/java/org/rdfarchitect/config/InMemoryDatabaseConfig.java index efc25dda..d3d605ae 100644 --- a/backend/src/main/java/org/rdfarchitect/config/InMemoryDatabaseConfig.java +++ b/backend/src/main/java/org/rdfarchitect/config/InMemoryDatabaseConfig.java @@ -26,7 +26,7 @@ public class InMemoryDatabaseConfig { @Bean - public InMemoryDatabase createInMemoryDatabase() { - return new InMemoryDatabaseImpl(); + public InMemoryDatabase createInMemoryDatabase(SchemaConfig schemaConfig) { + return new InMemoryDatabaseImpl(schemaConfig); } } diff --git a/backend/src/main/java/org/rdfarchitect/config/PortConfig.java b/backend/src/main/java/org/rdfarchitect/config/PortConfig.java index b6cc39cb..fe55faf0 100644 --- a/backend/src/main/java/org/rdfarchitect/config/PortConfig.java +++ b/backend/src/main/java/org/rdfarchitect/config/PortConfig.java @@ -30,8 +30,8 @@ public class PortConfig { @Bean - public DatabasePort databasePort(InMemoryDatabase inMemoryDatabase, SchemaConfig schemaConfig) { - return new InMemoryDatabaseAdapter(inMemoryDatabase, schemaConfig); + public DatabasePort databasePort(InMemoryDatabase inMemoryDatabase) { + return new InMemoryDatabaseAdapter(inMemoryDatabase); } @Bean diff --git a/backend/src/main/java/org/rdfarchitect/config/RenderingConfig.java b/backend/src/main/java/org/rdfarchitect/config/RenderingConfig.java index 0972b6a2..9f8059b7 100644 --- a/backend/src/main/java/org/rdfarchitect/config/RenderingConfig.java +++ b/backend/src/main/java/org/rdfarchitect/config/RenderingConfig.java @@ -21,7 +21,6 @@ import org.rdfarchitect.models.cim.rendering.mermaid.RenderCIMCollectionMermaidService; import org.rdfarchitect.models.cim.rendering.svelteflow.RenderCIMCollectionSvelteFlowService; import org.rdfarchitect.services.dl.select.FetchRenderingLayoutDataUseCase; -import org.rdfarchitect.services.dl.update.EnsureDiagramLayoutForCIMCollectionUseCase; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -34,13 +33,10 @@ public class RenderingConfig { @Bean public RenderCIMCollectionUseCase renderingPort( - FetchRenderingLayoutDataUseCase fetchRenderingLayoutDataUseCase, - EnsureDiagramLayoutForCIMCollectionUseCase ensureDiagramLayoutForCIMCollectionUseCase) { + FetchRenderingLayoutDataUseCase fetchRenderingLayoutDataUseCase) { return switch (rendererType) { case "svelteflow" -> - new RenderCIMCollectionSvelteFlowService( - fetchRenderingLayoutDataUseCase, - ensureDiagramLayoutForCIMCollectionUseCase); + new RenderCIMCollectionSvelteFlowService(fetchRenderingLayoutDataUseCase); case "mermaid" -> new RenderCIMCollectionMermaidService(); default -> new RenderCIMCollectionMermaidService(); }; diff --git a/backend/src/main/java/org/rdfarchitect/database/DatabasePort.java b/backend/src/main/java/org/rdfarchitect/database/DatabasePort.java index e3def0eb..6598db51 100644 --- a/backend/src/main/java/org/rdfarchitect/database/DatabasePort.java +++ b/backend/src/main/java/org/rdfarchitect/database/DatabasePort.java @@ -19,7 +19,6 @@ import org.apache.jena.graph.Graph; import org.apache.jena.shared.PrefixMapping; -import org.rdfarchitect.database.inmemory.GraphWithContext; import org.rdfarchitect.database.inmemory.diagrams.CustomDiagram; import org.rdfarchitect.rdf.graph.wrapper.DiagramLayout; @@ -30,12 +29,12 @@ public interface DatabasePort { /** - * Get a {@link GraphWithContext} from the database. + * Get a {@link GraphContext} for the specified graph. * * @param graphIdentifier The identifier of the graph. - * @return {@link GraphWithContext} + * @return {@link GraphContext} */ - GraphWithContext getGraphWithContext(GraphIdentifier graphIdentifier); + GraphContext getGraphWithContext(GraphIdentifier graphIdentifier); /** * Get all {@link CustomDiagram} for a dataset. @@ -86,44 +85,6 @@ public interface DatabasePort { */ void createEmptyGraph(GraphIdentifier graphIdentifier); - /** - * Indicates whether there is a forward change that can be re-applied on the graph. - * - * @param graphIdentifier identifies dataset and graph URI - * @return {@code true} if redo is possible, otherwise {@code false} - */ - Boolean canRedo(GraphIdentifier graphIdentifier); - - /** - * Indicates whether there is a previous change that can be undone on the graph. - * - * @param graphIdentifier identifies dataset and graph URI - * @return {@code true} if undo is possible, otherwise {@code false} - */ - Boolean canUndo(GraphIdentifier graphIdentifier); - - /** - * Re-applies the next change in the graph history. - * - * @param graphIdentifier identifies dataset and graph URI - */ - void redo(GraphIdentifier graphIdentifier); - - /** - * Reverts the latest change recorded in the graph history. - * - * @param graphIdentifier identifies dataset and graph URI - */ - void undo(GraphIdentifier graphIdentifier); - - /** - * Restores a historic version of the graph identified by {@code graphIdentifier}. - * - * @param graphIdentifier identifies dataset and graph URI - * @param versionId historic version to restore - */ - void restore(GraphIdentifier graphIdentifier, UUID versionId); - /** * Lists all graph URIs belonging to the dataset. * diff --git a/backend/src/main/java/org/rdfarchitect/database/GraphContext.java b/backend/src/main/java/org/rdfarchitect/database/GraphContext.java new file mode 100644 index 00000000..2a2b0a47 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/database/GraphContext.java @@ -0,0 +1,48 @@ +/* + * 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.database; + +import org.apache.jena.graph.Graph; +import org.apache.jena.query.ReadWrite; +import org.rdfarchitect.database.inmemory.diagrams.CustomDiagram; +import org.rdfarchitect.models.changelog.ChangeLog; +import org.rdfarchitect.rdf.graph.wrapper.DiagramLayoutDelta; +import org.rdfarchitect.rdf.graph.wrapper.RDFGraphDelta; + +import java.util.Map; +import java.util.UUID; + +/** + * Transactional access to the RDF graph, diagram layout, and custom SHACL data for a single named + * graph. Extends {@link Transactional} so it can be used in try-with-resources. + */ +public interface GraphContext extends Transactional, VersionControl { + + @Override + GraphContext begin(ReadWrite mode); + + Graph getRdfGraph(); + + DiagramLayoutDelta getDiagramLayout(); + + RDFGraphDelta getCustomSHACL(); + + ChangeLog getChangeLog(); + + Map getCustomDiagrams(); +} diff --git a/backend/src/main/java/org/rdfarchitect/database/Transactional.java b/backend/src/main/java/org/rdfarchitect/database/Transactional.java new file mode 100644 index 00000000..9307d743 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/database/Transactional.java @@ -0,0 +1,44 @@ +/* + * 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.database; + +import org.apache.jena.query.ReadWrite; + +/** + * Transaction lifecycle contract. Implementations must be usable in try-with-resources: {@link + * #begin(ReadWrite)} returns {@code this}, and {@link #close()} calls {@link #end()}. + */ +public interface Transactional extends AutoCloseable { + + Transactional begin(ReadWrite mode); + + void commit(); + + void commit(String message); + + void abort(); + + void end(); + + boolean isInTransaction(); + + ReadWrite transactionMode(); + + @Override + void close(); +} diff --git a/backend/src/main/java/org/rdfarchitect/database/VersionControl.java b/backend/src/main/java/org/rdfarchitect/database/VersionControl.java new file mode 100644 index 00000000..fe0a7e09 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/database/VersionControl.java @@ -0,0 +1,35 @@ +/* + * 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.database; + +import org.rdfarchitect.models.changelog.ChangeLogEntry; + +import java.util.UUID; + +public interface VersionControl { + + ChangeLogEntry undo(); + + ChangeLogEntry redo(); + + boolean canUndo(); + + boolean canRedo(); + + void restoreToVersion(UUID versionId); +} diff --git a/backend/src/main/java/org/rdfarchitect/database/inmemory/GraphWithContext.java b/backend/src/main/java/org/rdfarchitect/database/inmemory/GraphWithContext.java deleted file mode 100644 index 7b1fcded..00000000 --- a/backend/src/main/java/org/rdfarchitect/database/inmemory/GraphWithContext.java +++ /dev/null @@ -1,49 +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.database.inmemory; - -import lombok.Getter; -import lombok.Setter; - -import org.apache.jena.rdf.model.Model; -import org.rdfarchitect.database.inmemory.diagrams.CustomDiagram; -import org.rdfarchitect.rdf.graph.wrapper.DiagramLayout; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; - -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Wrapper class that combines an {@link GraphRewindableWithUUIDs} with its associated {@link - * DiagramLayout}. - */ -public class GraphWithContext { - - @Getter private final GraphRewindableWithUUIDs rdfGraph; - - @Getter - private final ConcurrentHashMap customDiagrams = new ConcurrentHashMap<>(); - - @Getter @Setter private DiagramLayout diagramLayout = new DiagramLayout(); - - @Getter @Setter private Model customSHACL; - - public GraphWithContext(GraphRewindableWithUUIDs rdfGraph) { - this.rdfGraph = rdfGraph; - } -} diff --git a/backend/src/main/java/org/rdfarchitect/database/inmemory/GraphWithContextCollection.java b/backend/src/main/java/org/rdfarchitect/database/inmemory/GraphWithContextCollection.java index 6259d50f..d66abfd4 100644 --- a/backend/src/main/java/org/rdfarchitect/database/inmemory/GraphWithContextCollection.java +++ b/backend/src/main/java/org/rdfarchitect/database/inmemory/GraphWithContextCollection.java @@ -23,30 +23,27 @@ import org.apache.jena.graph.Graph; import org.apache.jena.query.Dataset; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.rdf.model.Resource; import org.apache.jena.shared.PrefixMapping; import org.apache.jena.shared.impl.PrefixMappingImpl; -import org.apache.jena.sparql.core.Transactional; import org.apache.jena.sparql.graph.GraphFactory; import org.apache.jena.sparql.graph.PrefixMappingReadOnly; -import org.rdfarchitect.config.GraphCompressionConfig; import org.rdfarchitect.database.inmemory.diagrams.CustomDiagram; import org.rdfarchitect.rdf.RDFUtils; import org.rdfarchitect.rdf.graph.wrapper.DiagramLayout; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; import java.util.Iterator; import java.util.List; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; /** - * This class is a collection of {@link GraphWithContext graphs with context} It also contains a - * universal {@link PrefixMapping}. The goal is to mimic the structure of a {@link - * org.apache.jena.sparql.core.DatasetGraph DatasetGraph}, but only access each Graph individually. + * Collection of {@link GraphWithContextTransactional} objects with a shared {@link PrefixMapping}. + * Mimics the structure of a {@link org.apache.jena.sparql.core.DatasetGraph DatasetGraph} but + * exposes each graph individually. */ @NoArgsConstructor public class GraphWithContextCollection { @@ -55,94 +52,69 @@ public class GraphWithContextCollection { @Setter @Getter private volatile boolean isReadOnly = true; - private final int maxVersions = GraphCompressionConfig.getMaxVersions(); - private final int compressCount = GraphCompressionConfig.getCompressCount(); + private final ConcurrentMap graphs = + new ConcurrentHashMap<>(); - // holds graphs - private final ConcurrentMap graphs = new ConcurrentHashMap<>(); + private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); @Getter private final ConcurrentMap customDiagrams = new ConcurrentHashMap<>(); @Getter private final DiagramLayout diagramLayout = new DiagramLayout(); - // lock to prohibit dirty reads/writes - private final ReentrantLock lock = new ReentrantLock(); - - // universal prefix map for all graphs private final PrefixMapping prefixes = new PrefixMappingImpl(); + @SuppressWarnings("java:S2093") public GraphWithContextCollection(Dataset dataset) { - lock.lock(); + rwLock.writeLock().lock(); try { this.prefixes.setNsPrefixes(dataset.getPrefixMapping()); if (!dataset.getDefaultModel().isEmpty()) { - var rdfGraph = - new GraphRewindableWithUUIDs( - dataset.getDefaultModel().getGraph(), maxVersions, compressCount); - var graph = new GraphWithContext(rdfGraph); - graphs.put(DEFAULT_GRAPH_NAME, graph); + graphs.put( + DEFAULT_GRAPH_NAME, + new GraphWithContextTransactional(dataset.getDefaultModel().getGraph())); } for (Iterator it = dataset.listModelNames(); it.hasNext(); ) { var graphURI = it.next().getURI(); - var rdfGraph = - new GraphRewindableWithUUIDs( - dataset.getNamedModel(graphURI).getGraph(), - maxVersions, - compressCount); - var graph = new GraphWithContext(rdfGraph); - graphs.put(graphURI, graph); + graphs.put( + graphURI, + new GraphWithContextTransactional( + dataset.getNamedModel(graphURI).getGraph())); } } finally { - lock.unlock(); + rwLock.writeLock().unlock(); } } /** - * Begin a transaction on a {@link GraphRewindableWithUUIDs}. + * Returns the {@link GraphWithContextTransactional} identified by {@code graphUri}. * - * @param graphUri The GraphUri to identify the Graph. - * @param txnType The transactionType - * @return The {@link GraphRewindableWithUUIDs} the {@link Transactional Transaction} is - * performed on. - * @throws IllegalArgumentException if the graphUri is not a valid URI. + * @param graphUri the graph URI + * @return the {@link GraphWithContextTransactional} */ - public GraphRewindableWithUUIDs begin(String graphUri, TxnType txnType) { - lock.lock(); + public GraphWithContextTransactional getGraphWithContext(String graphUri) { + // Try read lock first — sufficient if the graph already exists. + rwLock.readLock().lock(); try { - graphUri = prefixes.expandPrefix(graphUri); - createGraphIfNonExistent(graphUri); - var rdfGraph = graphs.get(graphUri).getRdfGraph(); - rdfGraph.begin(txnType); - return rdfGraph; + var expanded = prefixes.expandPrefix(graphUri); + var existing = graphs.get(expanded); + if (existing != null) { + return existing; + } } finally { - lock.unlock(); + rwLock.readLock().unlock(); } - } - - /** - * Gets a {@link GraphWithContext} from the collection. - * - * @param graphUri The GraphUri to identify the GraphWithContext. - * @return The {@link GraphWithContext}. - */ - public GraphWithContext getGraphWithContext(String graphUri) { - lock.lock(); + // Graph not found — upgrade to write lock to create the default graph if applicable. + rwLock.writeLock().lock(); try { graphUri = prefixes.expandPrefix(graphUri); createGraphIfNonExistent(graphUri); return graphs.get(graphUri); } finally { - lock.unlock(); + rwLock.writeLock().unlock(); } } - /** - * Creates a new {@link GraphRewindableWithUUIDs} in the collection. If the {@link - * GraphRewindableWithUUIDs} already exists nothing happens. - * - * @param graphUri The GraphUri to identify the Graph. - */ private void createGraphIfNonExistent(String graphUri) { graphUri = prefixes.expandPrefix(graphUri); assertValidGraphName(graphUri); @@ -150,243 +122,107 @@ private void createGraphIfNonExistent(String graphUri) { if (!graphUri.equals(DEFAULT_GRAPH_NAME)) { throw new IllegalArgumentException("Graph URI " + graphUri + " does not exist."); } - var rdfGraph = - new GraphRewindableWithUUIDs( - GraphFactory.createDefaultGraph(), maxVersions, compressCount); - var graph = new GraphWithContext(rdfGraph); - graphs.put(graphUri, graph); + graphs.put( + DEFAULT_GRAPH_NAME, + new GraphWithContextTransactional(GraphFactory.createDefaultGraph())); } } /** - * Creates a new {@link GraphRewindableWithUUIDs} and immediately starts a {@link TxnType - * TxnType.WRITE} transaction on it. + * Creates a new {@link GraphWithContextTransactional} for {@code newGraph} and registers it + * under {@code graphUri}. * - * @param graphUri The GraphUri to identify the Graph. - * @param newGraph The new Graph. - * @throws IllegalArgumentException if the graphUri is not a valid URI. + * @param graphUri the graph URI + * @param newGraph initial graph content */ public void create(String graphUri, Graph newGraph) { - lock.lock(); - try { + rwLock.writeLock().lock(); + try (var ctx = new GraphWithContextTransactional(newGraph).begin(ReadWrite.WRITE)) { graphUri = prefixes.expandPrefix(graphUri); assertValidGraphName(graphUri); - var rdfGraph = new GraphRewindableWithUUIDs(newGraph, maxVersions, compressCount); - var graph = new GraphWithContext(rdfGraph); - graphs.put(graphUri, graph); + var existing = graphs.put(graphUri, ctx); + if (existing != null) { + try (var old = existing.begin(ReadWrite.WRITE)) { + old.getRdfGraph().close(); + } + } } finally { - lock.unlock(); + rwLock.writeLock().unlock(); } } - /** Closes all {@link GraphRewindableWithUUIDs GraphRewindables} and releases all resources. */ + /** Closes all graphs and clears the collection. */ public void clear() { - lock.lock(); + rwLock.writeLock().lock(); try { graphs.values() .forEach( - graph -> { - var rdfGraph = graph.getRdfGraph(); - rdfGraph.begin(TxnType.WRITE); - rdfGraph.close(); - rdfGraph.end(); + ctx -> { + try { + ctx.begin(ReadWrite.WRITE); + ctx.getRdfGraph().close(); + } finally { + ctx.end(); + } }); graphs.clear(); } finally { - lock.unlock(); + rwLock.writeLock().unlock(); } } /** - * Removes a {@link GraphRewindableWithUUIDs}. + * Removes and closes the {@link GraphWithContextTransactional} identified by {@code graphUri}. * - * @param graphUri The GraphUri to identify the Graph. - * @throws IllegalArgumentException if the graphUri is not a valid URI. + * @param graphUri the graph URI */ + @SuppressWarnings("resource") public void remove(String graphUri) { - lock.lock(); + rwLock.writeLock().lock(); try { graphUri = prefixes.expandPrefix(graphUri); assertValidGraphName(graphUri); - if (!containsGraph(graphUri)) { + if (!graphs.containsKey(graphUri)) { return; } - var rdfGraph = graphs.get(graphUri).getRdfGraph(); - rdfGraph.begin(TxnType.WRITE); - rdfGraph.close(); - rdfGraph.end(); - graphs.remove(graphUri); - } finally { - lock.unlock(); - } - } - - /** - * Checks Whether {@link GraphWithContextCollection this} contains a certain {@link - * GraphRewindableWithUUIDs} - * - * @param graphUri The GraphUri to identify the Graph. - * @return True if {@link GraphWithContextCollection this} contains the {@link - * GraphRewindableWithUUIDs}, otherwise false - * @throws IllegalArgumentException if the graphUri is not a valid URI. - */ - public boolean containsGraph(String graphUri) { - lock.lock(); - try { - graphUri = prefixes.expandPrefix(graphUri); - assertValidGraphName(graphUri); - return graphs.containsKey(graphUri); + try (var ctx = graphs.get(graphUri).begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().close(); + graphs.remove(graphUri); + } } finally { - lock.unlock(); + rwLock.writeLock().unlock(); } } /** - * Lists all names of the {@link GraphRewindableWithUUIDs}. - * - * @return List with all graph uris + * Returns a snapshot of the current graph URIs. The result may be stale by the time the caller + * inspects it. */ public List listGraphUris() { - lock.lock(); + rwLock.readLock().lock(); try { return graphs.keySet().stream().toList(); } finally { - lock.unlock(); + rwLock.readLock().unlock(); } } - /** - * Lists the stored Prefixes of {@link GraphWithContextCollection this} - * - * @return {@link PrefixMappingReadOnly} - */ public PrefixMappingReadOnly getPrefixMapping() { - lock.lock(); + rwLock.readLock().lock(); try { return new PrefixMappingReadOnly(prefixes); } finally { - lock.unlock(); + rwLock.readLock().unlock(); } } - /** - * Replace the entire {@link PrefixMapping} of {@link GraphWithContextCollection this}. The - * internal {@link PrefixMapping} does not reference the given one to prevent external - * modification. - * - * @param newPrefixMapping the new {@link PrefixMapping}. - */ public void setPrefixMapping(PrefixMapping newPrefixMapping) { - lock.lock(); + rwLock.writeLock().lock(); try { this.prefixes.clearNsPrefixMap(); this.prefixes.setNsPrefixes(newPrefixMapping); } finally { - lock.unlock(); - } - } - - /** - * Undoes the last commit on a {@link GraphRewindableWithUUIDs}. - * - * @param graphUri The GraphUri to identify the Graph. - * @throws IllegalArgumentException if the graphUri is not a valid URI or if the graph doesn't - * exist. - * @throws org.rdfarchitect.exception.graph.GraphVersionControlException if the graph is already - * at the first version. - */ - public void undo(String graphUri) { - lock.lock(); - try { - graphUri = prefixes.expandPrefix(graphUri); - assertValidGraphName(graphUri); - assertThatGraphExists(graphUri); - graphs.get(graphUri).getRdfGraph().undo(); - } finally { - lock.unlock(); - } - } - - /** - * Redoes the last undone commit on a {@link GraphRewindableWithUUIDs}. - * - * @param graphUri The GraphUri to identify the Graph. - * @throws IllegalArgumentException if the graphUri is not a valid URI or if the graph doesn't - * exist. - * @throws org.rdfarchitect.exception.graph.GraphVersionControlException if the graph is already - * at the last version. - */ - public void redo(String graphUri) { - lock.lock(); - try { - graphUri = prefixes.expandPrefix(graphUri); - assertValidGraphName(graphUri); - assertThatGraphExists(graphUri); - graphs.get(graphUri).getRdfGraph().redo(); - } finally { - lock.unlock(); - } - } - - /** - * Checks if an undo is possible on a {@link GraphRewindableWithUUIDs}. - * - * @param graphUri The GraphUri to identify the Graph. - * @return True if an undo is possible, otherwise false - */ - public boolean canUndo(String graphUri) { - lock.lock(); - try { - graphUri = prefixes.expandPrefix(graphUri); - assertValidGraphName(graphUri); - assertThatGraphExists(graphUri); - return graphs.get(graphUri).getRdfGraph().canUndo(); - } finally { - lock.unlock(); - } - } - - /** - * Checks if a redo is possible on a {@link GraphRewindableWithUUIDs}. - * - * @param graphUri The GraphUri to identify the Graph. - * @return True if a redo is possible, otherwise false - */ - public boolean canRedo(String graphUri) { - lock.lock(); - try { - graphUri = prefixes.expandPrefix(graphUri); - assertValidGraphName(graphUri); - assertThatGraphExists(graphUri); - return graphs.get(graphUri).getRdfGraph().canRedo(); - } finally { - lock.unlock(); - } - } - - /** - * Restores a graph to a specific version identified by its UUID. - * - * @param graphUri The GraphUri to identify the Graph. - * @param versionId The UUID of the version to restore. - * @throws IllegalArgumentException if the graphUri is not a valid URI or if the graph doesn't - * exist. - */ - public void restore(String graphUri, UUID versionId) { - lock.lock(); - try { - graphUri = prefixes.expandPrefix(graphUri); - assertValidGraphName(graphUri); - assertThatGraphExists(graphUri); - graphs.get(graphUri).getRdfGraph().restore(versionId); - } finally { - lock.unlock(); - } - } - - private void assertThatGraphExists(String graphUri) { - if (!containsGraph(graphUri)) { - throw new IllegalArgumentException("Graph URI " + graphUri + " does not exist"); + rwLock.writeLock().unlock(); } } diff --git a/backend/src/main/java/org/rdfarchitect/database/inmemory/GraphWithContextTransactional.java b/backend/src/main/java/org/rdfarchitect/database/inmemory/GraphWithContextTransactional.java new file mode 100644 index 00000000..74fcf42d --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/database/inmemory/GraphWithContextTransactional.java @@ -0,0 +1,405 @@ +/* + * 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.database.inmemory; + +import org.apache.jena.graph.Graph; +import org.apache.jena.query.ReadWrite; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.sparql.graph.GraphFactory; +import org.rdfarchitect.config.GraphCompressionConfig; +import org.rdfarchitect.database.GraphContext; +import org.rdfarchitect.database.inmemory.diagrams.CustomDiagram; +import org.rdfarchitect.exception.graph.GraphNotInATransactionException; +import org.rdfarchitect.exception.graph.GraphTransactionException; +import org.rdfarchitect.exception.graph.GraphVersionControlException; +import org.rdfarchitect.models.changelog.ChangeLog; +import org.rdfarchitect.models.changelog.ChangeLogEntry; +import org.rdfarchitect.models.changelog.ContextDelta; +import org.rdfarchitect.models.cim.CIMModifyingUtils; +import org.rdfarchitect.rdf.graph.GraphUtils; +import org.rdfarchitect.rdf.graph.wrapper.DiagramLayoutDelta; +import org.rdfarchitect.rdf.graph.wrapper.RDFGraphDelta; +import org.rdfarchitect.rdf.graph.wrapper.Rewindable; +import org.rdfarchitect.rdf.graph.wrapper.TransactionContext; +import org.rdfarchitect.rdf.graph.wrapper.TransactionParticipant; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Top-level context object that holds an {@link RDFGraphDelta} and a {@link DiagramLayoutDelta} + * under a single SWMR lock and a shared {@link TransactionContext}. + * + *

This is the only authorised entry point for transactions. Callers must call {@link + * #begin(ReadWrite)} here; the two inner components have no lock of their own and rely on this + * class to manage synchronisation. + */ +public class GraphWithContextTransactional implements GraphContext { + + private static final Logger logger = + LoggerFactory.getLogger(GraphWithContextTransactional.class); + + private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); + private final TransactionContext txnContext = new TransactionContext(); + + private final RDFGraphDelta rdfGraph; + private final DiagramLayoutDelta diagramLayout; + private final RDFGraphDelta customSHACL; + private final ChangeLog changeLog = new ChangeLog(txnContext); + private final ConcurrentHashMap customDiagrams = new ConcurrentHashMap<>(); + + /** The fixed participants that are always part of every transaction. */ + private final List graphParticipants; + + private final List coreRewindables; + private final AtomicInteger stepsSinceNamedCommit = new AtomicInteger(0); + + private record NamedRewindable(String name, Rewindable rewindable) {} + + public GraphWithContextTransactional(Graph base) { + txnContext.begin(ReadWrite.WRITE); + int maxVersions = GraphCompressionConfig.getMaxVersions(); + int compressCount = GraphCompressionConfig.getCompressCount(); + GraphUtils.enhanceWithUUIDs(base); + CIMModifyingUtils.replaceCommentDatatype(base); + this.rdfGraph = + new RDFGraphDelta( + GraphFactory.createDefaultGraph(), maxVersions, compressCount, txnContext); + var rdfModel = ModelFactory.createModelForGraph(rdfGraph); + rdfModel.setNsPrefixes(base.getPrefixMapping()); + rdfModel.add(ModelFactory.createModelForGraph(base)); + this.diagramLayout = new DiagramLayoutDelta(txnContext); + this.customSHACL = + new RDFGraphDelta( + GraphFactory.createDefaultGraph(), maxVersions, compressCount, txnContext); + this.graphParticipants = List.of(rdfGraph, customSHACL); + this.coreRewindables = + List.of( + new NamedRewindable("rdf", rdfGraph), + new NamedRewindable("shacl", customSHACL)); + commit("imported graph"); + txnContext.end(); + } + + // ------------------------------------------------------------------------- + // Helpers — build the effective participant / rewindable lists + // ------------------------------------------------------------------------- + + private List allParticipants() { + var all = new ArrayList(graphParticipants); + all.add(diagramLayout); + all.add(changeLog); + return all; + } + + /** + * Returns the core rewindable components. Custom diagrams are intentionally excluded: their + * state is not tracked in the changelog and is not undoable. + */ + private List allRewindables() { + return coreRewindables; + } + + // ------------------------------------------------------------------------- + // GraphContext methods + // ------------------------------------------------------------------------- + + @Override + public RDFGraphDelta getRdfGraph() { + return rdfGraph; + } + + @Override + public DiagramLayoutDelta getDiagramLayout() { + return diagramLayout; + } + + @Override + public RDFGraphDelta getCustomSHACL() { + return customSHACL; + } + + @Override + public ChangeLog getChangeLog() { + return changeLog; + } + + @Override + public Map getCustomDiagrams() { + return customDiagrams; + } + + // ------------------------------------------------------------------------- + // Transactional methods + // ------------------------------------------------------------------------- + + @Override + public GraphWithContextTransactional begin(ReadWrite mode) { + if (txnContext.isInTransaction()) { + throw new GraphTransactionException("A transaction is already active on this thread."); + } + txnContext.begin(mode); + switch (mode) { + case READ -> rwLock.readLock().lock(); + case WRITE -> rwLock.writeLock().lock(); + } + if (mode == ReadWrite.WRITE) { + customDiagrams.values().forEach(CustomDiagram::beginTransaction); + } + return this; + } + + @Override + public void commit() { + if (!isInTransaction()) { + throw new GraphNotInATransactionException(); + } + if (txnContext.transactionMode() == ReadWrite.READ) { + throw new GraphTransactionException("Cannot commit a read transaction."); + } + GraphUtils.enhanceWithUUIDs(rdfGraph); + changeLog.clearRedo(); + if (graphParticipants.stream().anyMatch(TransactionParticipant::hasChanges)) { + graphParticipants.forEach(TransactionParticipant::commit); + stepsSinceNamedCommit.incrementAndGet(); + } + diagramLayout.commit(); + changeLog.commit(); + customDiagrams.values().forEach(CustomDiagram::commit); + logger.debug("Context committed."); + } + + @Override + public void commit(String message) { + if (!isInTransaction()) { + throw new GraphNotInATransactionException(); + } + if (txnContext.transactionMode() == ReadWrite.READ) { + throw new GraphTransactionException("Cannot commit a read transaction."); + } + GraphUtils.enhanceWithUUIDs(rdfGraph); + changeLog.clearRedo(); + + // Commit graph participants to capture their deltas. + rdfGraph.commit(); + diagramLayout.commit(); + customSHACL.commit(); + customDiagrams.values().forEach(CustomDiagram::commit); + stepsSinceNamedCommit.incrementAndGet(); + int steps = stepsSinceNamedCommit.get(); + stepsSinceNamedCommit.set(0); + var contextDeltas = + coreRewindables.stream() + .map( + nr -> { + var delta = nr.rewindable().getLastDelta(); + return new ContextDelta( + nr.name(), + new WeakReference<>(delta.getAdditions()), + new WeakReference<>(delta.getDeletions())); + }) + .toList(); + changeLog.push(new ChangeLogEntry(message, steps, contextDeltas)); + changeLog.commit(); + logger.debug("Context committed with message: {}", message); + } + + private ChangeLogEntry applyHistoryStep( + String noHistoryMessage, + String inTransactionMessage, + java.util.function.BooleanSupplier canApply, + java.util.function.Supplier peekEntry, + Runnable bufferMove, + java.util.function.Consumer action) { + if (isInTransaction()) { + throw new GraphTransactionException(inTransactionMessage); + } + rwLock.writeLock().lock(); + try { + txnContext.begin(ReadWrite.WRITE); + try { + if (!canApply.getAsBoolean()) { + throw new GraphVersionControlException(noHistoryMessage); + } + var entry = peekEntry.get(); + bufferMove.run(); + changeLog.commit(); + for (int i = 0; i < entry.getSteps(); i++) { + allRewindables().forEach(nr -> action.accept(nr.rewindable())); + } + stepsSinceNamedCommit.set(0); + return entry; + } finally { + txnContext.end(); + } + } finally { + rwLock.writeLock().unlock(); + } + } + + @Override + public ChangeLogEntry undo() { + var entry = + applyHistoryStep( + "Cannot undo: no history available.", + "Cannot undo while a transaction is active.", + this::canUndoUnchecked, + changeLog::peekUndo, + changeLog::moveToRedo, + Rewindable::undo); + logger.debug("Context undone."); + return entry; + } + + @Override + public ChangeLogEntry redo() { + var entry = + applyHistoryStep( + "Cannot redo: no future history available.", + "Cannot redo while a transaction is active.", + this::canRedoUnchecked, + changeLog::peekRedo, + changeLog::moveToUndo, + Rewindable::redo); + logger.debug("Context redone."); + return entry; + } + + @Override + public boolean canUndo() { + return changeLog.canUndo(); + } + + @Override + public boolean canRedo() { + return changeLog.canRedo(); + } + + private boolean canUndoUnchecked() { + return changeLog.canUndo(); + } + + private boolean canRedoUnchecked() { + return changeLog.canRedo(); + } + + @Override + public void restoreToVersion(UUID versionId) { + if (isInTransaction()) { + throw new GraphTransactionException( + "Cannot restore version while a transaction is active."); + } + rwLock.writeLock().lock(); + try { + txnContext.begin(ReadWrite.WRITE); + try { + var undoHistory = changeLog.getUndoHistory(); + boolean found = + undoHistory.stream() + .anyMatch(entry -> entry.getChangeId().equals(versionId)); + if (!found) { + throw new GraphVersionControlException( + "Version " + versionId + " not found in undo history."); + } + while (canUndoUnchecked()) { + var top = changeLog.peekUndo(); + if (top.getChangeId().equals(versionId)) { + break; + } + changeLog.moveToRedo(); + changeLog.commit(); + for (int i = 0; i < top.getSteps(); i++) { + allRewindables().forEach(nr -> nr.rewindable().undo()); + } + } + stepsSinceNamedCommit.set(0); + logger.debug("Restored to version {}.", versionId); + } finally { + txnContext.end(); + } + } finally { + rwLock.writeLock().unlock(); + } + } + + @Override + public void abort() { + if (!isInTransaction()) { + throw new GraphNotInATransactionException(); + } + if (txnContext.transactionMode() == ReadWrite.READ) { + throw new GraphTransactionException("Cannot abort a read transaction."); + } + allParticipants().forEach(TransactionParticipant::abort); + customDiagrams.values().forEach(CustomDiagram::abort); + logger.debug("Context aborted."); + } + + @Override + public void end() { + if (!isInTransaction()) { + throw new GraphNotInATransactionException(); + } + if (txnContext.transactionMode() == ReadWrite.WRITE + && !rdfGraph.isClosed() + && allParticipants().stream().anyMatch(TransactionParticipant::hasChanges)) { + logger.warn("Ending write transaction with uncommitted changes — aborting."); + allParticipants().forEach(TransactionParticipant::abort); + customDiagrams.values().forEach(CustomDiagram::abort); + } + var lock = + txnContext.transactionMode() == ReadWrite.READ + ? rwLock.readLock() + : rwLock.writeLock(); + lock.unlock(); + txnContext.end(); + logger.debug("Context transaction ended."); + } + + @Override + public boolean isInTransaction() { + return txnContext.isInTransaction(); + } + + @Override + public ReadWrite transactionMode() { + if (!isInTransaction()) { + throw new GraphNotInATransactionException(); + } + return txnContext.transactionMode(); + } + + // ------------------------------------------------------------------------- + // AutoClosable method + // ------------------------------------------------------------------------- + + @Override + public void close() { + if (isInTransaction()) { + end(); + } + } +} diff --git a/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabase.java b/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabase.java index 7e6e997e..78c4ab59 100644 --- a/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabase.java +++ b/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabase.java @@ -18,16 +18,14 @@ package org.rdfarchitect.database.inmemory; import org.apache.jena.graph.Graph; -import org.apache.jena.query.TxnType; import org.apache.jena.shared.PrefixMapping; -import org.apache.jena.sparql.core.Transactional; import org.apache.jena.sparql.graph.PrefixMappingReadOnly; import org.rdfarchitect.database.DatabaseConnection; +import org.rdfarchitect.database.GraphContext; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.database.inmemory.diagrams.CustomDiagram; import org.rdfarchitect.exception.database.DataAccessException; import org.rdfarchitect.rdf.graph.wrapper.DiagramLayout; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; import java.util.List; import java.util.Map; @@ -51,23 +49,12 @@ public interface InMemoryDatabase { List listDatasets(); /** - * Begin a transaction on a {@link GraphRewindableWithUUIDs}. - * - * @param graphIdentifier The identifier of the graph, which includes the dataset name and the - * graph URI. - * @param txnType The transactionType - * @return The {@link GraphRewindableWithUUIDs} the {@link Transactional Transaction} is - * performed on. - */ - GraphRewindableWithUUIDs begin(GraphIdentifier graphIdentifier, TxnType txnType); - - /** - * Get a {@link GraphWithContext} from the database. + * Get a {@link GraphContext} for the specified graph. * * @param graphIdentifier The identifier of the graph. - * @return {@link GraphWithContext} + * @return {@link GraphContext} */ - GraphWithContext getGraphWithContext(GraphIdentifier graphIdentifier); + GraphContext getGraphWithContext(GraphIdentifier graphIdentifier); /** * Get all {@link CustomDiagram} for a dataset. @@ -86,19 +73,29 @@ public interface InMemoryDatabase { DiagramLayout getDatasetDiagramLayout(String datasetName); /** - * Creates a new {@link GraphRewindableWithUUIDs} in a specified dataset. If the dataset does - * not exist yet, it will be created. If the {@link GraphRewindableWithUUIDs} already exists - * nothing happens. + * Creates a new named graph in a specified dataset. If the dataset does not exist yet, it will + * be created. If the graph already exists, nothing happens. Merges the graph's prefix mapping + * into the dataset's existing prefixes. * * @param graphIdentifier The identifier of the graph, which includes the dataset name and the * graph URI. * @param newGraph The new Graph. */ - void create(GraphIdentifier graphIdentifier, Graph newGraph); + void createGraph(GraphIdentifier graphIdentifier, Graph newGraph); + + /** + * Creates an empty graph referenced by {@code graphIdentifier}. + * + *

If the dataset does not exist yet, it will be created with editing enabled and default + * namespace prefixes from the schema configuration. + * + * @param graphIdentifier identifies dataset and graph URI + */ + void createEmptyGraph(GraphIdentifier graphIdentifier); /** - * Deletes a {@link GraphRewindableWithUUIDs} from a specified dataset. If the graph or dataset - * does not exist, nothing happens. + * Deletes the named graph from a specified dataset. If the graph or dataset does not exist, + * nothing happens. * * @param graphIdentifier The identifier of the graph, which includes the dataset name and the * graph URI. @@ -169,44 +166,6 @@ public interface InMemoryDatabase { */ void fetchSnapshot(DatabaseConnection databaseConnection, String base64Token); - /** - * Undoes the last change made to a graph. - * - * @param graphIdentifier The identifier of the graph, which includes the dataset name and the - * graph URI. - * @throws DataAccessException if the dataset or graph does not exist. - */ - void undo(GraphIdentifier graphIdentifier); - - /** - * Redoes the last previously undone change made to a graph. - * - * @param graphIdentifier The identifier of the graph, which includes the dataset name and the - * graph URI. - * @throws DataAccessException if the dataset or graph does not exist. - */ - void redo(GraphIdentifier graphIdentifier); - - /** - * Checks whether the last change made to a graph can be undone. - * - * @param graphIdentifier The identifier of the graph, which includes the dataset name and the - * graph URI. - * @return True if the last change can be undone, otherwise False. - * @throws DataAccessException if the dataset or graph does not exist. - */ - boolean canUndo(GraphIdentifier graphIdentifier); - - /** - * Checks whether the last undone change can be redone. - * - * @param graphIdentifier The identifier of the graph, which includes the dataset name and the - * graph URI. - * @return True if the last undone change can be redone, otherwise False. - * @throws DataAccessException if the dataset or graph does not exist. - */ - boolean canRedo(GraphIdentifier graphIdentifier); - /** * Checks if a dataset is currently set to read-only. * @@ -231,15 +190,4 @@ public interface InMemoryDatabase { * @throws DataAccessException if the dataset does not exist. */ void disableEditing(String datasetName); - - /** - * Restores a graph to a specific version identified by its UUID. - * - * @param graphIdentifier The identifier of the graph, which includes the dataset name and the - * graph URI. - * @param versionId The UUID of the version to restore. - * @throws DataAccessException if the dataset or graph does not exist, or if the versionId is - * invalid. - */ - void restore(GraphIdentifier graphIdentifier, UUID versionId); } diff --git a/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabaseAdapter.java b/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabaseAdapter.java index ef509b75..57d253f3 100644 --- a/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabaseAdapter.java +++ b/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabaseAdapter.java @@ -21,11 +21,9 @@ import org.apache.jena.graph.Graph; import org.apache.jena.shared.PrefixMapping; -import org.apache.jena.shared.impl.PrefixMappingImpl; -import org.apache.jena.sparql.graph.GraphFactory; -import org.rdfarchitect.config.SchemaConfig; import org.rdfarchitect.database.DatabaseConnection; import org.rdfarchitect.database.DatabasePort; +import org.rdfarchitect.database.GraphContext; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.database.inmemory.diagrams.CustomDiagram; import org.rdfarchitect.rdf.graph.wrapper.DiagramLayout; @@ -39,10 +37,8 @@ public class InMemoryDatabaseAdapter implements DatabasePort { private final InMemoryDatabase database; - private final SchemaConfig schemaConfig; - @Override - public GraphWithContext getGraphWithContext(GraphIdentifier graphIdentifier) { + public GraphContext getGraphWithContext(GraphIdentifier graphIdentifier) { return database.getGraphWithContext(graphIdentifier); } @@ -68,53 +64,12 @@ public void deleteGraph(GraphIdentifier graphIdentifier) { @Override public void createGraph(GraphIdentifier graphIdentifier, Graph graph) { - database.create(graphIdentifier, graph); - var currentPrefixMapping = - new PrefixMappingImpl() - .setNsPrefixes(database.getPrefixMapping(graphIdentifier.datasetName())) - .setNsPrefixes(graph.getPrefixMapping()); - database.setPrefixMapping(graphIdentifier.datasetName(), currentPrefixMapping); + database.createGraph(graphIdentifier, graph); } @Override public void createEmptyGraph(GraphIdentifier graphIdentifier) { - var datasetName = graphIdentifier.datasetName(); - var isNewDataset = !database.listDatasets().contains(datasetName); - database.create(graphIdentifier, GraphFactory.createDefaultGraph()); - if (isNewDataset) { - database.enableEditing(datasetName); - var configNamespaces = schemaConfig.getNamespaces(); - var prefixMapping = new PrefixMappingImpl().setNsPrefixes(PrefixMapping.Standard); - for (var entry : configNamespaces.entrySet()) { - prefixMapping.setNsPrefix(entry.getKey(), entry.getValue()); - } - database.setPrefixMapping(graphIdentifier.datasetName(), prefixMapping); - } - } - - @Override - public Boolean canRedo(GraphIdentifier graphIdentifier) { - return database.canRedo(graphIdentifier); - } - - @Override - public Boolean canUndo(GraphIdentifier graphIdentifier) { - return database.canUndo(graphIdentifier); - } - - @Override - public void redo(GraphIdentifier graphIdentifier) { - database.redo(graphIdentifier); - } - - @Override - public void undo(GraphIdentifier graphIdentifier) { - database.undo(graphIdentifier); - } - - @Override - public void restore(GraphIdentifier graphIdentifier, UUID versionId) { - database.restore(graphIdentifier, versionId); + database.createEmptyGraph(graphIdentifier); } @Override diff --git a/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabaseImpl.java b/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabaseImpl.java index e43cde84..b24d4bd5 100644 --- a/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabaseImpl.java +++ b/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemoryDatabaseImpl.java @@ -17,16 +17,20 @@ package org.rdfarchitect.database.inmemory; +import lombok.RequiredArgsConstructor; + import org.apache.jena.graph.Graph; -import org.apache.jena.query.TxnType; import org.apache.jena.shared.PrefixMapping; +import org.apache.jena.shared.impl.PrefixMappingImpl; +import org.apache.jena.sparql.graph.GraphFactory; import org.apache.jena.sparql.graph.PrefixMappingReadOnly; +import org.rdfarchitect.config.SchemaConfig; import org.rdfarchitect.context.SessionContext; import org.rdfarchitect.database.DatabaseConnection; +import org.rdfarchitect.database.GraphContext; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.database.inmemory.diagrams.CustomDiagram; import org.rdfarchitect.rdf.graph.wrapper.DiagramLayout; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; import java.util.List; import java.util.Map; @@ -38,8 +42,11 @@ * {@link SessionDataStore} of the current session. The session is extracted from the {@link * SessionContext}. */ +@RequiredArgsConstructor public class InMemoryDatabaseImpl implements InMemoryDatabase { + private final SchemaConfig schemaConfig; + private final ConcurrentHashMap sessionStores = new ConcurrentHashMap<>(); @@ -53,11 +60,6 @@ public List listDatasets() { return getOrCreateSessionDataStore().listDatasets(); } - @Override - public GraphRewindableWithUUIDs begin(GraphIdentifier graphIdentifier, TxnType txnType) { - return getOrCreateSessionDataStore().begin(graphIdentifier, txnType); - } - @Override public Map getDatasetDiagrams(String datasetName) { return getOrCreateSessionDataStore().getDatasetDiagrams(datasetName); @@ -69,13 +71,36 @@ public DiagramLayout getDatasetDiagramLayout(String datasetName) { } @Override - public GraphWithContext getGraphWithContext(GraphIdentifier graphIdentifier) { + public GraphContext getGraphWithContext(GraphIdentifier graphIdentifier) { return getOrCreateSessionDataStore().getGraphWithContext(graphIdentifier); } @Override - public void create(GraphIdentifier graphIdentifier, Graph newGraph) { - getOrCreateSessionDataStore().create(graphIdentifier, newGraph); + public void createGraph(GraphIdentifier graphIdentifier, Graph newGraph) { + var store = getOrCreateSessionDataStore(); + store.create(graphIdentifier, newGraph); + var currentPrefixMapping = + new PrefixMappingImpl() + .setNsPrefixes(store.getPrefixMapping(graphIdentifier.datasetName())) + .setNsPrefixes(newGraph.getPrefixMapping()); + store.setPrefixMapping(graphIdentifier.datasetName(), currentPrefixMapping); + } + + @Override + public void createEmptyGraph(GraphIdentifier graphIdentifier) { + var store = getOrCreateSessionDataStore(); + var datasetName = graphIdentifier.datasetName(); + var isNewDataset = !store.listDatasets().contains(datasetName); + store.create(graphIdentifier, GraphFactory.createDefaultGraph()); + if (isNewDataset) { + store.enableEditing(datasetName); + var configNamespaces = schemaConfig.getNamespaces(); + var prefixMapping = new PrefixMappingImpl().setNsPrefixes(PrefixMapping.Standard); + for (var entry : configNamespaces.entrySet()) { + prefixMapping.setNsPrefix(entry.getKey(), entry.getValue()); + } + store.setPrefixMapping(datasetName, prefixMapping); + } } @Override @@ -119,26 +144,6 @@ public void fetchSnapshot(DatabaseConnection databaseConnection, String base64To getOrCreateSessionDataStore().fetchSnapshot(databaseConnection, base64Token); } - @Override - public void undo(GraphIdentifier graphIdentifier) { - getOrCreateSessionDataStore().undo(graphIdentifier); - } - - @Override - public void redo(GraphIdentifier graphIdentifier) { - getOrCreateSessionDataStore().redo(graphIdentifier); - } - - @Override - public boolean canUndo(GraphIdentifier graphIdentifier) { - return getOrCreateSessionDataStore().canUndo(graphIdentifier); - } - - @Override - public boolean canRedo(GraphIdentifier graphIdentifier) { - return getOrCreateSessionDataStore().canRedo(graphIdentifier); - } - @Override public boolean isReadOnly(String datasetName) { return getOrCreateSessionDataStore().isReadOnly(datasetName); @@ -154,11 +159,6 @@ public void disableEditing(String datasetName) { getOrCreateSessionDataStore().disableEditing(datasetName); } - @Override - public void restore(GraphIdentifier graphIdentifier, UUID versionId) { - getOrCreateSessionDataStore().restore(graphIdentifier, versionId); - } - /** Returns the SessionDataStore for the current session, creating one if necessary. */ private SessionDataStore getOrCreateSessionDataStore() { return sessionStores.computeIfAbsent( diff --git a/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemorySparqlExecutor.java b/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemorySparqlExecutor.java index 1ea34953..e7c12761 100644 --- a/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemorySparqlExecutor.java +++ b/backend/src/main/java/org/rdfarchitect/database/inmemory/InMemorySparqlExecutor.java @@ -20,47 +20,21 @@ import lombok.experimental.UtilityClass; import org.apache.jena.query.Query; -import org.apache.jena.query.QueryExecution; import org.apache.jena.query.QueryExecutionFactory; +import org.apache.jena.query.ReadWrite; import org.apache.jena.query.ResultSet; import org.apache.jena.query.ResultSetFactory; -import org.apache.jena.query.TxnType; -import org.apache.jena.update.UpdateExecutionFactory; -import org.apache.jena.update.UpdateRequest; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; +import org.rdfarchitect.database.GraphContext; @UtilityClass public class InMemorySparqlExecutor { - /** - * Opens a single WRITE transaction on {@code graph} and runs {@code update}. Use this only when - * the {@link UpdateRequest} can be fully prepared before the transaction opens. - */ - public void executeSingleUpdate( - GraphRewindableWithUUIDs graph, UpdateRequest update, String graphUri) { - try { - graph.begin(TxnType.WRITE); - var dataset = SessionDataStore.wrapGraphInDataset(graph, graphUri); - UpdateExecutionFactory.create(update, dataset).execute(); - graph.commit(); - } finally { - graph.end(); - } - } - - public ResultSet executeSingleQuery( - GraphRewindableWithUUIDs graph, Query query, String graphUri) { - QueryExecution queryExecution = null; - try { - graph.begin(TxnType.READ); - var dataset = SessionDataStore.wrapGraphInDataset(graph, graphUri); - queryExecution = QueryExecutionFactory.create(query, dataset); - var resultSet = queryExecution.execSelect(); - return ResultSetFactory.copyResults(resultSet); - } finally { - graph.end(); - if (queryExecution != null) { - queryExecution.close(); + public ResultSet executeSingleQuery(GraphContext graph, Query query, String graphUri) { + try (var ctx = graph.begin(ReadWrite.READ)) { + var dataset = SessionDataStore.wrapGraphInDataset(ctx.getRdfGraph(), graphUri); + try (var queryExecution = QueryExecutionFactory.create(query, dataset)) { + var resultSet = queryExecution.execSelect(); + return ResultSetFactory.copyResults(resultSet); } } } diff --git a/backend/src/main/java/org/rdfarchitect/database/inmemory/SessionDataStore.java b/backend/src/main/java/org/rdfarchitect/database/inmemory/SessionDataStore.java index 82b7c6d2..b4924489 100644 --- a/backend/src/main/java/org/rdfarchitect/database/inmemory/SessionDataStore.java +++ b/backend/src/main/java/org/rdfarchitect/database/inmemory/SessionDataStore.java @@ -20,17 +20,15 @@ import org.apache.jena.graph.Graph; import org.apache.jena.query.Dataset; import org.apache.jena.query.DatasetFactory; -import org.apache.jena.query.TxnType; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.shared.PrefixMapping; -import org.apache.jena.sparql.core.Transactional; import org.apache.jena.sparql.graph.PrefixMappingReadOnly; import org.rdfarchitect.database.DatabaseConnection; +import org.rdfarchitect.database.GraphContext; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.database.inmemory.diagrams.CustomDiagram; import org.rdfarchitect.exception.database.DataAccessException; import org.rdfarchitect.rdf.graph.wrapper.DiagramLayout; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; import java.util.List; import java.util.Map; @@ -57,23 +55,12 @@ static Dataset wrapGraphInDataset(Graph graph, String graphUri) { List listDatasets(); /** - * Begin a transaction on a {@link GraphRewindableWithUUIDs}. - * - * @param graphIdentifier The identifier of the graph, which includes the dataset name and the - * graph URI. - * @param txnType The transactionType - * @return The {@link GraphRewindableWithUUIDs} the {@link Transactional Transaction} is - * performed on. - */ - GraphRewindableWithUUIDs begin(GraphIdentifier graphIdentifier, TxnType txnType); - - /** - * Get a {@link GraphWithContext} from the database. + * Get a {@link GraphContext} for the specified graph. * * @param graphIdentifier The identifier of the graph. - * @return The {@link GraphWithContext}. + * @return The {@link GraphContext}. */ - GraphWithContext getGraphWithContext(GraphIdentifier graphIdentifier); + GraphContext getGraphWithContext(GraphIdentifier graphIdentifier); /** * Get all {@link CustomDiagram} for a dataset. @@ -92,9 +79,8 @@ static Dataset wrapGraphInDataset(Graph graph, String graphUri) { DiagramLayout getDatasetDiagramLayout(String datasetName); /** - * Creates a new {@link GraphRewindableWithUUIDs} in a specified dataset. If the dataset does - * not exist yet, it will be created. If the {@link GraphRewindableWithUUIDs} already exists - * nothing happens. + * Creates a new named graph in a specified dataset. If the dataset does not exist yet, it will + * be created. If the graph already exists, nothing happens. * * @param graphIdentifier The identifier of the graph, which includes the dataset name and the * graph URI. @@ -103,8 +89,8 @@ static Dataset wrapGraphInDataset(Graph graph, String graphUri) { void create(GraphIdentifier graphIdentifier, Graph newGraph); /** - * Deletes a {@link GraphRewindableWithUUIDs} from a specified dataset. If the graph or dataset - * does not exist, nothing happens. + * Deletes the named graph from a specified dataset. If the graph or dataset does not exist, + * nothing happens. * * @param graphIdentifier The identifier of the graph, which includes the dataset name and the * graph URI. @@ -174,44 +160,6 @@ static Dataset wrapGraphInDataset(Graph graph, String graphUri) { */ void fetchSnapshot(DatabaseConnection databaseConnection, String base64Token); - /** - * Undoes the last change made to a graph. - * - * @param graphIdentifier The identifier of the graph, which includes the dataset name and the - * graph URI. - * @throws DataAccessException if the dataset or graph does not exist. - */ - void undo(GraphIdentifier graphIdentifier); - - /** - * Redoes the last previously undone change made to a graph. - * - * @param graphIdentifier The identifier of the graph, which includes the dataset name and the - * graph URI. - * @throws DataAccessException if the dataset or graph does not exist. - */ - void redo(GraphIdentifier graphIdentifier); - - /** - * Checks whether the last change made to a graph can be undone. - * - * @param graphIdentifier The identifier of the graph, which includes the dataset name and the - * graph URI. - * @return True if the last change can be undone, otherwise False. - * @throws DataAccessException if the dataset or graph does not exist. - */ - boolean canUndo(GraphIdentifier graphIdentifier); - - /** - * Checks whether the last undone change can be redone. - * - * @param graphIdentifier The identifier of the graph, which includes the dataset name and the - * graph URI. - * @return True if the last undone change can be redone, otherwise False. - * @throws DataAccessException if the dataset or graph does not exist. - */ - boolean canRedo(GraphIdentifier graphIdentifier); - /** * Checks if a dataset is currently set to read-only. * @@ -236,14 +184,4 @@ static Dataset wrapGraphInDataset(Graph graph, String graphUri) { * @throws DataAccessException if the dataset does not exist. */ void disableEditing(String datasetName); - - /** - * Restores a graph to a specific version. - * - * @param graphIdentifier The identifier of the graph, which includes the dataset name and the - * graph URI. - * @param versionId The ID of the version to restore to. - * @throws DataAccessException if the graph does not exist. - */ - void restore(GraphIdentifier graphIdentifier, UUID versionId); } diff --git a/backend/src/main/java/org/rdfarchitect/database/inmemory/SessionDataStoreImpl.java b/backend/src/main/java/org/rdfarchitect/database/inmemory/SessionDataStoreImpl.java index f5911476..2cb9128d 100644 --- a/backend/src/main/java/org/rdfarchitect/database/inmemory/SessionDataStoreImpl.java +++ b/backend/src/main/java/org/rdfarchitect/database/inmemory/SessionDataStoreImpl.java @@ -24,19 +24,19 @@ import org.apache.jena.graph.Graph; import org.apache.jena.query.Dataset; import org.apache.jena.query.DatasetFactory; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.shared.PrefixMapping; import org.apache.jena.sparql.graph.GraphFactory; import org.apache.jena.sparql.graph.PrefixMappingReadOnly; import org.rdfarchitect.database.DatabaseConnection; +import org.rdfarchitect.database.GraphContext; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.database.inmemory.diagrams.CustomDiagram; import org.rdfarchitect.exception.database.DataAccessException; import org.rdfarchitect.models.cim.queries.select.CIMBaseQueryBuilder; import org.rdfarchitect.rdf.graph.source.builder.implementations.GraphSourceBuilderImpl; import org.rdfarchitect.rdf.graph.wrapper.DiagramLayout; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; import java.util.ArrayList; import java.util.List; @@ -46,9 +46,8 @@ import java.util.concurrent.locks.ReentrantLock; /** - * Class that provides {@link GraphRewindableWithUUIDs GraphRewindables} belonging to a session. - * Write actions on this class are irreversible. closing the provided {@link - * GraphRewindableWithUUIDs GraphRewindables} is mandatory. + * Class that provides {@link GraphContext} instances belonging to a session. Write actions on this + * class are irreversible. Closing the provided {@link GraphContext} instances is mandatory. */ public class SessionDataStoreImpl implements SessionDataStore { @@ -69,13 +68,7 @@ public void deleteDataset(String datasetName) { if (!graphCollections.containsKey(datasetName)) { return; } - var graphRewindableCollection = graphCollections.get(datasetName); - for (String graphUri : graphRewindableCollection.listGraphUris()) { - GraphRewindableWithUUIDs graph = - graphRewindableCollection.begin(graphUri, TxnType.WRITE); - graph.close(); - graph.end(); - } + graphCollections.get(datasetName).clear(); graphCollections.remove(datasetName); } finally { lock.unlock(); @@ -93,20 +86,7 @@ public List listDatasets() { } @Override - public GraphRewindableWithUUIDs begin(GraphIdentifier graphIdentifier, TxnType txnType) { - final String datasetName = graphIdentifier.datasetName(); - final String graphUri = graphIdentifier.graphUri(); - lock.lock(); - try { - createDataset(datasetName); - return graphCollections.get(datasetName).begin(graphUri, txnType); - } finally { - lock.unlock(); - } - } - - @Override - public GraphWithContext getGraphWithContext(GraphIdentifier graphIdentifier) { + public GraphContext getGraphWithContext(GraphIdentifier graphIdentifier) { lock.lock(); try { createDataset(graphIdentifier.datasetName()); @@ -178,7 +158,7 @@ public boolean containsGraph(GraphIdentifier graphIdentifier) { lock.lock(); try { return graphCollections.containsKey(datasetName) - && graphCollections.get(datasetName).containsGraph(graphUri); + && graphCollections.get(datasetName).listGraphUris().contains(graphUri); } finally { lock.unlock(); } @@ -225,17 +205,17 @@ public void writeToDatabase( final String datasetName = graphIdentifier.datasetName(); final String graphUri = graphIdentifier.graphUri(); lock.lock(); - GraphRewindableWithUUIDs graph = null; try { assertThatGraphExists(graphIdentifier); - graph = begin(graphIdentifier, TxnType.READ); - var graphSource = - new GraphSourceBuilderImpl().setGraph(graph).setGraphName(graphUri).build(); - databaseConnection.insertGraph(graphSource, datasetName); - } finally { - if (graph != null) { - graph.end(); + try (var ctx = getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + var graphSource = + new GraphSourceBuilderImpl() + .setGraph(ctx.getRdfGraph()) + .setGraphName(graphUri) + .build(); + databaseConnection.insertGraph(graphSource, datasetName); } + } finally { lock.unlock(); } } @@ -350,71 +330,6 @@ private Graph fetchGraph( return resGraph; } - @Override - public void undo(GraphIdentifier graphIdentifier) { - final String datasetName = graphIdentifier.datasetName(); - final String graphUri = graphIdentifier.graphUri(); - lock.lock(); - try { - assertThatDatasetExists(datasetName); - graphCollections.get(datasetName).undo(graphUri); - } finally { - lock.unlock(); - } - } - - @Override - public void redo(GraphIdentifier graphIdentifier) { - final String datasetName = graphIdentifier.datasetName(); - final String graphUri = graphIdentifier.graphUri(); - lock.lock(); - try { - assertThatDatasetExists(datasetName); - graphCollections.get(datasetName).redo(graphUri); - } finally { - lock.unlock(); - } - } - - @Override - public boolean canUndo(GraphIdentifier graphIdentifier) { - final String datasetName = graphIdentifier.datasetName(); - final String graphUri = graphIdentifier.graphUri(); - lock.lock(); - try { - assertThatDatasetExists(datasetName); - return graphCollections.get(datasetName).canUndo(graphUri); - } finally { - lock.unlock(); - } - } - - @Override - public boolean canRedo(GraphIdentifier graphIdentifier) { - final String datasetName = graphIdentifier.datasetName(); - final String graphUri = graphIdentifier.graphUri(); - lock.lock(); - try { - assertThatDatasetExists(datasetName); - return graphCollections.get(datasetName).canRedo(graphUri); - } finally { - lock.unlock(); - } - } - - @Override - public void restore(GraphIdentifier graphIdentifier, UUID versionId) { - final String datasetName = graphIdentifier.datasetName(); - final String graphUri = graphIdentifier.graphUri(); - lock.lock(); - try { - assertThatGraphExists(graphIdentifier); - graphCollections.get(datasetName).restore(graphUri, versionId); - } finally { - lock.unlock(); - } - } - @Override public boolean isReadOnly(String datasetName) { lock.lock(); @@ -464,14 +379,14 @@ private void assertThatDatasetExists(String datasetName) { * Throws an exceptions if the graph or its dataset does not exist * * @param graphIdentifier The identifier of the graph, which includes the dataset name and the - * graph URI.. + * graph URI. * @throws DataAccessException if the dataset or graph does not exist. */ private void assertThatGraphExists(GraphIdentifier graphIdentifier) { final String datasetName = graphIdentifier.datasetName(); final String graphUri = graphIdentifier.graphUri(); assertThatDatasetExists(datasetName); - if (!graphCollections.get(datasetName).containsGraph(graphUri)) { + if (!graphCollections.get(datasetName).listGraphUris().contains(graphUri)) { throw new DataAccessException( "Graph " + graphUri + " does not exist in dataset " + datasetName); } diff --git a/backend/src/main/java/org/rdfarchitect/database/inmemory/diagrams/CustomDiagram.java b/backend/src/main/java/org/rdfarchitect/database/inmemory/diagrams/CustomDiagram.java index 729d8455..36390696 100644 --- a/backend/src/main/java/org/rdfarchitect/database/inmemory/diagrams/CustomDiagram.java +++ b/backend/src/main/java/org/rdfarchitect/database/inmemory/diagrams/CustomDiagram.java @@ -17,14 +17,227 @@ package org.rdfarchitect.database.inmemory.diagrams; -import lombok.Data; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +import org.rdfarchitect.rdf.graph.DeltaCompressible; +import org.rdfarchitect.rdf.graph.wrapper.Rewindable; +import org.rdfarchitect.rdf.graph.wrapper.TransactionParticipant; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; import java.util.List; import java.util.UUID; -@Data -public class CustomDiagram { - private final UUID diagramId; - private String name; +/** + * A custom diagram that participates in the transactional undo/redo framework. + * + *

State is managed via snapshots: on each {@link #commit()}, the current state is deep-copied + * and pushed onto the undo stack. {@link #undo()} and {@link #redo()} restore previous snapshots. + * + *

Before a write transaction begins, the coordinator must call {@link #beginTransaction()} so + * that {@link #abort()} can restore the pre-transaction state and {@link #hasChanges()} can detect + * modifications. The method is package-private so that only the coordinator ({@link + * org.rdfarchitect.database.inmemory.GraphWithContextTransactional}) in the same package can invoke + * it. + * + *

Like all other transaction participants, this class has no lock of its own. Synchronisation is + * the responsibility of the owning coordinator. + */ +public class CustomDiagram implements TransactionParticipant, Rewindable { + + @Getter private final UUID diagramId; + @Getter @Setter private String name; private List classes; + + private final Deque undoStack = new ArrayDeque<>(); + private final Deque redoStack = new ArrayDeque<>(); + + /** + * Snapshot of the state at the start of the current transaction. {@code null} outside + * transactions. + */ + private DiagramSnapshot preTransactionSnapshot; + + private record DiagramSnapshot(UUID versionId, String name, List classes) { + + static DiagramSnapshot of(CustomDiagram diagram) { + return new DiagramSnapshot( + UUID.randomUUID(), diagram.name, deepCopyClasses(diagram.classes)); + } + + void applyTo(CustomDiagram diagram) { + diagram.name = name; + diagram.classes = deepCopyClasses(classes); + } + + private static List deepCopyClasses(List source) { + if (source == null) { + return Collections.emptyList(); + } + var copy = new ArrayList(source.size()); + for (var c : source) { + copy.add(new ClassInDiagram(c.getUuid(), c.getGraphUri())); + } + return copy; + } + } + + /** + * Jackson deserialization constructor. The {@code diagramId} field is {@code final} and must be + * supplied via {@code @JsonCreator} since a no-args constructor cannot initialise it. + */ + @JsonCreator + public CustomDiagram( + @JsonProperty("diagramId") UUID diagramId, + @JsonProperty("name") String name, + @JsonProperty("classes") List classes) { + this.diagramId = diagramId; + this.name = name; + setClasses(classes); + } + + public CustomDiagram(UUID diagramId) { + this.diagramId = diagramId; + } + + // ------------------------------------------------------------------------- + // Accessors for classes (defensive copying) + // ------------------------------------------------------------------------- + + /** + * Returns a deep copy of the classes list so that callers cannot mutate internal state. + * + * @return a deep copy of the diagram's class list, never {@code null} + */ + public List getClasses() { + return DiagramSnapshot.deepCopyClasses(classes); + } + + /** + * Stores a deep copy of the provided list, preventing external aliasing after assignment. + * + * @param classes the new list of classes; {@code null} is treated as an empty list + */ + public void setClasses(List classes) { + this.classes = DiagramSnapshot.deepCopyClasses(classes); + } + + // ------------------------------------------------------------------------- + // TransactionParticipant + // ------------------------------------------------------------------------- + + /** + * Captures a snapshot of the current state at the start of a write transaction. Must be called + * by the coordinator before any modifications are made, so that {@link #abort()} can restore + * the original state and {@link #hasChanges()} can detect modifications. + * + *

Internal API: only {@link + * org.rdfarchitect.database.inmemory.GraphWithContextTransactional} should call this method. + */ + public void beginTransaction() { + preTransactionSnapshot = DiagramSnapshot.of(this); + } + + /** + * Pushes the current state onto the undo stack and clears the redo stack. The pre-transaction + * snapshot is reset so that the next transaction starts clean. + */ + @Override + public void commit() { + undoStack.push(DiagramSnapshot.of(this)); + redoStack.clear(); + preTransactionSnapshot = null; + } + + /** + * Restores the diagram to its pre-transaction state, discarding any modifications made during + * the current transaction. + */ + @Override + public void abort() { + if (preTransactionSnapshot != null) { + preTransactionSnapshot.applyTo(this); + preTransactionSnapshot = null; + } + } + + /** + * Returns {@code true} if the diagram has been modified since {@link #beginTransaction()} was + * called. Returns {@code false} if no transaction is active. + */ + @Override + public boolean hasChanges() { + if (preTransactionSnapshot == null) { + return false; + } + return !java.util.Objects.equals(preTransactionSnapshot.name, name) + || !java.util.Objects.equals(preTransactionSnapshot.classes, classes); + } + + // ------------------------------------------------------------------------- + // Rewindable + // ------------------------------------------------------------------------- + + @Override + public void undo() { + if (!canUndo()) { + return; + } + redoStack.push(undoStack.pop()); + var current = undoStack.peek(); + assert current != null : "canUndo() guarantees at least one entry remains"; + current.applyTo(this); + } + + @Override + public void redo() { + if (!canRedo()) { + return; + } + var snapshot = redoStack.pop(); + undoStack.push(snapshot); + snapshot.applyTo(this); + } + + @Override + public boolean canUndo() { + return undoStack.size() > 1; + } + + @Override + public boolean canRedo() { + return !redoStack.isEmpty(); + } + + @Override + public void restore(UUID versionId) { + while (undoStack.size() > 1) { + var top = undoStack.peek(); + assert top != null; + if (top.versionId().equals(versionId)) { + break; + } + redoStack.push(undoStack.pop()); + } + var current = undoStack.peek(); + if (current != null) { + current.applyTo(this); + } + } + + /** + * Not applicable for snapshot-based rewindables. Returns {@code null}. + * + * @return {@code null} always + */ + @Override + public DeltaCompressible getLastDelta() { + return null; + } } diff --git a/backend/src/main/java/org/rdfarchitect/database/snapshots/FusekiSnapshotAdapter.java b/backend/src/main/java/org/rdfarchitect/database/snapshots/FusekiSnapshotAdapter.java index 0400755e..43f8924f 100644 --- a/backend/src/main/java/org/rdfarchitect/database/snapshots/FusekiSnapshotAdapter.java +++ b/backend/src/main/java/org/rdfarchitect/database/snapshots/FusekiSnapshotAdapter.java @@ -20,9 +20,8 @@ import static org.rdfarchitect.database.snapshots.SnapshotUtils.constructSnapshotName; import static org.rdfarchitect.database.snapshots.SnapshotUtils.findSnapshotName; import static org.rdfarchitect.database.snapshots.SnapshotUtils.generateBase64Token; -import static org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs.removeUUIDs; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdfconnection.RDFConnection; import org.apache.jena.system.Txn; @@ -36,7 +35,6 @@ import org.rdfarchitect.exception.database.FusekiServerException; import org.rdfarchitect.exception.database.SnapshotException; import org.rdfarchitect.rdf.graph.GraphUtils; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; public class FusekiSnapshotAdapter implements SnapshotPort { @@ -102,25 +100,17 @@ public boolean snapshotExists(String base64Token) { } private void transferGraph(RDFConnection conn, GraphIdentifier graphIdentifier) { - GraphRewindableWithUUIDs graph = null; - try { - graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - graph.begin(TxnType.READ); - - var copiedGraph = GraphUtils.deepCopy(graph); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + var copiedGraph = GraphUtils.deepCopy(ctx.getRdfGraph()); copiedGraph .getPrefixMapping() .setNsPrefixes(databasePort.getPrefixMapping(graphIdentifier.datasetName())); - removeUUIDs(copiedGraph); + GraphUtils.removeUUIDs(copiedGraph); conn.put(graphIdentifier.graphUri(), ModelFactory.createModelForGraph(copiedGraph)); } catch (Exception e) { throw new FusekiServerException(e.getMessage()); - } finally { - if (graph != null) { - graph.end(); - } } } } diff --git a/backend/src/main/java/org/rdfarchitect/listeners/DatasetSessionListener.java b/backend/src/main/java/org/rdfarchitect/listeners/DatasetSessionListener.java index b8a2808c..8cb0baa5 100644 --- a/backend/src/main/java/org/rdfarchitect/listeners/DatasetSessionListener.java +++ b/backend/src/main/java/org/rdfarchitect/listeners/DatasetSessionListener.java @@ -26,8 +26,7 @@ import org.rdfarchitect.database.DatabaseConnection; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; -import org.rdfarchitect.models.changelog.ChangeLogEntry; -import org.rdfarchitect.services.ChangeLogUseCase; +import org.rdfarchitect.services.dl.update.packagelayout.CreateDiagramLayoutUseCase; import org.springframework.stereotype.Component; @Component @@ -36,32 +35,22 @@ public class DatasetSessionListener implements HttpSessionListener { private final DatabasePort databasePort; private final DatabaseConnection databaseConnection; - private final ChangeLogUseCase changeLogUseCase; + private final CreateDiagramLayoutUseCase createDiagramLayoutUseCase; @Override public void sessionCreated(HttpSessionEvent event) { SessionContext.setSessionId(event.getSession().getId()); databasePort.fetchFromDatabase(databaseConnection); - var datasets = databasePort.listDatasets(); - for (var dataset : datasets) { - var graphUris = databasePort.listGraphUris(dataset); - for (var graphUri : graphUris) { - var graphIdentifier = new GraphIdentifier(dataset, graphUri); - changeLogUseCase.recordChange( - graphIdentifier, - new ChangeLogEntry( - "Imported graph into dataset '" - + dataset - + "' with graph URI '" - + graphUri - + "'.", - databasePort - .getGraphWithContext(graphIdentifier) - .getRdfGraph() - .getLastDelta())); + createDiagramLayoutsForAllGraphs(); + SessionContext.clear(); + } + + private void createDiagramLayoutsForAllGraphs() { + for (var datasetName : databasePort.listDatasets()) { + for (var graphUri : databasePort.listGraphUris(datasetName)) { + createDiagramLayoutUseCase.createDiagramLayout( + new GraphIdentifier(datasetName, graphUri)); } } - - SessionContext.clear(); } } diff --git a/backend/src/main/java/org/rdfarchitect/models/changelog/ChangeLog.java b/backend/src/main/java/org/rdfarchitect/models/changelog/ChangeLog.java new file mode 100644 index 00000000..f395cc8f --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/changelog/ChangeLog.java @@ -0,0 +1,292 @@ +/* + * 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.changelog; + +import org.apache.jena.query.ReadWrite; +import org.rdfarchitect.exception.graph.GraphNotInATransactionException; +import org.rdfarchitect.exception.graph.GraphNotInAWriteTransactionException; +import org.rdfarchitect.rdf.graph.wrapper.TransactionContext; +import org.rdfarchitect.rdf.graph.wrapper.TransactionParticipant; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; +import java.util.UUID; + +/** + * Manages the undo/redo history of named commits. + * + *

The changelog maintains two stacks: an undo stack holding committed entries and a + * redo stack holding entries that have been undone and can be reapplied. Together they + * form a linear version history that supports arbitrary undo, redo, and restore operations. + * + *

This class implements {@link TransactionParticipant}. All mutating operations buffer their + * effect as pending mutations that are only applied when {@link #commit()} is called. Calling + * {@link #abort()} discards all pending mutations, leaving both stacks unchanged. This ensures that + * the changelog stays consistent with the graph participants it is coordinated with. + * + *

All mutating methods require an active write transaction on the shared {@link + * TransactionContext}. Read-only query methods require at least a read transaction. Violating these + * preconditions throws {@link GraphNotInATransactionException} or {@link + * GraphNotInAWriteTransactionException}. + * + *

Thread safety: this class is not thread-safe on its own. Synchronisation is + * the responsibility of the owning coordinator (typically {@link + * org.rdfarchitect.database.inmemory.GraphWithContextTransactional}). + * + * @see TransactionParticipant + * @see ChangeLogEntry + */ +public class ChangeLog implements TransactionParticipant { + + private final TransactionContext txnContext; + + private final Deque undoStack = new ArrayDeque<>(); + private final Deque redoStack = new ArrayDeque<>(); + + private final List pendingMutations = new ArrayList<>(); + + /** + * Creates a new, empty changelog bound to the given transaction context. + * + * @param txnContext the shared transaction context used to enforce transaction preconditions + */ + public ChangeLog(TransactionContext txnContext) { + this.txnContext = txnContext; + } + + // ------------------------------------------------------------------------- + // TransactionParticipant + // ------------------------------------------------------------------------- + + /** + * Applies all pending mutations to the undo and redo stacks in the order they were buffered. + * After this call, {@link #hasChanges()} returns {@code false}. + */ + @Override + public void commit() { + pendingMutations.forEach(Runnable::run); + pendingMutations.clear(); + } + + /** + * Discards all pending mutations without applying them. Both stacks remain in the state they + * were in before the current transaction began. After this call, {@link #hasChanges()} returns + * {@code false}. + */ + @Override + public void abort() { + pendingMutations.clear(); + } + + /** + * Returns whether there are any pending mutations that have not yet been committed or aborted. + * + * @return {@code true} if at least one mutation is buffered, {@code false} otherwise + */ + @Override + public boolean hasChanges() { + return !pendingMutations.isEmpty(); + } + + // ------------------------------------------------------------------------- + // Buffered mutators — require write transaction, deferred until commit() + // ------------------------------------------------------------------------- + + /** + * Buffers pushing a new entry onto the top of the undo stack. As a side effect, the redo stack + * is cleared when the mutation is applied, since a new commit invalidates any undone history. + * + * @param entry the changelog entry to push + * @throws GraphNotInATransactionException if no transaction is active + * @throws GraphNotInAWriteTransactionException if the active transaction is read-only + */ + public void push(ChangeLogEntry entry) { + checkWriteTransaction(); + pendingMutations.add( + () -> { + undoStack.push(entry); + redoStack.clear(); + }); + } + + /** + * Buffers clearing the entire redo stack. This is typically called before a regular (unnamed) + * commit to ensure that new changes invalidate the redo history. + * + * @throws GraphNotInATransactionException if no transaction is active + * @throws GraphNotInAWriteTransactionException if the active transaction is read-only + */ + public void clearRedo() { + checkWriteTransaction(); + pendingMutations.add(redoStack::clear); + } + + /** + * Buffers moving the top entry of the undo stack to the top of the redo stack. This is the + * write counterpart to {@link #peekUndo()}: the coordinator peeks to obtain the entry, then + * calls this method to buffer the actual stack move. + * + *

If the undo stack is empty when the mutation is applied, a {@link + * java.util.NoSuchElementException} will be thrown at commit time. + * + * @throws GraphNotInATransactionException if no transaction is active + * @throws GraphNotInAWriteTransactionException if the active transaction is read-only + */ + public void moveToRedo() { + checkWriteTransaction(); + pendingMutations.add( + () -> { + var entry = undoStack.pop(); + redoStack.push(entry); + }); + } + + /** + * Buffers moving the top entry of the redo stack back to the top of the undo stack. This is the + * write counterpart to {@link #peekRedo()}: the coordinator peeks to obtain the entry, then + * calls this method to buffer the actual stack move. + * + *

If the redo stack is empty when the mutation is applied, a {@link + * java.util.NoSuchElementException} will be thrown at commit time. + * + * @throws GraphNotInATransactionException if no transaction is active + * @throws GraphNotInAWriteTransactionException if the active transaction is read-only + */ + public void moveToUndo() { + checkWriteTransaction(); + pendingMutations.add( + () -> { + var entry = redoStack.pop(); + undoStack.push(entry); + }); + } + + /** + * Buffers restoring the changelog to the entry with the given version ID. All entries newer + * than the target are moved from the undo stack to the redo stack. If no entry with the given + * ID exists, the undo stack will be emptied entirely. + * + * @param versionId the {@link ChangeLogEntry#getChangeId() change ID} to restore to + * @throws GraphNotInATransactionException if no transaction is active + * @throws GraphNotInAWriteTransactionException if the active transaction is read-only + */ + public void restore(UUID versionId) { + checkWriteTransaction(); + pendingMutations.add( + () -> { + while (!undoStack.isEmpty() + && !undoStack.peek().getChangeId().equals(versionId)) { + redoStack.push(undoStack.pop()); + } + }); + } + + // ------------------------------------------------------------------------- + // read queries + // ------------------------------------------------------------------------- + + /** + * Returns the most recent entry on the undo stack without modifying it, or {@code null} if the + * undo stack is empty. This is the read counterpart to {@link #moveToRedo()}. + * + * @return the top undo entry, or {@code null} if the stack is empty + * @throws GraphNotInATransactionException if no transaction is active + */ + public ChangeLogEntry peekUndo() { + checkTransaction(); + return undoStack.peek(); + } + + /** + * Returns the most recent entry on the redo stack without modifying it, or {@code null} if the + * redo stack is empty. This is the read counterpart to {@link #moveToUndo()}. + * + * @return the top redo entry, or {@code null} if the stack is empty + * @throws GraphNotInATransactionException if no transaction is active + */ + public ChangeLogEntry peekRedo() { + checkTransaction(); + return redoStack.peek(); + } + + /** + * Returns whether the undo stack contains more than one entry. A single entry represents the + * initial (base) state and cannot be undone. + * + * @return {@code true} if there is at least one entry that can be undone + * @throws GraphNotInATransactionException if no transaction is active + */ + public boolean canUndo() { + checkTransaction(); + return undoStack.size() > 1; + } + + /** + * Returns whether the redo stack is non-empty, meaning at least one undone entry can be + * reapplied. + * + * @return {@code true} if there is at least one entry that can be redone + * @throws GraphNotInATransactionException if no transaction is active + */ + public boolean canRedo() { + checkTransaction(); + return !redoStack.isEmpty(); + } + + /** + * Returns an immutable snapshot of the undo stack, ordered from newest (index 0) to oldest. + * + * @return an unmodifiable list of undo entries + * @throws GraphNotInATransactionException if no transaction is active + */ + public List getUndoHistory() { + checkTransaction(); + return List.copyOf(undoStack); + } + + /** + * Returns an immutable snapshot of the redo stack, ordered from the entry that would be redone + * next (index 0) to the entry that was undone first (last index). + * + * @return an unmodifiable list of redo entries + * @throws GraphNotInATransactionException if no transaction is active + */ + public List getRedoHistory() { + checkTransaction(); + return List.copyOf(redoStack); + } + + // ------------------------------------------------------------------------- + // Internal + // ------------------------------------------------------------------------- + + private void checkTransaction() { + if (!txnContext.isInTransaction()) { + throw new GraphNotInATransactionException(); + } + } + + private void checkWriteTransaction() { + checkTransaction(); + if (txnContext.transactionMode() == ReadWrite.READ) { + throw new GraphNotInAWriteTransactionException(); + } + } +} diff --git a/backend/src/main/java/org/rdfarchitect/models/changelog/ChangeLogEntry.java b/backend/src/main/java/org/rdfarchitect/models/changelog/ChangeLogEntry.java index 45f2b9c8..4af953b4 100644 --- a/backend/src/main/java/org/rdfarchitect/models/changelog/ChangeLogEntry.java +++ b/backend/src/main/java/org/rdfarchitect/models/changelog/ChangeLogEntry.java @@ -19,11 +19,8 @@ import lombok.Data; -import org.apache.jena.graph.Graph; -import org.rdfarchitect.rdf.graph.DeltaCompressible; - -import java.lang.ref.WeakReference; import java.time.LocalDateTime; +import java.util.List; import java.util.UUID; @Data @@ -32,25 +29,14 @@ public class ChangeLogEntry { private UUID changeId; private LocalDateTime timestamp; private String message; - private WeakReference additions; - private WeakReference deletions; + private int steps; + private List contextDeltas; - public ChangeLogEntry(String message, DeltaCompressible delta) { - this.changeId = delta.getVersionId(); + public ChangeLogEntry(String message, int steps, List contextDeltas) { + this.changeId = UUID.randomUUID(); this.timestamp = LocalDateTime.now(); this.message = message; - - this.additions = new WeakReference<>(delta.getAdditions()); - this.deletions = new WeakReference<>(delta.getDeletions()); - - var additionsGraph = this.additions.get(); - var deletionsGraph = this.deletions.get(); - if (additionsGraph == null || deletionsGraph == null) { - return; - } - - if (additionsGraph.isEmpty() && deletionsGraph.isEmpty()) { - this.additions = new WeakReference<>(delta.getBase()); - } + this.steps = steps; + this.contextDeltas = contextDeltas; } } diff --git a/backend/src/main/java/org/rdfarchitect/models/changelog/ContextDelta.java b/backend/src/main/java/org/rdfarchitect/models/changelog/ContextDelta.java new file mode 100644 index 00000000..01b28aef --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/changelog/ContextDelta.java @@ -0,0 +1,25 @@ +/* + * 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.changelog; + +import org.apache.jena.graph.Graph; + +import java.lang.ref.WeakReference; + +public record ContextDelta( + String contextName, WeakReference additions, WeakReference deletions) {} diff --git a/backend/src/main/java/org/rdfarchitect/models/changelog/GraphChangeLog.java b/backend/src/main/java/org/rdfarchitect/models/changelog/GraphChangeLog.java deleted file mode 100644 index 1ce2c41f..00000000 --- a/backend/src/main/java/org/rdfarchitect/models/changelog/GraphChangeLog.java +++ /dev/null @@ -1,67 +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.models.changelog; - -import lombok.Getter; - -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Deque; -import java.util.List; -import java.util.UUID; - -public class GraphChangeLog { - - @Getter private final List entries = new ArrayList<>(); - private final Deque undoneDeque = new ArrayDeque<>(); - - public void addEntry(ChangeLogEntry entry) { - entries.add(entry); - } - - public void undoChange() { - if (!entries.isEmpty()) { - var undoneEntry = entries.remove(entries.size() - 1); - undoneDeque.push(undoneEntry); - } - } - - public void redoChange() { - if (!undoneDeque.isEmpty()) { - var redoneEntry = undoneDeque.pop(); - entries.add(redoneEntry); - } - } - - public void restore(UUID versionId) { - int index = -1; - for (int i = 0; i < entries.size(); i++) { - if (versionId.equals(entries.get(i).getChangeId())) { - index = i; - break; - } - } - - if (index != -1) { - undoneDeque.clear(); - for (int i = entries.size() - 1; i > index; i--) { - entries.remove(entries.size() - 1); - } - } - } -} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/CIMModifyingUtils.java b/backend/src/main/java/org/rdfarchitect/models/cim/CIMModifyingUtils.java new file mode 100644 index 00000000..9c18252f --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/CIMModifyingUtils.java @@ -0,0 +1,153 @@ +/* + * 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; + +import lombok.experimental.UtilityClass; + +import org.apache.jena.graph.Graph; +import org.apache.jena.graph.Node; +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.rdf.model.Statement; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import org.rdfarchitect.models.cim.data.dto.relations.RDFSComment; +import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; +import org.rdfarchitect.models.cim.rdf.resources.CIMS; + +@UtilityClass +public class CIMModifyingUtils { + + private static final String PACKAGE_PREFIX = "Package_"; + + /** + * Standardizes all Package prefixes in a cim graph. + * + * @param graph The graph holding the cim schema + * @param usePackagePrefix Whether the {@code Package_} prefix should be used or not + */ + public void standardizePackagePrefix(Graph graph, boolean usePackagePrefix) { + var model = ModelFactory.createModelForGraph(graph); + var packages = model.listResourcesWithProperty(RDF.type, CIMS.classCategory).toList(); + + for (var packageResource : packages) { + var labelStmt = packageResource.getProperty(RDFS.label); + if (labelStmt == null) { + continue; + } + + var currentLabel = labelStmt.getString(); + var currentUri = packageResource.getURI(); + var sepIdx = Math.max(currentUri.lastIndexOf('#'), currentUri.lastIndexOf('/')); + var uriBase = currentUri.substring(0, sepIdx + 1); + var uriSuffix = currentUri.substring(sepIdx + 1); + + var newLabel = applyPrefix(currentLabel, usePackagePrefix); + var newUriSuffix = applyPrefix(uriSuffix, usePackagePrefix); + var newUri = uriBase + newUriSuffix; + + applyChanges( + packageResource, labelStmt, currentUri, newUri, currentLabel, newLabel, model); + } + } + + private String applyPrefix(String value, boolean usePackagePrefix) { + if (usePackagePrefix) { + return value.startsWith(PACKAGE_PREFIX) ? value : PACKAGE_PREFIX + value; + } + return value.startsWith(PACKAGE_PREFIX) ? value.substring(PACKAGE_PREFIX.length()) : value; + } + + private void applyChanges( + Resource packageResource, + Statement labelStmt, + String currentUri, + String newUri, + String currentLabel, + String newLabel, + Model model) { + var uriChanged = !newUri.equals(currentUri); + var labelChanged = !newLabel.equals(currentLabel); + + if (uriChanged) { + changeURIAndLabel(packageResource, labelChanged, newUri, newLabel, model); + } else if (labelChanged) { + changeLabel(packageResource, labelStmt, newLabel); + } + } + + private void changeURIAndLabel( + Resource packageResource, + boolean labelChanged, + String newUri, + String newLabel, + Model model) { + var newResource = model.createResource(newUri); + packageResource + .listProperties() + .toList() + .forEach( + stmt -> { + if (stmt.getPredicate().equals(RDFS.label) && labelChanged) { + changeLabel(newResource, stmt, newLabel); + } else { + newResource.addProperty(stmt.getPredicate(), stmt.getObject()); + } + }); + + model.listStatements(null, null, packageResource) + .toList() + .forEach(stmt -> model.add(stmt.getSubject(), stmt.getPredicate(), newResource)); + model.listStatements(null, null, packageResource).toList().forEach(model::remove); + + model.removeAll(packageResource, null, null); + } + + private void changeLabel(Resource packageResource, Statement labelStmt, String newLabel) { + var lang = labelStmt.getLanguage(); + packageResource.removeAll(RDFS.label); + if (lang != null && !lang.isEmpty()) { + packageResource.addProperty(RDFS.label, newLabel, lang); + } else { + packageResource.addProperty(RDFS.label, newLabel); + } + } + + /** + * Replaces the current datatype of the comments with {@code xsd:string} + * + * @param graph the graph holding the cim schema + */ + public void replaceCommentDatatype(Graph graph) { + graph.find(Node.ANY, RDFS.comment.asNode(), Node.ANY) + .toList() + .forEach( + triple -> { + var newComment = + new RDFSComment( + triple.getObject().getLiteralLexicalForm(), + new URI("http://www.w3.org/2001/XMLSchema#string")); + graph.delete(triple); + graph.add( + triple.getSubject(), + RDFS.comment.asNode(), + newComment.asTypedLiteral().asNode()); + }); + } +} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/queries/update/CIMUpdates.java b/backend/src/main/java/org/rdfarchitect/models/cim/queries/update/CIMUpdates.java index 979c32ca..9e0a6c0f 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/queries/update/CIMUpdates.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/queries/update/CIMUpdates.java @@ -88,25 +88,19 @@ public void replaceClass( graph, prefixMapping, null, - newClass.getUuid().toString(), + newClass.getUuid(), newClass.getAttributes(), newValuesAsBlankNode); UpdateExecutionFactory.create(updateAttributes, dataset).execute(); // replace associations in database var updateAssociations = replaceAssociations( - prefixMapping, - null, - newClass.getUuid().toString(), - newClass.getAssociationPairs()); + prefixMapping, null, newClass.getUuid(), newClass.getAssociationPairs()); UpdateExecutionFactory.create(updateAssociations.build(), dataset).execute(); // replace enum entries in database var updateEnumEntries = replaceEnumEntries( - prefixMapping, - null, - newClass.getUuid().toString(), - newClass.getEnumEntries()); + prefixMapping, null, newClass.getUuid(), newClass.getEnumEntries()); UpdateExecutionFactory.create(updateEnumEntries.build(), dataset).execute(); // update other references to this class updateReferences(graph, prefixMapping, newClass.getUuid(), newClass.getUri()); @@ -224,7 +218,7 @@ public UUID insertUMLAdaptedClass( return newClass.getUuid(); } - public void deleteClass(Graph graph, PrefixMapping prefixMapping, String classUUID) { + public void deleteClass(Graph graph, PrefixMapping prefixMapping, UUID classUUID) { var dataset = SessionDataStore.wrapGraphInDataset(graph, null); UpdateExecutionFactory.create(deleteAttributes(prefixMapping, null, classUUID), dataset) .execute(); @@ -233,7 +227,7 @@ public void deleteClass(Graph graph, PrefixMapping prefixMapping, String classUU var classBaseDelete = deleteBase(prefixMapping, null, classUUID); UpdateExecutionFactory.create(classBaseDelete.build(), dataset).execute(); - deleteUuidIfNotReferencedAnyWhereElse(graph, UUID.fromString(classUUID)); + deleteUuidIfNotReferencedAnyWhereElse(graph, classUUID); } /** @@ -366,7 +360,7 @@ private void appendValueNode( * UpdateRequest}. */ private UpdateRequest deleteAttributes( - PrefixMapping prefixMapping, String graphURI, String classUUID) { + PrefixMapping prefixMapping, String graphURI, UUID classUUID) { var request = deleteAttributeValueNodesForClass(graphURI, classUUID); request.add( new CIMBaseUpdateBuilder() @@ -374,7 +368,7 @@ private UpdateRequest deleteAttributes( .setGraph(graphURI) .build() .addDelete(CIMQueryVars.URI, ANY_1, ANY_2) - .addWhere(CIMQueryVars.DOMAIN_URI, RDFA.uuid, classUUID) + .addWhere(CIMQueryVars.DOMAIN_URI, RDFA.uuid, classUUID.toString()) .addWhere(CIMQueryVars.URI, RDFS.domain, CIMQueryVars.DOMAIN_URI) .addWhere(CIMQueryVars.URI, CIMS.stereotype, CIMStereotypes.attribute) .addWhere(CIMQueryVars.URI, ANY_1, ANY_2) @@ -393,7 +387,7 @@ public UpdateRequest replaceAttributes( Graph graph, PrefixMapping prefixMapping, String graphURI, - String classUUID, + UUID classUUID, List attributes, boolean newValuesAsBlankNode) { var request = deleteAttributes(prefixMapping, graphURI, classUUID); @@ -448,9 +442,9 @@ private UpdateRequest deleteAttributeValueNodes(String graphURI, UUID attributeU * the given class. {@code classUUID} is passed through Jena's node coercion rather than * interpolated. */ - private UpdateRequest deleteAttributeValueNodesForClass(String graphURI, String classUUID) { + private UpdateRequest deleteAttributeValueNodesForClass(String graphURI, UUID classUUID) { var request = new UpdateRequest(); - if (classUUID == null || classUUID.isBlank()) { + if (classUUID == null) { return request; } for (var predicate : VALUE_NODE_PREDICATES) { @@ -459,7 +453,7 @@ private UpdateRequest deleteAttributeValueNodesForClass(String graphURI, String .setGraph(graphURI) .build() .addDelete(VAR_BLANK, VAR_BLANK_P, VAR_BLANK_O) - .addWhere(VAR_CLASS, RDFA.uuid, classUUID) + .addWhere(VAR_CLASS, RDFA.uuid, classUUID.toString()) .addWhere(VAR_ATTRIBUTE, RDFS.domain, VAR_CLASS) .addWhere(VAR_ATTRIBUTE, CIMS.stereotype, CIMStereotypes.attribute) .addWhere(VAR_ATTRIBUTE, predicate, VAR_BLANK) @@ -552,14 +546,14 @@ private void appendInsertAssociation(UpdateBuilder baseUpdate, CIMAssociation as } private UpdateBuilder deleteAssociations( - PrefixMapping prefixMapping, String graphURI, String classUUID) { + PrefixMapping prefixMapping, String graphURI, UUID classUUID) { // init baseUpdate var baseUpdate = new CIMBaseUpdateBuilder().addPrefixes(prefixMapping).setGraph(graphURI).build(); return baseUpdate .addDelete(CIMQueryVars.URI, ANY_1, ANY_2) .addDelete(CIMQueryVars.Inverse.URI, ANY_3, ANY_4) - .addWhere(CIMQueryVars.DOMAIN_URI, RDFA.uuid, classUUID) + .addWhere(CIMQueryVars.DOMAIN_URI, RDFA.uuid, classUUID.toString()) .addWhere(CIMQueryVars.URI, RDFS.domain, CIMQueryVars.DOMAIN_URI) .addWhere(CIMQueryVars.URI, ANY_1, ANY_2) .addWhere(CIMQueryVars.URI, CIMS.inverseRoleName, CIMQueryVars.Inverse.URI) @@ -569,7 +563,7 @@ private UpdateBuilder deleteAssociations( public UpdateBuilder replaceAssociations( PrefixMapping prefixMapping, String graphURI, - String classUUID, + UUID classUUID, List associationPairs) { var baseUpdate = CIMUpdates.deleteAssociations(prefixMapping, graphURI, classUUID); var seenFromUUIDs = new HashSet(); @@ -602,13 +596,12 @@ public UpdateBuilder replaceClassBase( .addOptional(CIMQueryVars.URI, "?pre", "?obj"); } - public UpdateBuilder deleteBase( - PrefixMapping prefixMapping, String graphURI, String classUUID) { + public UpdateBuilder deleteBase(PrefixMapping prefixMapping, String graphURI, UUID classUUID) { var classBaseUpdate = new CIMBaseUpdateBuilder().addPrefixes(prefixMapping).setGraph(graphURI).build(); classBaseUpdate .addDelete(CIMQueryVars.URI, "?pre", "?obj") - .addWhere(CIMQueryVars.URI, RDFA.uuid, classUUID) + .addWhere(CIMQueryVars.URI, RDFA.uuid, classUUID.toString()) .addOptional(CIMQueryVars.URI, "?pre", "?obj") .addFilter(new ExprFactory().ne("?pre", RDFA.uuid)); @@ -649,22 +642,22 @@ public void insertEnumEntry( UpdateExecutionFactory.create(insertEnumEntry.build(), dataset).execute(); } - public void deleteEnumEntry(Graph graph, PrefixMapping prefixMapping, String enumEntryUUID) { + public void deleteEnumEntry(Graph graph, PrefixMapping prefixMapping, UUID enumEntryUUID) { var dataset = SessionDataStore.wrapGraphInDataset(graph, null); var deleteEnumEntry = deleteBase(prefixMapping, null, enumEntryUUID); UpdateExecutionFactory.create(deleteEnumEntry.build(), dataset).execute(); - deleteUuidIfNotReferencedAnyWhereElse(graph, UUID.fromString(enumEntryUUID)); + deleteUuidIfNotReferencedAnyWhereElse(graph, enumEntryUUID); } private UpdateBuilder deleteEnumEntries( - PrefixMapping prefixMapping, String graphURI, String classUUID) { + PrefixMapping prefixMapping, String graphURI, UUID classUUID) { return new CIMBaseUpdateBuilder() .addPrefixes(prefixMapping) .setGraph(graphURI) .build() .addDelete(CIMQueryVars.URI, ANY_1, ANY_2) - .addWhere(CIMQueryVars.TYPE_URI, RDFA.uuid, classUUID) + .addWhere(CIMQueryVars.TYPE_URI, RDFA.uuid, classUUID.toString()) .addWhere(CIMQueryVars.URI, RDF.type, CIMQueryVars.TYPE_URI) .addWhere(CIMQueryVars.URI, ANY_1, ANY_2); } @@ -672,7 +665,7 @@ private UpdateBuilder deleteEnumEntries( public UpdateBuilder replaceEnumEntries( PrefixMapping prefixMapping, String graphURI, - String classUUID, + UUID classUUID, List enumEntries) { var baseUpdate = CIMUpdates.deleteEnumEntries(prefixMapping, graphURI, classUUID); for (CIMEnumEntry enumEntry : enumEntries) { @@ -682,7 +675,7 @@ public UpdateBuilder replaceEnumEntries( } public void replaceEnumEntry(Graph graph, PrefixMapping prefixMapping, CIMEnumEntry enumEntry) { - deleteEnumEntry(graph, prefixMapping, enumEntry.getUuid().toString()); + deleteEnumEntry(graph, prefixMapping, enumEntry.getUuid()); insertEnumEntry(graph, prefixMapping, enumEntry); } @@ -692,7 +685,7 @@ public void replacePackage(Graph graph, PrefixMapping prefixMapping, CIMPackage updateReferences(graph, prefixMapping, newPackage.getUuid(), newPackage.getUri()); // delete old package - deletePackage(graph, prefixMapping, newPackage.getUuid().toString()); + deletePackage(graph, prefixMapping, newPackage.getUuid()); // insert new package insertPackage(graph, prefixMapping, newPackage); @@ -722,12 +715,12 @@ public void insertPackage(Graph graph, PrefixMapping prefixMapping, CIMPackage n UpdateExecutionFactory.create(packageUpdateBuilder.build(), dataset).execute(); } - public void deletePackage(Graph graph, PrefixMapping prefixMapping, String packageUUID) { + public void deletePackage(Graph graph, PrefixMapping prefixMapping, UUID packageUUID) { var dataset = SessionDataStore.wrapGraphInDataset(graph, null); var packageBaseDelete = deleteBase(prefixMapping, null, packageUUID); UpdateExecutionFactory.create(packageBaseDelete.build(), dataset).execute(); - deleteUuidIfNotReferencedAnyWhereElse(graph, UUID.fromString(packageUUID)); + deleteUuidIfNotReferencedAnyWhereElse(graph, packageUUID); } private void updateReferences(Graph graph, PrefixMapping prefixMapping, UUID uuid, URI newURI) { 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 012d8412..6a2ec448 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 @@ -19,7 +19,10 @@ import lombok.experimental.UtilityClass; +import org.apache.jena.graph.Graph; +import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDFS; import org.rdfarchitect.models.cim.rdf.resources.RDFA; import java.util.UUID; @@ -43,7 +46,7 @@ public boolean isExternalResource(Resource resource) { /** * Finds the uuid of a resource. * - * @param resource The resource to finde the uuid for. + * @param resource The resource to find the uuid for. * @return The uuid of the resource. */ public UUID findUuidForResource(Resource resource) { @@ -52,4 +55,33 @@ public UUID findUuidForResource(Resource resource) { } return UUID.fromString(resource.getProperty(RDFA.uuid).getString()); } + + public Resource findResourceForUuid(Graph graph, UUID uuid) { + var model = ModelFactory.createModelForGraph(graph); + var results = model.listSubjectsWithProperty(RDFA.uuid, uuid.toString()).toList(); + if (results.isEmpty()) { + throw new IllegalStateException("No resource with UUID " + uuid + " found in graph."); + } + if (results.size() > 1) { + throw new IllegalStateException( + "Multiple resources with UUID " + uuid + " found in graph."); + } + return results.getFirst(); + } + + public Resource findResourceForUri(Graph graph, String uri) { + var model = ModelFactory.createModelForGraph(graph); + var resource = model.getResource(uri); + if (!resource.listProperties().hasNext()) { + throw new IllegalStateException("No resource with URI " + uri + " found in graph."); + } + return resource; + } + + public String findLabelForResource(Resource resource) { + if (!resource.hasProperty(RDFS.label)) { + throw new IllegalStateException("Resource " + resource + " does not have a label."); + } + return resource.getProperty(RDFS.label).getString(); + } } diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/rendering/RenderCIMCollectionUseCase.java b/backend/src/main/java/org/rdfarchitect/models/cim/rendering/RenderCIMCollectionUseCase.java index 0d241041..a6480a1f 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/rendering/RenderCIMCollectionUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/rendering/RenderCIMCollectionUseCase.java @@ -17,8 +17,8 @@ package org.rdfarchitect.models.cim.rendering; +import org.rdfarchitect.api.dto.dl.RenderingLayoutData; import org.rdfarchitect.api.dto.rendering.RenderingDataDTO; -import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.models.cim.data.dto.CIMCollection; import java.util.UUID; @@ -29,21 +29,18 @@ public interface RenderCIMCollectionUseCase { /** - * Generates the rendering data for a CIMCollection, that belongs to a specific graph. + * Generates the rendering data for a CIMCollection using pre-fetched layout data. * * @param cimCollection the CIMCollection to be converted - * @param graphIdentifier the dataset name and graph uri of the graph that the collection - * belongs to - * @param diagramId the id of the package or custom diagram that should be rendered + * @param layoutData pre-fetched diagram layout data (may be null for mermaid) * @return a dto that contains all data required to render a UML diagram for the given * collection */ - RenderingDataDTO renderUML( - CIMCollection cimCollection, GraphIdentifier graphIdentifier, UUID diagramId); + RenderingDataDTO renderUML(CIMCollection cimCollection, RenderingLayoutData layoutData); /** - * Generates the rendering data for a CIMCollection, that belongs not to one specific graph, but - * the dataset as a whole. + * Generates the rendering data for a CIMCollection that belongs to a dataset-level custom + * diagram. * * @param cimCollection the CIMCollection to be converted * @param datasetName the dataset name that the collection belongs to diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/rendering/mermaid/RenderCIMCollectionMermaidService.java b/backend/src/main/java/org/rdfarchitect/models/cim/rendering/mermaid/RenderCIMCollectionMermaidService.java index bc0a0d2c..4cf01d47 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/rendering/mermaid/RenderCIMCollectionMermaidService.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/rendering/mermaid/RenderCIMCollectionMermaidService.java @@ -19,9 +19,9 @@ import lombok.Getter; +import org.rdfarchitect.api.dto.dl.RenderingLayoutData; import org.rdfarchitect.api.dto.rendering.RenderingDataDTO; import org.rdfarchitect.api.dto.rendering.mermaid.MermaidDTO; -import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.models.cim.data.dto.CIMClass; import org.rdfarchitect.models.cim.data.dto.CIMCollection; import org.rdfarchitect.models.cim.data.dto.CIMPackage; @@ -46,8 +46,7 @@ public class RenderCIMCollectionMermaidService implements RenderCIMCollectionUseCase { @Override - public RenderingDataDTO renderUML( - CIMCollection cimCollection, GraphIdentifier graphIdentifier, UUID diagramId) { + public RenderingDataDTO renderUML(CIMCollection cimCollection, RenderingLayoutData layoutData) { if (!RenderingUtils.hasRenderableClasses(cimCollection)) { return null; } @@ -79,7 +78,7 @@ public RenderingDataDTO renderUML( public RenderingDataDTO renderGlobalUML( CIMCollection cimCollection, String datasetName, UUID diagramId) { throw new UnsupportedOperationException( - "Rendering dataset Diagrams is not supported for the mermaid renderer."); + "Rendering dataset diagrams is not supported for the mermaid renderer."); } private static final String ON_CLICK_CALLBACK_FUNCTION_NAME = "getClassInformation"; diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/rendering/svelteflow/RenderCIMCollectionSvelteFlowService.java b/backend/src/main/java/org/rdfarchitect/models/cim/rendering/svelteflow/RenderCIMCollectionSvelteFlowService.java index c0657a20..2b10ca6d 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/rendering/svelteflow/RenderCIMCollectionSvelteFlowService.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/rendering/svelteflow/RenderCIMCollectionSvelteFlowService.java @@ -17,8 +17,6 @@ package org.rdfarchitect.models.cim.rendering.svelteflow; -import lombok.RequiredArgsConstructor; - import org.rdfarchitect.api.dto.dl.RenderingLayoutData; import org.rdfarchitect.api.dto.rendering.RenderingDataDTO; import org.rdfarchitect.api.dto.rendering.svelteflow.SvelteFlowDTO; @@ -28,7 +26,6 @@ import org.rdfarchitect.api.dto.rendering.svelteflow.sub.NodeDTO; import org.rdfarchitect.api.dto.rendering.svelteflow.sub.NodeDataDTO; import org.rdfarchitect.api.dto.rendering.svelteflow.sub.PositionDTO; -import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.models.cim.data.dto.CIMAssociation; import org.rdfarchitect.models.cim.data.dto.CIMClass; import org.rdfarchitect.models.cim.data.dto.CIMCollection; @@ -40,7 +37,6 @@ import org.rdfarchitect.models.cim.rendering.RenderCIMCollectionUseCase; import org.rdfarchitect.models.cim.rendering.RenderingUtils; import org.rdfarchitect.services.dl.select.FetchRenderingLayoutDataUseCase; -import org.rdfarchitect.services.dl.update.EnsureDiagramLayoutForCIMCollectionUseCase; import org.springframework.util.CollectionUtils; import java.util.ArrayList; @@ -53,54 +49,44 @@ * Converts a {@link CIMCollection} to a DTO Record that contains two JSON arrays with nodes and * edges used to render a UML diagram using the JavaScript library SvelteFlow. */ -@RequiredArgsConstructor public class RenderCIMCollectionSvelteFlowService implements RenderCIMCollectionUseCase { + private final FetchRenderingLayoutDataUseCase fetchRenderingLayoutDataUseCase; + + public RenderCIMCollectionSvelteFlowService( + FetchRenderingLayoutDataUseCase fetchRenderingLayoutDataUseCase) { + this.fetchRenderingLayoutDataUseCase = fetchRenderingLayoutDataUseCase; + } + // CONSTANTS FOR SVELTEFLOW CUSTOM NODE/EDGE TYPES private static final String CLASS_NODE_TYPE = "class"; private static final String INHERITANCE_EDGE_TYPE = "inheritance"; private static final String ASSOCIATION_EDGE_TYPE = "association"; - private final FetchRenderingLayoutDataUseCase fetchRenderingLayoutDataUseCase; - private final EnsureDiagramLayoutForCIMCollectionUseCase - ensureDiagramLayoutForCIMCollectionUseCase; @Override - public RenderingDataDTO renderUML( - CIMCollection cimCollection, GraphIdentifier graphIdentifier, UUID diagramId) { + public RenderingDataDTO renderUML(CIMCollection cimCollection, RenderingLayoutData layoutData) { if (!RenderingUtils.hasRenderableClasses(cimCollection)) { return createEmptyDiagram(); } - ensureDiagramLayoutForCIMCollectionUseCase.ensureDiagramLayoutExists( - graphIdentifier, diagramId, cimCollection); - var renderingLayoutData = - fetchRenderingLayoutDataUseCase.fetchRenderingLayoutData( - graphIdentifier, diagramId); - return renderUML(cimCollection, renderingLayoutData); + var uriToUUIDMap = RenderingUtils.createUUIDUriPairs(cimCollection); + var renderContext = new RenderContext(cimCollection, uriToUUIDMap, layoutData); + + var nodes = assembleNodeDTOList(renderContext); + var edges = assembleEdgeDTOList(renderContext); + + return SvelteFlowDTO.builder().nodes(nodes).edges(edges).build(); } @Override public RenderingDataDTO renderGlobalUML( CIMCollection cimCollection, String datasetName, UUID diagramId) { - ensureDiagramLayoutForCIMCollectionUseCase.ensureDiagramLayoutExists( - datasetName, diagramId, cimCollection); var renderingLayoutData = fetchRenderingLayoutDataUseCase.fetchGlobalRenderingLayoutData( datasetName, diagramId); return renderUML(cimCollection, renderingLayoutData); } - private RenderingDataDTO renderUML( - CIMCollection cimCollection, RenderingLayoutData renderingLayoutData) { - var uriToUUIDMap = RenderingUtils.createUUIDUriPairs(cimCollection); - var renderContext = new RenderContext(cimCollection, uriToUUIDMap, renderingLayoutData); - - var nodes = assembleNodeDTOList(renderContext); - var edges = assembleEdgeDTOList(renderContext); - - return SvelteFlowDTO.builder().nodes(nodes).edges(edges).build(); - } - private SvelteFlowDTO createEmptyDiagram() { return SvelteFlowDTO.builder().nodes(List.of()).edges(List.of()).build(); } @@ -125,7 +111,13 @@ private List assembleNodeDTOList(RenderContext renderContext) { * @return NodeDTO containing class data */ private NodeDTO assembleNodeDTO(RenderContext renderContext, CIMClass cimClass) { - var dop = renderContext.layoutingData().getClassLayoutingData().get(cimClass.getUuid()); + var dop = + renderContext.layoutingData() != null + ? renderContext + .layoutingData() + .getClassLayoutingData() + .get(cimClass.getUuid()) + : null; var nodeDTO = NodeDTO.builder().id(cimClass.getUuid()).type(CLASS_NODE_TYPE); @@ -134,11 +126,13 @@ private NodeDTO assembleNodeDTO(RenderContext renderContext, CIMClass cimClass) var enumEntries = getClassEnumEntries(renderContext, cimClass); var positionDTO = - PositionDTO.builder() - .x(dop.getPosition().getX()) - .y(dop.getPosition().getY()) - .z(dop.getPosition().getZ()) - .build(); + dop != null + ? PositionDTO.builder() + .x(dop.getPosition().getX()) + .y(dop.getPosition().getY()) + .z(dop.getPosition().getZ()) + .build() + : PositionDTO.builder().x(0).y(0).z(0).build(); nodeDTO.position(positionDTO); var nodeDataDTO = diff --git a/backend/src/main/java/org/rdfarchitect/rdf/graph/GraphUtils.java b/backend/src/main/java/org/rdfarchitect/rdf/graph/GraphUtils.java index 4e897fc1..c0fa16ed 100644 --- a/backend/src/main/java/org/rdfarchitect/rdf/graph/GraphUtils.java +++ b/backend/src/main/java/org/rdfarchitect/rdf/graph/GraphUtils.java @@ -21,12 +21,40 @@ import lombok.NoArgsConstructor; import org.apache.jena.graph.Graph; +import org.apache.jena.graph.Node; +import org.apache.jena.graph.NodeFactory; +import org.apache.jena.graph.Triple; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.Statement; import org.apache.jena.sparql.graph.GraphFactory; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import org.rdfarchitect.models.cim.rdf.resources.RDFA; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** Utility class for graph operations */ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class GraphUtils { + private static final Set RELEVANT_TYPES = + Set.of(RDF.Property.toString(), RDFS.Class.toString()); + public static Graph deepCopy(Graph graph) { var newGraph = GraphFactory.createDefaultGraph(); var iterator = graph.find(); @@ -35,4 +63,144 @@ public static Graph deepCopy(Graph graph) { } return newGraph; } + + public static Graph enhanceWithUUIDs(Graph graph) { + var model = ModelFactory.createModelForGraph(graph); + addUUIDsToTypedResources(model); + addUUIDsToReferencedOnlyResources(model); + return graph; + } + + public static void removeUUIDs(Graph graph) { + graph.find(Node.ANY, RDFA.uuid.asNode(), Node.ANY).toList().forEach(graph::delete); + } + + private static void addUUIDsToTypedResources(Model model) { + var subjects = + model.listResourcesWithProperty(RDF.type) + .filterKeep(r -> r.isURIResource() && !r.hasProperty(RDFA.uuid)) + .toSet(); + for (var subject : subjects) { + subject.addProperty(RDFA.uuid, UUID.randomUUID().toString()); + } + } + + 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(GraphUtils::isReferencedOnlyURI) + .mapWith(RDFNode::asResource) + .forEachRemaining(objects::add)); + objects.forEach(o -> o.addProperty(RDFA.uuid, UUID.randomUUID().toString())); + } + + private static boolean hasAnyType(Resource resource) { + return resource.listProperties(RDF.type) + .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(); + } + + /** + * Returns a copy of {@code graph} where every blank node is relabelled with a deterministic, + * content-addressed ID derived from hashing its structural neighbourhood. Re-parsing the same + * RDF content produces the same blank-node labels, so diffing two versions of the graph yields + * only real semantic changes rather than spurious blank-node renames. + */ + public static Graph normalizeBlankNodes(Graph graph) { + var triples = graph.find().toList(); + var blankNodes = + triples.stream() + .flatMap(t -> Stream.of(t.getSubject(), t.getObject())) + .filter(Node::isBlank) + .collect(Collectors.toSet()); + + var result = GraphFactory.createDefaultGraph(); + if (blankNodes.isEmpty()) { + triples.forEach(result::add); + } else { + var hashes = computeBlankNodeHashes(triples, blankNodes); + for (var t : triples) { + result.add( + Triple.create( + remapBlank(t.getSubject(), hashes), + t.getPredicate(), + remapBlank(t.getObject(), hashes))); + } + } + result.getPrefixMapping().setNsPrefixes(graph.getPrefixMapping()); + return result; + } + + private static Node remapBlank(Node n, Map hashes) { + return n.isBlank() ? NodeFactory.createBlankNode(hashes.get(n)) : n; + } + + private static Map computeBlankNodeHashes( + List triples, Set blankNodes) { + Map hashes = new HashMap<>(); + blankNodes.forEach(bn -> hashes.put(bn, "")); + + for (int pass = 0; pass < 10; pass++) { + var next = new HashMap(); + var changed = false; + for (var bn : blankNodes) { + var h = blankNodeFingerprint(bn, triples, hashes); + next.put(bn, h); + if (!h.equals(hashes.get(bn))) { + changed = true; + } + } + hashes.putAll(next); + if (!changed) { + break; + } + } + return hashes; + } + + private static String blankNodeFingerprint( + Node bn, List triples, Map hashes) { + var lines = new ArrayList(); + for (var t : triples) { + if (t.getSubject().equals(bn)) { + lines.add("o:" + t.getPredicate() + "=" + nodeLabel(t.getObject(), hashes)); + } + if (t.getObject().equals(bn)) { + lines.add("s:" + t.getPredicate() + "=" + nodeLabel(t.getSubject(), hashes)); + } + } + Collections.sort(lines); + return sha256prefix(String.join("|", lines)); + } + + private static String nodeLabel(Node n, Map hashes) { + return n.isBlank() ? ("B:" + hashes.getOrDefault(n, "")) : n.toString(); + } + + private static String sha256prefix(String input) { + try { + var digest = MessageDigest.getInstance("SHA-256"); + var bytes = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + var sb = new StringBuilder(32); + for (int i = 0; i < 16; i++) { + sb.append(String.format("%02x", bytes[i])); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } } diff --git a/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/DiagramLayoutDelta.java b/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/DiagramLayoutDelta.java new file mode 100644 index 00000000..0e0e0daf --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/DiagramLayoutDelta.java @@ -0,0 +1,125 @@ +/* + * 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.rdf.graph.wrapper; + +import lombok.Getter; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.sparql.graph.GraphFactory; +import org.apache.jena.vocabulary.RDF; +import org.rdfarchitect.config.GraphCompressionConfig; +import org.rdfarchitect.dl.data.dto.relations.MRID; +import org.rdfarchitect.dl.rdf.resources.CIM; +import org.rdfarchitect.rdf.graph.DeltaCompressible; + +import java.util.UUID; + +/** + * Transactional diagram-layout store backed by an {@link RDFGraphDelta}. Has no lock of its own — + * transaction lifecycle is managed exclusively by the owning coordinator. + */ +public class DiagramLayoutDelta implements TransactionParticipant, Rewindable { + + @Getter private final MRID defaultPackageMRID; + private final RDFGraphDelta inner; + + public DiagramLayoutDelta(TransactionContext txnContext) { + this.defaultPackageMRID = new MRID(UUID.randomUUID()); + var emptyBase = GraphFactory.createDefaultGraph(); + var prefixModel = ModelFactory.createModelForGraph(emptyBase); + prefixModel.setNsPrefix(CIM.PREFIX, CIM.NAMESPACE); + prefixModel.setNsPrefix("rdf", RDF.uri); + int maxVersions = GraphCompressionConfig.getMaxVersions(); + int compressCount = GraphCompressionConfig.getCompressCount(); + this.inner = new RDFGraphDelta(emptyBase, maxVersions, compressCount, txnContext); + } + + /** + * Returns a live {@link Model} view of the current diagram-layout state. Modifications to the + * returned model are written into the active delta and will be committed or aborted together + * with the enclosing transaction. + */ + public Model getDiagramLayoutModel() { + return ModelFactory.createModelForGraph(inner); + } + + /** + * Returns a live {@link Model} view of the last committed diagram-layout state, + * bypassing the transaction layer. Reads and writes succeed without an active transaction. + * Writes go directly into the committed head delta and are not tracked as a separate undo entry + * — they survive undo/redo of semantic changes. Use this for infrastructure operations (layout + * initialisation, auto-positions) that must not pollute the undo history. + */ + public Model getDiagramLayoutModelDirect() { + return ModelFactory.createModelForGraph(inner.getLastDelta()); + } + + // ------------------------------------------------------------------------- + // TransactionParticipant + // ------------------------------------------------------------------------- + + @Override + public void commit() { + inner.commit(); + } + + @Override + public void abort() { + inner.abort(); + } + + @Override + public boolean hasChanges() { + return inner.hasChanges(); + } + + // ------------------------------------------------------------------------- + // Rewindable + // ------------------------------------------------------------------------- + + @Override + public void undo() { + inner.undo(); + } + + @Override + public void redo() { + inner.redo(); + } + + @Override + public boolean canUndo() { + return inner.canUndo(); + } + + @Override + public boolean canRedo() { + return inner.canRedo(); + } + + @Override + public void restore(UUID versionId) { + inner.restore(versionId); + } + + @Override + public DeltaCompressible getLastDelta() { + return inner.getLastDelta(); + } +} diff --git a/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/GraphRewindable.java b/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/GraphRewindable.java index d1232be5..10c47678 100644 --- a/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/GraphRewindable.java +++ b/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/GraphRewindable.java @@ -55,6 +55,7 @@ * will delete all previously following changes. this implementation ensures thread safety by * utilizing the single-writer multiple-reader principle (SWMR). */ +@Deprecated public class GraphRewindable implements Graph, Transactional, Rewindable { // logger @@ -77,8 +78,7 @@ public class GraphRewindable implements Graph, Transactional, Rewindable { protected final int compressCount; /** - * 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 GraphRewindable}. * * @param base The base graph * @param maxVersions The maximum amount of versions the graph stores. 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 a34d5085..6d292c72 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 @@ -18,31 +18,16 @@ package org.rdfarchitect.rdf.graph.wrapper; import org.apache.jena.graph.Graph; -import org.apache.jena.graph.Node; import org.apache.jena.query.ReadWrite; -import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.ModelFactory; -import org.apache.jena.rdf.model.RDFNode; -import org.apache.jena.rdf.model.Resource; -import org.apache.jena.rdf.model.Statement; -import org.apache.jena.vocabulary.RDF; -import org.apache.jena.vocabulary.RDFS; import org.jetbrains.annotations.NotNull; import org.rdfarchitect.exception.graph.GraphNotInATransactionException; import org.rdfarchitect.exception.graph.GraphTransactionException; -import org.rdfarchitect.models.cim.rdf.resources.CIMS; -import org.rdfarchitect.models.cim.rdf.resources.RDFA; import org.rdfarchitect.rdf.graph.DeltaCompressible; +import org.rdfarchitect.rdf.graph.GraphUtils; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; - +@Deprecated public class GraphRewindableWithUUIDs extends GraphRewindable { - 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}. @@ -53,7 +38,7 @@ public class GraphRewindableWithUUIDs extends GraphRewindable { * compressing. */ public GraphRewindableWithUUIDs(@NotNull Graph base, int maxVersions, int compressCount) { - super(enhanceWithUUIDs(base), maxVersions, compressCount); + super(GraphUtils.enhanceWithUUIDs(base), maxVersions, compressCount); } @Override @@ -68,7 +53,7 @@ public void commit() { logger.debug("Commiting a transaction with no changes."); return; } - enhanceWithUUIDs(this); + GraphUtils.enhanceWithUUIDs(this); pastDeltas.push(currentDelta); assert pastDeltas.peek() != null; currentDelta = new DeltaCompressible(pastDeltas.peek()); @@ -78,145 +63,4 @@ public void commit() { } logger.debug("Committed transaction."); } - - static Graph enhanceWithUUIDs(Graph graph) { - var model = ModelFactory.createModelForGraph(graph); - addUUIDsToTypedResources(model); - addUUIDsToReferencedOnlyResources(model); - return graph; - } - - private static void addUUIDsToTypedResources(Model model) { - var subjects = - model.listResourcesWithProperty(RDF.type) - .filterKeep(r -> r.isURIResource() && !r.hasProperty(RDFA.uuid)) - .toSet(); - - for (var subject : subjects) { - subject.addProperty(RDFA.uuid, createUUID()); - } - } - - 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)); - - 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(); - } - - private static boolean isReferencedOnlyURI(RDFNode node) { - return node.isURIResource() - && !RELEVANT_TYPES.contains(node.asResource().getURI()) - && !node.asResource().hasProperty(RDFA.uuid) - && !node.asResource().listProperties().hasNext(); - } - - private static String createUUID() { - return UUID.randomUUID().toString(); - } - - public static void removeUUIDs(Graph graph) { - graph.find(Node.ANY, RDFA.uuid.asNode(), Node.ANY).toList().forEach(graph::delete); - } - - public static void correctPackagePrefix(Graph graph, boolean usePackagePrefix) { - var model = ModelFactory.createModelForGraph(graph); - var packages = model.listResourcesWithProperty(RDF.type, CIMS.classCategory).toList(); - - final var PREFIX = "Package_"; - - for (var packageResource : packages) { - var labelStmt = packageResource.getProperty(RDFS.label); - if (labelStmt == null) { - continue; - } - - var currentLabel = labelStmt.getString(); - var currentUri = packageResource.getURI(); - var sepIdx = Math.max(currentUri.lastIndexOf('#'), currentUri.lastIndexOf('/')); - var uriBase = currentUri.substring(0, sepIdx + 1); - var uriSuffix = currentUri.substring(sepIdx + 1); - - String newLabel; - String newUriSuffix; - - if (usePackagePrefix) { - newLabel = currentLabel.startsWith(PREFIX) ? currentLabel : PREFIX + currentLabel; - newUriSuffix = uriSuffix.startsWith(PREFIX) ? uriSuffix : PREFIX + uriSuffix; - } else { - newLabel = - currentLabel.startsWith(PREFIX) - ? currentLabel.substring(PREFIX.length()) - : currentLabel; - newUriSuffix = - uriSuffix.startsWith(PREFIX) - ? uriSuffix.substring(PREFIX.length()) - : uriSuffix; - } - - var newUri = uriBase + newUriSuffix; - var uriChanged = !newUri.equals(currentUri); - var labelChanged = !newLabel.equals(currentLabel); - - if (uriChanged) { - changeURIAndLabel(packageResource, labelChanged, newUri, newLabel, model); - } else if (labelChanged) { - changeLabel(packageResource, labelStmt, newLabel); - } - } - } - - private static void changeURIAndLabel( - Resource packageResource, - boolean labelChanged, - String newUri, - String newLabel, - Model model) { - var newResource = model.createResource(newUri); - packageResource - .listProperties() - .toList() - .forEach( - stmt -> { - if (stmt.getPredicate().equals(RDFS.label) && labelChanged) { - changeLabel(newResource, stmt, newLabel); - } else { - newResource.addProperty(stmt.getPredicate(), stmt.getObject()); - } - }); - - model.listStatements(null, null, packageResource) - .toList() - .forEach(stmt -> model.add(stmt.getSubject(), stmt.getPredicate(), newResource)); - model.listStatements(null, null, packageResource).toList().forEach(model::remove); - - model.removeAll(packageResource, null, null); - } - - private static void changeLabel( - Resource packageResource, Statement labelStmt, String newLabel) { - var lang = labelStmt.getLanguage(); - packageResource.removeAll(RDFS.label); - if (lang != null && !lang.isEmpty()) { - packageResource.addProperty(RDFS.label, newLabel, lang); - } else { - packageResource.addProperty(RDFS.label, newLabel); - } - } } diff --git a/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/RDFGraphDelta.java b/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/RDFGraphDelta.java new file mode 100644 index 00000000..72d4078b --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/RDFGraphDelta.java @@ -0,0 +1,299 @@ +/* + * 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.rdf.graph.wrapper; + +import org.apache.jena.graph.Graph; +import org.apache.jena.graph.GraphEventManager; +import org.apache.jena.graph.Node; +import org.apache.jena.graph.TransactionHandler; +import org.apache.jena.graph.Triple; +import org.apache.jena.query.ReadWrite; +import org.apache.jena.shared.AddDeniedException; +import org.apache.jena.shared.DeleteDeniedException; +import org.apache.jena.shared.PrefixMapping; +import org.apache.jena.util.iterator.ExtendedIterator; +import org.jetbrains.annotations.NotNull; +import org.rdfarchitect.exception.graph.GraphNotInATransactionException; +import org.rdfarchitect.exception.graph.GraphNotInAWriteTransactionException; +import org.rdfarchitect.exception.graph.GraphVersionControlException; +import org.rdfarchitect.rdf.graph.DeltaCompressible; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.UUID; + +/** + * A {@link Graph} implementation backed by {@link DeltaCompressible} deltas. Has no lock of its own + * — transaction lifecycle is managed exclusively by {@link + * org.rdfarchitect.database.inmemory.GraphWithContextTransactional}. All {@link Graph} methods + * enforce that the coordinator has an active transaction via the shared {@link TransactionContext}. + */ +public class RDFGraphDelta implements Graph, TransactionParticipant, Rewindable { + + private static final Logger logger = LoggerFactory.getLogger(RDFGraphDelta.class); + + private final TransactionContext txnContext; + + private final Deque pastDeltas; + private DeltaCompressible currentDelta; + private final Deque futureDeltas; + + private final int maxVersions; + private final int compressCount; + + public RDFGraphDelta( + @NotNull Graph base, + int maxVersions, + int compressCount, + TransactionContext txnContext) { + this.txnContext = txnContext; + this.maxVersions = maxVersions; + this.compressCount = compressCount; + pastDeltas = new ArrayDeque<>(); + pastDeltas.push(new DeltaCompressible(base)); + currentDelta = new DeltaCompressible(head()); + futureDeltas = new ArrayDeque<>(); + } + + // ------------------------------------------------------------------------- + // Graph interface — all check transaction state via txnContext + // ------------------------------------------------------------------------- + + @Override + public void add(Triple t) throws AddDeniedException { + checkWriteTransaction(); + currentDelta.add(t); + } + + @Override + public void delete(Triple t) throws DeleteDeniedException { + checkWriteTransaction(); + currentDelta.delete(t); + } + + @Override + public ExtendedIterator find(Triple m) { + checkTransaction(); + return currentDelta.find(m); + } + + @Override + public ExtendedIterator find(Node s, Node p, Node o) { + return find( + Triple.create( + s != null ? s : Node.ANY, + p != null ? p : Node.ANY, + o != null ? o : Node.ANY)); + } + + @Override + public boolean contains(Triple t) { + checkTransaction(); + return currentDelta.contains(t); + } + + @Override + public boolean contains(Node s, Node p, Node o) { + return contains(Triple.create(s, p, o)); + } + + @Override + public boolean isIsomorphicWith(Graph g) { + checkTransaction(); + return currentDelta.isIsomorphicWith(g); + } + + @Override + public void clear() { + checkWriteTransaction(); + currentDelta.clear(); + } + + @Override + public void remove(Node s, Node p, Node o) { + checkWriteTransaction(); + currentDelta.remove(s, p, o); + } + + @Override + public void close() { + checkWriteTransaction(); + if (!futureDeltas.isEmpty()) { + futureDeltas.peekFirst().close(); + } + currentDelta.close(); + } + + @Override + public boolean isClosed() { + checkTransaction(); + return currentDelta.isClosed(); + } + + @Override + public boolean isEmpty() { + checkTransaction(); + return currentDelta.isEmpty(); + } + + @Override + public int size() { + checkTransaction(); + return currentDelta.size(); + } + + @Override + public TransactionHandler getTransactionHandler() { + checkTransaction(); + return currentDelta.getTransactionHandler(); + } + + @Override + public GraphEventManager getEventManager() { + checkTransaction(); + return currentDelta.getEventManager(); + } + + @Override + public PrefixMapping getPrefixMapping() { + checkTransaction(); + return currentDelta.getPrefixMapping(); + } + + // ------------------------------------------------------------------------- + // TransactionParticipant + // ------------------------------------------------------------------------- + + @Override + public void commit() { + pastDeltas.push(currentDelta); + currentDelta = new DeltaCompressible(head()); + futureDeltas.clear(); + if (countVersions() > maxVersions) { + compressBase(); + } + logger.debug("Committed transaction."); + } + + @Override + public void abort() { + if (!hasChanges()) { + logger.debug("Aborting a transaction with no changes."); + return; + } + currentDelta = new DeltaCompressible(head()); + logger.debug("Aborted transaction."); + } + + @Override + public void undo() { + if (!canUndo()) { + throw new GraphVersionControlException("Cannot undo: already at the oldest version."); + } + futureDeltas.push(pastDeltas.pop()); + currentDelta = new DeltaCompressible(head()); + } + + @Override + public void redo() { + if (!canRedo()) { + throw new GraphVersionControlException("Cannot redo: already at the newest version."); + } + pastDeltas.push(futureDeltas.pop()); + currentDelta = new DeltaCompressible(head()); + } + + @Override + public boolean canUndo() { + return currentVersion() > 0; + } + + @Override + public boolean canRedo() { + return !futureDeltas.isEmpty(); + } + + @Override + public void restore(UUID versionId) { + if (!containsDelta(versionId)) { + throw new GraphVersionControlException( + "Cannot restore to version " + versionId + ": does not exist."); + } + while (!pastDeltas.isEmpty() && !pastDeltas.peek().getVersionId().equals(versionId)) { + pastDeltas.pop(); + } + currentDelta = new DeltaCompressible(head()); + } + + @Override + public DeltaCompressible getLastDelta() { + return pastDeltas.peek(); + } + + @Override + public boolean hasChanges() { + return !currentDelta.getAdditions().isEmpty() || !currentDelta.getDeletions().isEmpty(); + } + + // ------------------------------------------------------------------------- + // Internals + // ------------------------------------------------------------------------- + + private void checkTransaction() { + if (!txnContext.isInTransaction()) { + throw new GraphNotInATransactionException(); + } + } + + private void checkWriteTransaction() { + checkTransaction(); + if (txnContext.transactionMode() == ReadWrite.READ) { + throw new GraphNotInAWriteTransactionException(); + } + } + + private DeltaCompressible head() { + var head = pastDeltas.peek(); + if (head == null) { + throw new IllegalStateException("Delta stack is empty."); + } + return head; + } + + private int currentVersion() { + return pastDeltas.size() - 1; + } + + private int countVersions() { + return pastDeltas.size() + futureDeltas.size(); + } + + private boolean containsDelta(UUID versionId) { + return pastDeltas.stream().anyMatch(d -> d.getVersionId().equals(versionId)); + } + + private void compressBase() { + int deleteCount = Math.min(pastDeltas.size() - 1, compressCount); + for (int i = 0; i < deleteCount; i++) { + pastDeltas.removeLast(); + } + pastDeltas.getLast().compress(); + } +} diff --git a/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/TransactionContext.java b/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/TransactionContext.java new file mode 100644 index 00000000..583acff5 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/TransactionContext.java @@ -0,0 +1,41 @@ +/* + * 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.rdf.graph.wrapper; + +import org.apache.jena.query.ReadWrite; + +public class TransactionContext { + + private final ThreadLocal mode = new ThreadLocal<>(); + + public void begin(ReadWrite mode) { + this.mode.set(mode); + } + + public void end() { + mode.remove(); + } + + public boolean isInTransaction() { + return mode.get() != null; + } + + public ReadWrite transactionMode() { + return mode.get(); + } +} diff --git a/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/TransactionParticipant.java b/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/TransactionParticipant.java new file mode 100644 index 00000000..0af21540 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/rdf/graph/wrapper/TransactionParticipant.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.rdf.graph.wrapper; + +/** + * Implemented by every component that participates in a transaction managed by {@link + * org.rdfarchitect.database.inmemory.GraphWithContextTransactional}. + */ +public interface TransactionParticipant { + + void commit(); + + void abort(); + + boolean hasChanges(); +} diff --git a/backend/src/main/java/org/rdfarchitect/services/ChangeLogService.java b/backend/src/main/java/org/rdfarchitect/services/ChangeLogService.java index c40136cb..f99e7ec9 100644 --- a/backend/src/main/java/org/rdfarchitect/services/ChangeLogService.java +++ b/backend/src/main/java/org/rdfarchitect/services/ChangeLogService.java @@ -19,65 +19,26 @@ import lombok.RequiredArgsConstructor; +import org.apache.jena.query.ReadWrite; import org.rdfarchitect.api.dto.ChangeLogEntryDTO; import org.rdfarchitect.api.dto.ChangeLogEntryMapper; -import org.rdfarchitect.context.SessionContext; +import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; -import org.rdfarchitect.models.changelog.ChangeLogEntry; -import org.rdfarchitect.models.changelog.GraphChangeLog; import org.springframework.stereotype.Service; -import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; @Service @RequiredArgsConstructor public class ChangeLogService implements ChangeLogUseCase { private final ChangeLogEntryMapper mapper; - - private final Map>> changeLogs = - new ConcurrentHashMap<>(); - - private GraphChangeLog getChangeLog(GraphIdentifier graphIdentifier) { - return changeLogs - .getOrDefault(SessionContext.getSessionId(), Collections.emptyMap()) - .getOrDefault(graphIdentifier.datasetName(), Collections.emptyMap()) - .getOrDefault(graphIdentifier.graphUri(), new GraphChangeLog()); - } - - @Override - public void recordChange(GraphIdentifier graphIdentifier, ChangeLogEntry entry) { - var graphChangeLog = - changeLogs - .computeIfAbsent( - SessionContext.getSessionId(), _ -> new ConcurrentHashMap<>()) - .computeIfAbsent( - graphIdentifier.datasetName(), _ -> new ConcurrentHashMap<>()) - .computeIfAbsent(graphIdentifier.graphUri(), _ -> new GraphChangeLog()); - graphChangeLog.addEntry(entry); - } - - @Override - public void undoChange(GraphIdentifier graphIdentifier) { - getChangeLog(graphIdentifier).undoChange(); - } - - @Override - public void redoChange(GraphIdentifier graphIdentifier) { - getChangeLog(graphIdentifier).redoChange(); - } + private final DatabasePort databasePort; @Override public List listChanges(GraphIdentifier graphIdentifier) { - return mapper.toDTOList(getChangeLog(graphIdentifier).getEntries()); - } - - @Override - public void restoreVersion(GraphIdentifier graphIdentifier, UUID versionId) { - getChangeLog(graphIdentifier).restore(versionId); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + return mapper.toDTOList(ctx.getChangeLog().getUndoHistory()); + } } } diff --git a/backend/src/main/java/org/rdfarchitect/services/ChangeLogUseCase.java b/backend/src/main/java/org/rdfarchitect/services/ChangeLogUseCase.java index dc1dae3f..86df972b 100644 --- a/backend/src/main/java/org/rdfarchitect/services/ChangeLogUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/ChangeLogUseCase.java @@ -19,45 +19,13 @@ import org.rdfarchitect.api.dto.ChangeLogEntryDTO; import org.rdfarchitect.database.GraphIdentifier; -import org.rdfarchitect.models.changelog.ChangeLogEntry; import java.util.List; -import java.util.UUID; public interface ChangeLogUseCase { /** - * Records a change in the changelog for the specified graph and current session. - * - * @param graphIdentifier The identifier of the graph where the change occurred. - * @param entry The changelog entry to record. - */ - void recordChange(GraphIdentifier graphIdentifier, ChangeLogEntry entry); - - /** - * Records an undo operation in the changelog for the specified graph and current session. - * - * @param graphIdentifier The identifier of the graph where the undo occurred. - */ - void undoChange(GraphIdentifier graphIdentifier); - - /** - * Records a redo operation in the changelog for the specified graph and current session. - * - * @param graphIdentifier The identifier of the graph where the redo occurred. - */ - void redoChange(GraphIdentifier graphIdentifier); - - /** - * Restores a specific version of the graph for the graph provided through the graph identifier. - * - * @param graphIdentifier The identifier of the graph to restore. - * @param versionId The unique identifier of the version to restore. - */ - void restoreVersion(GraphIdentifier graphIdentifier, UUID versionId); - - /** - * Lists all changelog entries for the specified graph and the current session. + * Lists all changelog entries for the specified graph. * * @param graphIdentifier The identifier of the graph for which to list changes. * @return A list of changelog entries for the specified graph. diff --git a/backend/src/main/java/org/rdfarchitect/services/ClassExtensionService.java b/backend/src/main/java/org/rdfarchitect/services/ClassExtensionService.java index 779012c2..e66c1852 100644 --- a/backend/src/main/java/org/rdfarchitect/services/ClassExtensionService.java +++ b/backend/src/main/java/org/rdfarchitect/services/ClassExtensionService.java @@ -20,7 +20,7 @@ import lombok.AllArgsConstructor; import org.apache.jena.graph.Graph; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.vocabulary.RDF; @@ -29,7 +29,6 @@ import org.rdfarchitect.api.dto.ClassMapper; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; -import org.rdfarchitect.models.changelog.ChangeLogEntry; import org.rdfarchitect.models.cim.data.CIMObjectFetcher; import org.rdfarchitect.models.cim.data.dto.CIMClass; import org.rdfarchitect.models.cim.data.dto.relations.CIMSBelongsToCategory; @@ -40,7 +39,6 @@ import org.rdfarchitect.models.cim.rdf.resources.CIMStereotypes; import org.rdfarchitect.models.cim.rdf.resources.RDFA; import org.rdfarchitect.models.cim.relations.CIMClassRelationFinder; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindable; import org.springframework.stereotype.Service; import java.util.List; @@ -52,40 +50,29 @@ public class ClassExtensionService implements ClassExtensionUseCase { private DatabasePort databasePort; private ClassMapper classMapper; - private ChangeLogUseCase changeLogUseCase; @Override public ClassDTO extendClass( GraphIdentifier graphIdentifier, String classUUID, GraphIdentifier newGraphIdentifier) { - var graph = databasePort.getGraphWithContext(graphIdentifier); CIMClass classCopy; List superClasses; - GraphRewindable rdfGraph = null; - try { - rdfGraph = graph.getRdfGraph(); - rdfGraph.begin(TxnType.READ); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + var rdfGraph = ctx.getRdfGraph(); classCopy = fetchStubbedClassCopy(graphIdentifier, classUUID); superClasses = fetchStubbedSuperClasses(rdfGraph, UUID.fromString(classUUID)); - - } finally { - if (rdfGraph != null) { - rdfGraph.end(); - } } - - var newGraph = databasePort.getGraphWithContext(newGraphIdentifier).getRdfGraph(); - insertNewClasses(newGraph, newGraphIdentifier, classCopy, superClasses); - changeLogUseCase.recordChange( - newGraphIdentifier, - new ChangeLogEntry( - "Added " - + classCopy.getLabel().getValue() - + " and its superclasses to graph " - + newGraphIdentifier.graphUri(), - newGraph.getLastDelta())); - + try (var ctx = + databasePort.getGraphWithContext(newGraphIdentifier).begin(ReadWrite.WRITE)) { + var newGraph = databasePort.getGraphWithContext(newGraphIdentifier).getRdfGraph(); + insertNewClasses(newGraph, classCopy, superClasses); + ctx.commit( + "Added " + + classCopy.getLabel().getValue() + + " and its superclasses to graph " + + newGraphIdentifier.graphUri()); + } return classMapper.toDTO(classCopy); } @@ -134,33 +121,19 @@ private List fetchStubbedSuperClasses(Graph graph, UUID classUUID) { return superClasses.stream().toList(); } - private void insertNewClasses( - GraphRewindable newGraph, - GraphIdentifier newGraphIdentifier, - CIMClass classCopy, - List superClasses) { - try { - var model = ModelFactory.createModelForGraph(newGraph); - newGraph.begin(TxnType.WRITE); - var prefixMapping = databasePort.getPrefixMapping(newGraphIdentifier.datasetName()); - - var newPackage = fetchNewPackage(model); - - for (var cls : superClasses) { - if (!model.contains(model.createResource(cls.getUri().toString()), null)) { - cls.setBelongsToCategory(newPackage); - CIMUpdates.insertClass(newGraph, prefixMapping, cls); - } - } - classCopy.setBelongsToCategory(newPackage); - CIMUpdates.insertClass(newGraph, prefixMapping, classCopy); + private void insertNewClasses(Graph newGraph, CIMClass classCopy, List superClasses) { + var model = ModelFactory.createModelForGraph(newGraph); + + var newPackage = fetchNewPackage(model); - newGraph.commit(); - } finally { - if (newGraph != null) { - newGraph.end(); + for (var cls : superClasses) { + if (!model.contains(model.createResource(cls.getUri().toString()), null)) { + cls.setBelongsToCategory(newPackage); + CIMUpdates.insertClass(newGraph, newGraph.getPrefixMapping(), cls); } } + classCopy.setBelongsToCategory(newPackage); + CIMUpdates.insertClass(newGraph, newGraph.getPrefixMapping(), classCopy); } private CIMSBelongsToCategory fetchNewPackage(Model model) { diff --git a/backend/src/main/java/org/rdfarchitect/services/GetRenderingDataService.java b/backend/src/main/java/org/rdfarchitect/services/GetRenderingDataService.java new file mode 100644 index 00000000..42ee848d --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/services/GetRenderingDataService.java @@ -0,0 +1,70 @@ +/* + * 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; + +import lombok.RequiredArgsConstructor; + +import org.apache.jena.graph.Graph; +import org.apache.jena.query.ReadWrite; +import org.rdfarchitect.api.dto.dl.RenderingLayoutData; +import org.rdfarchitect.api.dto.rendering.RenderingDataDTO; +import org.rdfarchitect.database.DatabasePort; +import org.rdfarchitect.database.GraphIdentifier; +import org.rdfarchitect.dl.queries.select.DLObjectFetcher; +import org.rdfarchitect.models.cim.rendering.GraphFilter; +import org.rdfarchitect.models.cim.rendering.RenderCIMCollectionUseCase; +import org.rdfarchitect.rdf.graph.GraphUtils; +import org.rdfarchitect.services.rendering.GraphToCIMCollectionConverterUseCase; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class GetRenderingDataService implements GetRenderingDataUseCase { + + private final DatabasePort databasePort; + private final GraphToCIMCollectionConverterUseCase converter; + private final RenderCIMCollectionUseCase renderer; + + @Override + public RenderingDataDTO getRenderingData( + GraphIdentifier graphIdentifier, GraphFilter filter, UUID packageUUID) { + Graph rdfGraphCopy; + RenderingLayoutData layoutData; + + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + rdfGraphCopy = GraphUtils.deepCopy(ctx.getRdfGraph()); + + var diagramLayout = ctx.getDiagramLayout(); + var diagramLayoutModel = diagramLayout.getDiagramLayoutModel(); + var classLayoutingData = + packageUUID == null + ? DLObjectFetcher.fetchDiagramDOPPerClass( + diagramLayoutModel, + diagramLayout.getDefaultPackageMRID().getUuid()) + : DLObjectFetcher.fetchDiagramDOPPerClass( + diagramLayoutModel, packageUUID); + layoutData = + RenderingLayoutData.builder().classLayoutingData(classLayoutingData).build(); + } + + var cimCollection = converter.convert(rdfGraphCopy, graphIdentifier, filter); + return renderer.renderUML(cimCollection, layoutData); + } +} diff --git a/backend/src/main/java/org/rdfarchitect/services/GetRenderingDataUseCase.java b/backend/src/main/java/org/rdfarchitect/services/GetRenderingDataUseCase.java new file mode 100644 index 00000000..e522750d --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/services/GetRenderingDataUseCase.java @@ -0,0 +1,34 @@ +/* + * 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; + +import org.rdfarchitect.api.dto.rendering.RenderingDataDTO; +import org.rdfarchitect.database.GraphIdentifier; +import org.rdfarchitect.models.cim.rendering.GraphFilter; + +import java.util.UUID; + +/** + * Fetches and renders a graph as a UML diagram within a single READ transaction, ensuring a + * consistent snapshot of both the RDF graph and the diagram layout. + */ +public interface GetRenderingDataUseCase { + + RenderingDataDTO getRenderingData( + GraphIdentifier graphIdentifier, GraphFilter filter, UUID packageUUID); +} diff --git a/backend/src/main/java/org/rdfarchitect/services/SearchService.java b/backend/src/main/java/org/rdfarchitect/services/SearchService.java index 504439f2..abdafeed 100644 --- a/backend/src/main/java/org/rdfarchitect/services/SearchService.java +++ b/backend/src/main/java/org/rdfarchitect/services/SearchService.java @@ -272,16 +272,13 @@ public void searchGraph( appendPackageConstraint(filter, specificExternalQuery); var internalQueryObject = QueryFactory.create(internalQueryWithPackageConstraint); var externalQueryObject = QueryFactory.create(externalQueryWithPackageConstraint); + var ctx = databasePort.getGraphWithContext(graphIdentifier); var internalResultSet = InMemorySparqlExecutor.executeSingleQuery( - databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), - internalQueryObject, - graphIdentifier.graphUri()); + ctx, internalQueryObject, graphIdentifier.graphUri()); var externalResultSet = InMemorySparqlExecutor.executeSingleQuery( - databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), - externalQueryObject, - graphIdentifier.graphUri()); + ctx, externalQueryObject, graphIdentifier.graphUri()); searchResults.addAll( SearchResultObjectFactory.createSearchResultObjectList( diff --git a/backend/src/main/java/org/rdfarchitect/services/compare/SchemaComparisonService.java b/backend/src/main/java/org/rdfarchitect/services/compare/SchemaComparisonService.java index 4d333488..59de2879 100644 --- a/backend/src/main/java/org/rdfarchitect/services/compare/SchemaComparisonService.java +++ b/backend/src/main/java/org/rdfarchitect/services/compare/SchemaComparisonService.java @@ -19,7 +19,7 @@ import lombok.RequiredArgsConstructor; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.models.changes.triplechanges.TriplePackageChange; @@ -40,22 +40,15 @@ public class SchemaComparisonService implements SchemaComparisonUseCase { @Override public List compareSchemas( GraphIdentifier graphIdentifier, MultipartFile file) { - var updatedGraph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); var originalGraph = new GraphFileSourceBuilderImpl() .setFile(file) .setGraphName(GRAPH_URI) .build() .graph(); - List result; - - try { - updatedGraph.begin(TxnType.READ); - result = TripleChangeAnalyser.compareGraphs(originalGraph, updatedGraph); - } finally { - updatedGraph.end(); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + return TripleChangeAnalyser.compareGraphs(originalGraph, ctx.getRdfGraph()); } - return result; } @Override @@ -79,24 +72,19 @@ public List compareSchemas(MultipartFile file1, MultipartFi @Override public List compareSchemas( GraphIdentifier originalGraphIdentifier, GraphIdentifier updatedGraphIdentifier) { - var originalGraph = databasePort.getGraphWithContext(originalGraphIdentifier).getRdfGraph(); - var updatedGraph = databasePort.getGraphWithContext(updatedGraphIdentifier).getRdfGraph(); - List result; - if (originalGraphIdentifier.equals(updatedGraphIdentifier)) { return new ArrayList<>(); } - try { - originalGraph.begin(TxnType.READ); - try { - updatedGraph.begin(TxnType.READ); - result = TripleChangeAnalyser.compareGraphs(originalGraph, updatedGraph); - } finally { - updatedGraph.end(); - } - } finally { - originalGraph.end(); + try (var originalCtx = + databasePort + .getGraphWithContext(originalGraphIdentifier) + .begin(ReadWrite.READ); + var updatedCtx = + databasePort + .getGraphWithContext(updatedGraphIdentifier) + .begin(ReadWrite.READ)) { + return TripleChangeAnalyser.compareGraphs( + originalCtx.getRdfGraph(), updatedCtx.getRdfGraph()); } - return result; } } 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 1333189c..031d0320 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/DeleteResourcesService.java @@ -19,7 +19,7 @@ import lombok.RequiredArgsConstructor; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.Resource; @@ -35,7 +35,6 @@ 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; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -65,16 +64,15 @@ private record ResolvedDeleteRequest( @Override public void executeDeleteRequests( 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(); - } + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var deleteCount = + deleteRequests.stream() + .filter(r -> r.getAction() == DeleteAction.DELETE) + .count(); + + deleteResources(ModelFactory.createModelForGraph(ctx.getRdfGraph()), deleteRequests); + ctx.commit( + "Deleted " + deleteCount + " " + (deleteCount == 1 ? "resource" : "resources")); } } 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 8243b55b..9e4efbcf 100644 --- a/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java +++ b/backend/src/main/java/org/rdfarchitect/services/delete/FindDeleteDependenciesService.java @@ -20,7 +20,7 @@ import lombok.RequiredArgsConstructor; import org.apache.jena.graph.Graph; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.Resource; @@ -41,7 +41,6 @@ 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; import java.util.ArrayList; @@ -295,15 +294,8 @@ private ResourceIdentifier createResourceIdentifier(Resource resource) { } 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(); - } + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + return GraphUtils.deepCopy(ctx.getRdfGraph()); } } } diff --git a/backend/src/main/java/org/rdfarchitect/services/diagrams/CustomDiagramService.java b/backend/src/main/java/org/rdfarchitect/services/diagrams/CustomDiagramService.java index 2fd1d70d..075c6836 100644 --- a/backend/src/main/java/org/rdfarchitect/services/diagrams/CustomDiagramService.java +++ b/backend/src/main/java/org/rdfarchitect/services/diagrams/CustomDiagramService.java @@ -19,9 +19,9 @@ import lombok.RequiredArgsConstructor; +import org.apache.jena.query.ReadWrite; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; -import org.rdfarchitect.database.inmemory.diagrams.ClassInDiagram; import org.rdfarchitect.database.inmemory.diagrams.CustomDiagram; import org.springframework.stereotype.Service; @@ -35,19 +35,15 @@ public class CustomDiagramService implements GetCustomDiagramsUseCase, ReplaceCustomDiagramUseCase, DeleteCustomDiagramUseCase, - AddToDiagramUseCase, RemoveFromDiagramUseCase { private final DatabasePort databasePort; @Override public List getCustomDiagramsForGraph(GraphIdentifier graphIdentifier) { - return databasePort - .getGraphWithContext(graphIdentifier) - .getCustomDiagrams() - .values() - .stream() - .toList(); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + return ctx.getCustomDiagrams().values().stream().toList(); + } } @Override @@ -75,29 +71,23 @@ public void replaceCustomDiagram(String datasetName, String diagramId, CustomDia diagrams.put(UUID.fromString(diagramId), diagram); } - @Override - public void addToDiagram(String datasetName, String diagramId, List classes) { - var diagrams = databasePort.getDatasetDiagrams(datasetName); - var diagram = diagrams.get(UUID.fromString(diagramId)); - if (diagram != null) { - diagram.getClasses().addAll(classes); - } - } - @Override public void removeFromDiagram(String datasetName, String diagramId, UUID classId) { var diagrams = databasePort.getDatasetDiagrams(datasetName); - var diagram = diagrams.get(UUID.fromString(diagramId)); if (diagram != null) { - diagram.getClasses().removeIf(c -> c.getUuid().equals(classId)); + var classes = diagram.getClasses(); + classes.removeIf(c -> c.getUuid().equals(classId)); + diagram.setClasses(classes); } } @Override public void deleteCustomDiagram(GraphIdentifier graphIdentifier, String diagramId) { - var graphWithContext = databasePort.getGraphWithContext(graphIdentifier); - graphWithContext.getCustomDiagrams().remove(UUID.fromString(diagramId)); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + ctx.getCustomDiagrams().remove(UUID.fromString(diagramId)); + ctx.commit("deleted diagram %s".formatted(diagramId)); + } } @Override @@ -111,38 +101,40 @@ public void replaceCustomDiagram( + diagram.getDiagramId() + "'"); } - var graphWithContext = databasePort.getGraphWithContext(graphIdentifier); - graphWithContext.getCustomDiagrams().put(UUID.fromString(diagramId), diagram); - } - - @Override - public void addToDiagram( - GraphIdentifier graphIdentifier, String diagramId, List classes) { - var graphWithContext = databasePort.getGraphWithContext(graphIdentifier); - var diagram = graphWithContext.getCustomDiagrams().get(UUID.fromString(diagramId)); - if (diagram != null) { - diagram.getClasses().addAll(classes); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + ctx.getCustomDiagrams().put(UUID.fromString(diagramId), diagram); + ctx.commit("replaced diagram %s".formatted(diagramId)); } } @Override public void removeFromDiagram(GraphIdentifier graphIdentifier, String diagramId, UUID classId) { - var graphWithContext = databasePort.getGraphWithContext(graphIdentifier); - var diagram = graphWithContext.getCustomDiagrams().get(UUID.fromString(diagramId)); - if (diagram != null) { - diagram.getClasses().removeIf(c -> c.getUuid().equals(classId)); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var diagram = ctx.getCustomDiagrams().get(UUID.fromString(diagramId)); + if (diagram != null) { + var classes = diagram.getClasses(); + classes.removeIf(c -> c.getUuid().equals(classId)); + diagram.setClasses(classes); + } + ctx.commit("removed class %s from diagram %s".formatted(classId, diagramId)); } } @Override public void removeFromAllDiagrams(GraphIdentifier graphIdentifier, UUID classId) { - var graphWithContext = databasePort.getGraphWithContext(graphIdentifier); - for (var diagram : graphWithContext.getCustomDiagrams().values()) { - diagram.getClasses().removeIf(c -> c.getUuid().equals(classId)); - } - var datasetDiagrams = databasePort.getDatasetDiagrams(graphIdentifier.datasetName()); - for (var diagram : datasetDiagrams.values()) { - diagram.getClasses().removeIf(c -> c.getUuid().equals(classId)); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + for (var diagram : ctx.getCustomDiagrams().values()) { + var classes = diagram.getClasses(); + classes.removeIf(c -> c.getUuid().equals(classId)); + diagram.setClasses(classes); + } + for (var diagram : + databasePort.getDatasetDiagrams(graphIdentifier.datasetName()).values()) { + var classes = diagram.getClasses(); + classes.removeIf(c -> c.getUuid().equals(classId)); + diagram.setClasses(classes); + } + ctx.commit("removed class %s from all diagrams".formatted(classId)); } } } diff --git a/backend/src/main/java/org/rdfarchitect/services/dl/select/QueryDiagramLayoutService.java b/backend/src/main/java/org/rdfarchitect/services/dl/select/QueryDiagramLayoutService.java index 2875a428..edbe383e 100644 --- a/backend/src/main/java/org/rdfarchitect/services/dl/select/QueryDiagramLayoutService.java +++ b/backend/src/main/java/org/rdfarchitect/services/dl/select/QueryDiagramLayoutService.java @@ -19,13 +19,14 @@ import lombok.RequiredArgsConstructor; +import org.apache.jena.query.ReadWrite; import org.apache.jena.rdf.model.Model; import org.rdfarchitect.api.dto.dl.RenderingLayoutData; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.dl.data.dto.DiagramObjectPoint; import org.rdfarchitect.dl.queries.select.DLObjectFetcher; -import org.rdfarchitect.rdf.graph.wrapper.DiagramLayout; +import org.rdfarchitect.rdf.graph.wrapper.DiagramLayoutDelta; import org.springframework.stereotype.Service; import java.util.Map; @@ -40,26 +41,31 @@ public class QueryDiagramLayoutService implements FetchRenderingLayoutDataUseCas @Override public RenderingLayoutData fetchRenderingLayoutData( GraphIdentifier graphIdentifier, UUID packageUUID) { - var diagramLayout = databasePort.getGraphWithContext(graphIdentifier).getDiagramLayout(); - var diagramLayoutModel = diagramLayout.getDiagramLayoutModel(); - return fetchRenderingLayoutData(diagramLayout, diagramLayoutModel, packageUUID); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + DiagramLayoutDelta diagramLayout = ctx.getDiagramLayout(); + var diagramLayoutModel = diagramLayout.getDiagramLayoutModel(); + return fetchRenderingLayoutData( + diagramLayout.getDefaultPackageMRID().getUuid(), + diagramLayoutModel, + packageUUID); + } } @Override public RenderingLayoutData fetchGlobalRenderingLayoutData(String datasetName, UUID diagramId) { var diagramLayout = databasePort.getDatasetDiagramLayout(datasetName); var diagramLayoutModel = diagramLayout.getDiagramLayoutModel(); - return fetchRenderingLayoutData(diagramLayout, diagramLayoutModel, diagramId); + return fetchRenderingLayoutData( + diagramLayout.getDefaultPackageMRID().getUuid(), diagramLayoutModel, diagramId); } private RenderingLayoutData fetchRenderingLayoutData( - DiagramLayout diagramLayout, Model diagramLayoutModel, UUID diagramId) { + UUID defaultPackageUUID, Model diagramLayoutModel, UUID diagramId) { Map classLayoutingData; if (diagramId == null) { classLayoutingData = - DLObjectFetcher.fetchDiagramDOPPerClass( - diagramLayoutModel, diagramLayout.getDefaultPackageMRID().getUuid()); + DLObjectFetcher.fetchDiagramDOPPerClass(diagramLayoutModel, defaultPackageUUID); } else { classLayoutingData = DLObjectFetcher.fetchDiagramDOPPerClass(diagramLayoutModel, diagramId); diff --git a/backend/src/main/java/org/rdfarchitect/services/dl/update/EnsureDiagramLayoutForCIMCollectionUseCase.java b/backend/src/main/java/org/rdfarchitect/services/dl/update/EnsureDiagramLayoutForCIMCollectionUseCase.java deleted file mode 100644 index 9eb3cf2d..00000000 --- a/backend/src/main/java/org/rdfarchitect/services/dl/update/EnsureDiagramLayoutForCIMCollectionUseCase.java +++ /dev/null @@ -1,39 +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.services.dl.update; - -import org.rdfarchitect.database.GraphIdentifier; -import org.rdfarchitect.models.cim.data.dto.CIMCollection; - -import java.util.UUID; - -public interface EnsureDiagramLayoutForCIMCollectionUseCase { - - /** - * Ensures that the necessary diagram layout data exists for the {@link CIMCollection} provided - * - * @param graphIdentifier the identifier of the graph - * @param diagramUUID the UUID of the package or diagram to be rendered - * @param cimCollection the CIMCollection containing all packages, classes and enums - */ - void ensureDiagramLayoutExists( - GraphIdentifier graphIdentifier, UUID diagramUUID, CIMCollection cimCollection); - - void ensureDiagramLayoutExists( - String datasetName, UUID diagramUUID, CIMCollection cimCollection); -} diff --git a/backend/src/main/java/org/rdfarchitect/services/dl/update/UpdateDiagramLayoutService.java b/backend/src/main/java/org/rdfarchitect/services/dl/update/UpdateDiagramLayoutService.java index f3ad9543..645f11ca 100644 --- a/backend/src/main/java/org/rdfarchitect/services/dl/update/UpdateDiagramLayoutService.java +++ b/backend/src/main/java/org/rdfarchitect/services/dl/update/UpdateDiagramLayoutService.java @@ -19,29 +19,26 @@ import lombok.RequiredArgsConstructor; -import org.apache.jena.rdf.model.Model; +import org.apache.jena.query.ReadWrite; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.dl.data.dto.Diagram; -import org.rdfarchitect.dl.data.dto.DiagramObject; -import org.rdfarchitect.dl.data.dto.relations.MRID; import org.rdfarchitect.dl.data.dto.relations.OrientationKind; -import org.rdfarchitect.dl.queries.select.DLObjectFetcher; import org.rdfarchitect.dl.queries.update.DLUpdates; import org.rdfarchitect.models.cim.data.dto.CIMCollection; +import org.rdfarchitect.models.cim.data.dto.CIMPackage; import org.rdfarchitect.models.cim.rendering.GraphFilter; -import org.rdfarchitect.rdf.graph.wrapper.DiagramLayout; import org.rdfarchitect.services.dl.update.packagelayout.CreateDiagramLayoutUseCase; import org.rdfarchitect.services.rendering.GraphToCIMCollectionConverterUseCase; import org.springframework.stereotype.Service; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.UUID; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor -public class UpdateDiagramLayoutService - implements CreateDiagramLayoutUseCase, EnsureDiagramLayoutForCIMCollectionUseCase { +public class UpdateDiagramLayoutService implements CreateDiagramLayoutUseCase { private static final String DEFAULT_PACKAGE_NAME = "default"; @@ -50,170 +47,67 @@ public class UpdateDiagramLayoutService @Override public void createDiagramLayout(GraphIdentifier graphIdentifier) { - var diagramLayout = databasePort.getGraphWithContext(graphIdentifier).getDiagramLayout(); - var diagramLayoutModel = diagramLayout.getDiagramLayoutModel(); - var allPackagesGraphFilter = new GraphFilter(false); - CIMCollection allPackagesCIMCollection = - converter.convert(graphIdentifier, allPackagesGraphFilter); + var allPackagesCIMCollection = converter.convert(graphIdentifier, allPackagesGraphFilter); - // set filter to also include classes outside the package var packageGraphFilter = new GraphFilter(false); packageGraphFilter.setIncludeInheritance(true); packageGraphFilter.setIncludeAssociations(true); packageGraphFilter.setIncludeRelationsToExternalPackages(true); - - // create DL diagram for the default package - DLUpdates.insertDiagram( - diagramLayoutModel, - Diagram.builder() - .mRID(diagramLayout.getDefaultPackageMRID()) - .name(graphIdentifier.graphUri() + "/" + DEFAULT_PACKAGE_NAME) - .orientation(OrientationKind.NEGATIVE) - .build()); - // create DOs and DOPs for the default package packageGraphFilter.setPackageUUID(null); var defaultPackageClassesCIMCollection = converter.convert(graphIdentifier, packageGraphFilter); - for (var cimClassOrEnum : defaultPackageClassesCIMCollection.getClassesAndEnums()) { - var diagramObjectMRID = - DiagramLayoutServiceUtils.insertDiagramObject( - diagramLayoutModel, - diagramLayout.getDefaultPackageMRID().getUuid(), - cimClassOrEnum.getLabel().getValue(), - cimClassOrEnum.getUuid()); - DiagramLayoutServiceUtils.insertDiagramObjectPoint( - diagramLayoutModel, - packageGraphFilter.getPackageUUID() == null - ? diagramLayout.getDefaultPackageMRID().getUuid() - : UUID.fromString(packageGraphFilter.getPackageUUID()), - diagramObjectMRID); - } - // create DOs and DOPs for each frontend diagram + Map classesByPackage = new LinkedHashMap<>(); for (var cimPackage : allPackagesCIMCollection.getPackages()) { - DiagramLayoutServiceUtils.insertDiagram( - diagramLayoutModel, cimPackage.getUuid(), cimPackage.getLabel().getValue()); - packageGraphFilter.setPackageUUID(cimPackage.getUuid().toString()); - var classesCIMCollection = converter.convert(graphIdentifier, packageGraphFilter); - - for (var cimClassOrEnum : classesCIMCollection.getClassesAndEnums()) { - var diagramObjectMRID = - DiagramLayoutServiceUtils.insertDiagramObject( - diagramLayoutModel, - cimPackage.getUuid(), - cimClassOrEnum.getLabel().getValue(), - cimClassOrEnum.getUuid()); - DiagramLayoutServiceUtils.insertDiagramObjectPoint( - diagramLayoutModel, cimPackage.getUuid(), diagramObjectMRID); - } - } - } - - @Override - public void ensureDiagramLayoutExists( - GraphIdentifier graphIdentifier, UUID diagramUUID, CIMCollection cimCollection) { - var diagramLayout = databasePort.getGraphWithContext(graphIdentifier).getDiagramLayout(); - var diagramLayoutModel = diagramLayout.getDiagramLayoutModel(); - - var resolvedUUID = - diagramUUID != null ? diagramUUID : diagramLayout.getDefaultPackageMRID().getUuid(); - - // ensure a DL diagram exists for the requested package or diagram - var diagram = DLObjectFetcher.fetchDiagram(diagramLayoutModel, resolvedUUID); - if (diagram == null) { - String packageName = - getDiagramName(diagramLayout, graphIdentifier, cimCollection, resolvedUUID); - - if (packageName == null) { - throw new IllegalArgumentException( - "Diagram with UUID " + resolvedUUID + " not found"); - } - - DiagramLayoutServiceUtils.insertDiagram(diagramLayoutModel, resolvedUUID, packageName); + classesByPackage.put( + cimPackage, converter.convert(graphIdentifier, packageGraphFilter)); } - ensureDiagramObjectsExist(resolvedUUID, cimCollection, diagramLayoutModel); - } - - @Override - public void ensureDiagramLayoutExists( - String datasetName, UUID diagramUUID, CIMCollection cimCollection) { - var diagramLayout = databasePort.getDatasetDiagramLayout(datasetName); - var diagramLayoutModel = diagramLayout.getDiagramLayoutModel(); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var diagramLayout = ctx.getDiagramLayout(); + var diagramLayoutModel = diagramLayout.getDiagramLayoutModel(); - // ensure a DL diagram exists for the requested package or diagram - var diagram = DLObjectFetcher.fetchDiagram(diagramLayoutModel, diagramUUID); - if (diagram == null) { - String diagramName = getDiagramName(datasetName, diagramUUID); - - if (diagramName == null) { - throw new IllegalArgumentException( - "Custom diagram with UUID " + diagramUUID + " not found"); - } - - DiagramLayoutServiceUtils.insertDiagram(diagramLayoutModel, diagramUUID, diagramName); - } - - ensureDiagramObjectsExist(diagramUUID, cimCollection, diagramLayoutModel); - } - - private void ensureDiagramObjectsExist( - UUID diagramUUID, CIMCollection cimCollection, Model diagramLayoutModel) { - var cimClasses = cimCollection.getClassesAndEnums(); - var diagramObjects = - DLObjectFetcher.fetchDiagramDOs(diagramLayoutModel, new MRID(diagramUUID)); - var existingMRIDs = - diagramObjects.stream() - .map(DiagramObject::getBelongsToIdentifiedObject) - .collect(Collectors.toSet()); - for (var cimClass : cimClasses) { - if (!existingMRIDs.contains(new MRID(cimClass.getUuid()))) { - var diagramObjectMRID = + DLUpdates.insertDiagram( + diagramLayoutModel, + Diagram.builder() + .mRID(diagramLayout.getDefaultPackageMRID()) + .name(graphIdentifier.graphUri() + "/" + DEFAULT_PACKAGE_NAME) + .orientation(OrientationKind.NEGATIVE) + .build()); + + for (var cimClassOrEnum : defaultPackageClassesCIMCollection.getClassesAndEnums()) { + var doMRID = DiagramLayoutServiceUtils.insertDiagramObject( diagramLayoutModel, - diagramUUID, - cimClass.getLabel().getValue(), - cimClass.getUuid()); + diagramLayout.getDefaultPackageMRID().getUuid(), + cimClassOrEnum.getLabel().getValue(), + cimClassOrEnum.getUuid()); DiagramLayoutServiceUtils.insertDiagramObjectPoint( - diagramLayoutModel, diagramUUID, diagramObjectMRID); + diagramLayoutModel, + packageGraphFilter.getPackageUUID() == null + ? diagramLayout.getDefaultPackageMRID().getUuid() + : UUID.fromString(packageGraphFilter.getPackageUUID()), + doMRID); } - } - } - private String getDiagramName( - DiagramLayout diagramLayout, - GraphIdentifier graphIdentifier, - CIMCollection cimCollection, - UUID diagramUUID) { - if (diagramUUID == diagramLayout.getDefaultPackageMRID().getUuid()) { - return graphIdentifier.graphUri() + "/" + DEFAULT_PACKAGE_NAME; - } - - for (var cimPackage : cimCollection.getPackages()) { - if (cimPackage.getUuid().equals(diagramUUID)) { - return cimPackage.getLabel().getValue(); - } - } - - for (var customDiagram : - databasePort.getGraphWithContext(graphIdentifier).getCustomDiagrams().values()) { - if (customDiagram.getDiagramId().equals(diagramUUID)) { - return customDiagram.getName(); + for (var entry : classesByPackage.entrySet()) { + var cimPackage = entry.getKey(); + DiagramLayoutServiceUtils.insertDiagram( + diagramLayoutModel, cimPackage.getUuid(), cimPackage.getLabel().getValue()); + for (var cimClassOrEnum : entry.getValue().getClassesAndEnums()) { + var doMRID = + DiagramLayoutServiceUtils.insertDiagramObject( + diagramLayoutModel, + cimPackage.getUuid(), + cimClassOrEnum.getLabel().getValue(), + cimClassOrEnum.getUuid()); + DiagramLayoutServiceUtils.insertDiagramObjectPoint( + diagramLayoutModel, cimPackage.getUuid(), doMRID); + } } + ctx.commit(); } - - return null; - } - - private String getDiagramName(String datasetName, UUID diagramUUID) { - for (var customDiagram : databasePort.getDatasetDiagrams(datasetName).values()) { - if (customDiagram.getDiagramId().equals(diagramUUID)) { - return customDiagram.getName(); - } - } - - return null; } } diff --git a/backend/src/main/java/org/rdfarchitect/services/diagrams/AddToDiagramUseCase.java b/backend/src/main/java/org/rdfarchitect/services/dl/update/classlayout/CustomDiagramLayoutUseCase.java similarity index 50% rename from backend/src/main/java/org/rdfarchitect/services/diagrams/AddToDiagramUseCase.java rename to backend/src/main/java/org/rdfarchitect/services/dl/update/classlayout/CustomDiagramLayoutUseCase.java index 6df49faa..3484eb97 100644 --- a/backend/src/main/java/org/rdfarchitect/services/diagrams/AddToDiagramUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/dl/update/classlayout/CustomDiagramLayoutUseCase.java @@ -15,31 +15,24 @@ * */ -package org.rdfarchitect.services.diagrams; +package org.rdfarchitect.services.dl.update.classlayout; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.database.inmemory.diagrams.ClassInDiagram; import java.util.List; +import java.util.UUID; -public interface AddToDiagramUseCase { - - /** - * Adds a list of classes to a specified custom diagram. - * - * @param graphIdentifier The graph of the custom diagram - * @param diagramId The id of the custom diagram - * @param classes The classes to add to the custom diagram - */ - void addToDiagram( - GraphIdentifier graphIdentifier, String diagramId, List classes); - - /** - * Adds a list of classes to a specified custom diagram. - * - * @param datasetName The name of the dataset containing the diagram - * @param diagramId The ID of the custom diagram - * @param classes The classes to add to the diagram - */ - void addToDiagram(String datasetName, String diagramId, List classes); +public interface CustomDiagramLayoutUseCase { + + void addClassesToCustomDiagram( + GraphIdentifier graphIdentifier, UUID diagramUUID, List classes); + + void removeClassFromCustomDiagram( + GraphIdentifier graphIdentifier, UUID diagramUUID, UUID classUUID); + + void addClassesToCustomDatasetDiagram( + String datasetName, UUID diagramUUID, List classes); + + void removeClassFromCustomDatasetDiagram(String datasetName, UUID diagramUUID, UUID classUUID); } diff --git a/backend/src/main/java/org/rdfarchitect/services/dl/update/classlayout/UpdateClassLayoutService.java b/backend/src/main/java/org/rdfarchitect/services/dl/update/classlayout/UpdateClassLayoutService.java index 1b2c14c6..bf5eaa6f 100644 --- a/backend/src/main/java/org/rdfarchitect/services/dl/update/classlayout/UpdateClassLayoutService.java +++ b/backend/src/main/java/org/rdfarchitect/services/dl/update/classlayout/UpdateClassLayoutService.java @@ -19,14 +19,14 @@ import lombok.RequiredArgsConstructor; -import org.apache.jena.rdf.model.Model; +import org.apache.jena.query.ReadWrite; import org.rdfarchitect.api.dto.dl.ClassLayoutPositionDTO; import org.rdfarchitect.api.dto.dl.ClassPositionDTO; import org.rdfarchitect.api.dto.packages.PackageDTO; import org.rdfarchitect.api.dto.packages.PackageMapper; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; -import org.rdfarchitect.dl.data.dto.relations.MRID; +import org.rdfarchitect.database.inmemory.diagrams.ClassInDiagram; import org.rdfarchitect.dl.data.dto.relations.XYZPosition; import org.rdfarchitect.dl.queries.select.DLObjectFetcher; import org.rdfarchitect.dl.queries.update.DLUpdates; @@ -42,7 +42,8 @@ public class UpdateClassLayoutService implements UpdateClassPositionsUseCase, CreateClassLayoutDataUseCase, DeleteClassLayoutDataUseCase, - UpdateDiagramObjectNameUseCase { + UpdateDiagramObjectNameUseCase, + CustomDiagramLayoutUseCase { private final DatabasePort databasePort; private final PackageMapper packageMapper; @@ -54,24 +55,23 @@ public void createClassLayoutData( String className, UUID classUUID, ClassLayoutPositionDTO classLayoutPosition) { - var diagramLayout = databasePort.getGraphWithContext(graphIdentifier).getDiagramLayout(); - var diagramLayoutModel = diagramLayout.getDiagramLayoutModel(); - UUID packageUUID; + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var diagramLayout = ctx.getDiagramLayout(); + var diagramLayoutModel = diagramLayout.getDiagramLayoutModel(); + UUID packageUUID = + packageDTO != null + ? packageMapper.toCIMObject(packageDTO).getUuid() + : diagramLayout.getDefaultPackageMRID().getUuid(); - if (packageDTO != null) { - var cimPackage = packageMapper.toCIMObject(packageDTO); - packageUUID = cimPackage.getUuid(); - } else { - packageUUID = diagramLayout.getDefaultPackageMRID().getUuid(); + var doMRID = + DiagramLayoutServiceUtils.insertDiagramObject( + diagramLayoutModel, packageUUID, className, classUUID); + float xPosition = classLayoutPosition != null ? classLayoutPosition.getXPosition() : 0; + float yPosition = classLayoutPosition != null ? classLayoutPosition.getYPosition() : 0; + DiagramLayoutServiceUtils.insertDiagramObjectPoint( + diagramLayoutModel, doMRID, packageUUID, xPosition, yPosition); + ctx.commit(); } - - MRID doMRID = - DiagramLayoutServiceUtils.insertDiagramObject( - diagramLayoutModel, packageUUID, className, classUUID); - float xPosition = classLayoutPosition != null ? classLayoutPosition.getXPosition() : 0; - float yPosition = classLayoutPosition != null ? classLayoutPosition.getYPosition() : 0; - DiagramLayoutServiceUtils.insertDiagramObjectPoint( - diagramLayoutModel, doMRID, packageUUID, xPosition, yPosition); } @Override @@ -79,12 +79,54 @@ public void updateClassPositions( GraphIdentifier graphIdentifier, UUID packageUUID, List classPositionDTOList) { - var diagramLayout = databasePort.getGraphWithContext(graphIdentifier).getDiagramLayout(); - var diagramLayoutModel = diagramLayout.getDiagramLayoutModel(); - var resolvedPackageUUID = - packageUUID != null ? packageUUID : diagramLayout.getDefaultPackageMRID().getUuid(); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var diagramLayout = ctx.getDiagramLayout(); + var diagramLayoutModel = diagramLayout.getDiagramLayoutModel(); + var resolvedPackageUUID = + packageUUID != null + ? packageUUID + : diagramLayout.getDefaultPackageMRID().getUuid(); + + for (var classPositionDTO : classPositionDTOList) { + var diagramObject = + DLObjectFetcher.fetchDiagramDOForClass( + diagramLayoutModel, + resolvedPackageUUID, + classPositionDTO.getClassUUID()); + if (diagramObject == null) { + if (DLObjectFetcher.fetchDiagram(diagramLayoutModel, resolvedPackageUUID) + == null) { + DiagramLayoutServiceUtils.insertDiagram( + diagramLayoutModel, resolvedPackageUUID, ""); + } + var doMRID = + DiagramLayoutServiceUtils.insertDiagramObject( + diagramLayoutModel, + resolvedPackageUUID, + "", + classPositionDTO.getClassUUID()); + DiagramLayoutServiceUtils.insertDiagramObjectPoint( + diagramLayoutModel, + doMRID, + resolvedPackageUUID, + classPositionDTO.getXPosition(), + classPositionDTO.getYPosition()); + continue; + } + var diagramObjectPoint = + DLObjectFetcher.fetchDOPForDO(diagramLayoutModel, diagramObject.getMRID()); + DLUpdates.deleteDiagramObjectPoint( + diagramLayoutModel, diagramObjectPoint.getMRID()); + diagramObjectPoint.setPosition( + new XYZPosition( + classPositionDTO.getXPosition(), + classPositionDTO.getYPosition(), + classPositionDTO.getZPosition())); + DLUpdates.insertDiagramObjectPoint(diagramLayoutModel, diagramObjectPoint); + } - updateDiagramObjects(resolvedPackageUUID, classPositionDTOList, diagramLayoutModel); + ctx.commit(); + } } @Override @@ -93,32 +135,36 @@ public void updateClassPositions( var diagramLayout = databasePort.getDatasetDiagramLayout(datasetName); var diagramLayoutModel = diagramLayout.getDiagramLayoutModel(); - updateDiagramObjects(diagramUUID, classPositionDTOList, diagramLayoutModel); - } - - private void updateDiagramObjects( - UUID diagramUUID, - List classPositionDTOList, - Model diagramLayoutModel) { for (var classPositionDTO : classPositionDTOList) { var diagramObject = DLObjectFetcher.fetchDiagramDOForClass( diagramLayoutModel, diagramUUID, classPositionDTO.getClassUUID()); - - var doMRID = diagramObject.getMRID(); - - var diagramObjectPoint = DLObjectFetcher.fetchDOPForDO(diagramLayoutModel, doMRID); - - var dopMRID = diagramObjectPoint.getMRID(); - - DLUpdates.deleteDiagramObjectPoint(diagramLayoutModel, dopMRID); - + if (diagramObject == null) { + if (DLObjectFetcher.fetchDiagram(diagramLayoutModel, diagramUUID) == null) { + DiagramLayoutServiceUtils.insertDiagram(diagramLayoutModel, diagramUUID, ""); + } + var doMRID = + DiagramLayoutServiceUtils.insertDiagramObject( + diagramLayoutModel, + diagramUUID, + "", + classPositionDTO.getClassUUID()); + DiagramLayoutServiceUtils.insertDiagramObjectPoint( + diagramLayoutModel, + doMRID, + diagramUUID, + classPositionDTO.getXPosition(), + classPositionDTO.getYPosition()); + continue; + } + var diagramObjectPoint = + DLObjectFetcher.fetchDOPForDO(diagramLayoutModel, diagramObject.getMRID()); + DLUpdates.deleteDiagramObjectPoint(diagramLayoutModel, diagramObjectPoint.getMRID()); diagramObjectPoint.setPosition( new XYZPosition( classPositionDTO.getXPosition(), classPositionDTO.getYPosition(), classPositionDTO.getZPosition())); - DLUpdates.insertDiagramObjectPoint(diagramLayoutModel, diagramObjectPoint); } } @@ -126,28 +172,116 @@ private void updateDiagramObjects( @Override public void updateDiagramObjectName( GraphIdentifier graphIdentifier, UUID classUUID, String name) { - var diagramLayoutModel = - databasePort - .getGraphWithContext(graphIdentifier) - .getDiagramLayout() - .getDiagramLayoutModel(); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var diagramLayoutModel = ctx.getDiagramLayout().getDiagramLayoutModel(); + for (var diagramObject : DLObjectFetcher.fetchAllDOs(diagramLayoutModel, classUUID)) { + DLUpdates.updateDiagramObjectName(diagramLayoutModel, diagramObject, name); + } + ctx.commit(); + } + } - var diagramObjects = DLObjectFetcher.fetchAllDOs(diagramLayoutModel, classUUID); + @Override + public void deleteClassLayoutData(GraphIdentifier graphIdentifier, UUID classUUID) { + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var diagramLayoutModel = ctx.getDiagramLayout().getDiagramLayoutModel(); + for (var diagramObject : DLObjectFetcher.fetchAllDOs(diagramLayoutModel, classUUID)) { + DLUpdates.deleteDiagramObjectCascade(diagramLayoutModel, diagramObject.getMRID()); + } + ctx.commit(); + } + } - for (var diagramObject : diagramObjects) { - DLUpdates.updateDiagramObjectName(diagramLayoutModel, diagramObject, name); + @Override + public void addClassesToCustomDiagram( + GraphIdentifier graphIdentifier, UUID diagramUUID, List classes) { + if (classes.isEmpty()) { + return; + } + + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var diagram = ctx.getCustomDiagrams().get(diagramUUID); + if (diagram != null) { + var updated = diagram.getClasses(); + updated.addAll(classes); + diagram.setClasses(updated); + } + var diagramLayoutModel = ctx.getDiagramLayout().getDiagramLayoutModel(); + if (DLObjectFetcher.fetchDiagram(diagramLayoutModel, diagramUUID) == null) { + DiagramLayoutServiceUtils.insertDiagram(diagramLayoutModel, diagramUUID, ""); + } + for (var cls : classes) { + var doMRID = + DiagramLayoutServiceUtils.insertDiagramObject( + diagramLayoutModel, diagramUUID, "", cls.getUuid()); + DiagramLayoutServiceUtils.insertDiagramObjectPoint( + diagramLayoutModel, diagramUUID, doMRID); + } + ctx.commit(); } } @Override - public void deleteClassLayoutData(GraphIdentifier graphIdentifier, UUID classUUID) { + public void removeClassFromCustomDiagram( + GraphIdentifier graphIdentifier, UUID diagramUUID, UUID classUUID) { + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var diagram = ctx.getCustomDiagrams().get(diagramUUID); + if (diagram != null) { + var updated = diagram.getClasses(); + updated.removeIf(c -> c.getUuid().equals(classUUID)); + diagram.setClasses(updated); + } + var diagramLayoutModel = ctx.getDiagramLayout().getDiagramLayoutModel(); + var diagramObject = + DLObjectFetcher.fetchDiagramDOForClass( + diagramLayoutModel, diagramUUID, classUUID); + if (diagramObject != null) { + DLUpdates.deleteDiagramObjectCascade(diagramLayoutModel, diagramObject.getMRID()); + } + ctx.commit(); + } + } + + @Override + public void addClassesToCustomDatasetDiagram( + String datasetName, UUID diagramUUID, List classes) { + if (classes.isEmpty()) { + return; + } + var diagram = databasePort.getDatasetDiagrams(datasetName).get(diagramUUID); + if (diagram != null) { + var updated = diagram.getClasses(); + updated.addAll(classes); + diagram.setClasses(updated); + } var diagramLayoutModel = - databasePort - .getGraphWithContext(graphIdentifier) - .getDiagramLayout() - .getDiagramLayoutModel(); + databasePort.getDatasetDiagramLayout(datasetName).getDiagramLayoutModel(); + if (DLObjectFetcher.fetchDiagram(diagramLayoutModel, diagramUUID) == null) { + DiagramLayoutServiceUtils.insertDiagram(diagramLayoutModel, diagramUUID, ""); + } + for (var cls : classes) { + var doMRID = + DiagramLayoutServiceUtils.insertDiagramObject( + diagramLayoutModel, diagramUUID, "", cls.getUuid()); + DiagramLayoutServiceUtils.insertDiagramObjectPoint( + diagramLayoutModel, diagramUUID, doMRID); + } + } - for (var diagramObject : DLObjectFetcher.fetchAllDOs(diagramLayoutModel, classUUID)) { + @Override + public void removeClassFromCustomDatasetDiagram( + String datasetName, UUID diagramUUID, UUID classUUID) { + var diagram = databasePort.getDatasetDiagrams(datasetName).get(diagramUUID); + if (diagram != null) { + var updated = diagram.getClasses(); + updated.removeIf(c -> c.getUuid().equals(classUUID)); + diagram.setClasses(updated); + } + var diagramLayoutModel = + databasePort.getDatasetDiagramLayout(datasetName).getDiagramLayoutModel(); + var diagramObject = + DLObjectFetcher.fetchDiagramDOForClass(diagramLayoutModel, diagramUUID, classUUID); + if (diagramObject != null) { DLUpdates.deleteDiagramObjectCascade(diagramLayoutModel, diagramObject.getMRID()); } } diff --git a/backend/src/main/java/org/rdfarchitect/services/dl/update/packagelayout/UpdatePackageLayoutService.java b/backend/src/main/java/org/rdfarchitect/services/dl/update/packagelayout/UpdatePackageLayoutService.java index 71630fd4..010d9850 100644 --- a/backend/src/main/java/org/rdfarchitect/services/dl/update/packagelayout/UpdatePackageLayoutService.java +++ b/backend/src/main/java/org/rdfarchitect/services/dl/update/packagelayout/UpdatePackageLayoutService.java @@ -19,6 +19,7 @@ import lombok.RequiredArgsConstructor; +import org.apache.jena.query.ReadWrite; import org.rdfarchitect.api.dto.packages.PackageDTO; import org.rdfarchitect.api.dto.packages.PackageMapper; import org.rdfarchitect.database.DatabasePort; @@ -53,70 +54,52 @@ public void createPackageLayoutData( var cimPackage = packageMapper.toCIMObject(packageDTO); cimPackage.setUuid(newPackageUUID); - var diagramLayoutModel = - databasePort - .getGraphWithContext(graphIdentifier) - .getDiagramLayout() - .getDiagramLayoutModel(); - - DiagramLayoutServiceUtils.insertDiagram( - diagramLayoutModel, cimPackage.getUuid(), cimPackage.getLabel().getValue()); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var diagramLayoutModel = ctx.getDiagramLayout().getDiagramLayoutModel(); + DiagramLayoutServiceUtils.insertDiagram( + diagramLayoutModel, cimPackage.getUuid(), cimPackage.getLabel().getValue()); + ctx.commit(); + } } @Override public void deletePackageLayoutData(GraphIdentifier graphIdentifier, UUID packageUUID) { - var diagramLayoutModel = - databasePort - .getGraphWithContext(graphIdentifier) - .getDiagramLayout() - .getDiagramLayoutModel(); - var packageGraphFilter = new GraphFilter(false); packageGraphFilter.setIncludeInheritance(true); packageGraphFilter.setIncludeAssociations(true); packageGraphFilter.setIncludeRelationsToExternalPackages(true); packageGraphFilter.setPackageUUID(packageUUID.toString()); - var classesCIMCollection = converter.convert(graphIdentifier, packageGraphFilter); - for (var cimClassOrEnum : classesCIMCollection.getClassesAndEnums()) { - // if the class belongs to the package, delete all DOs and DOPs globally - if (cimClassOrEnum.getBelongsToCategory().getUuid() == packageUUID) { - for (var diagramObject : - DLObjectFetcher.fetchAllDOs(diagramLayoutModel, cimClassOrEnum.getUuid())) { - DLUpdates.deleteDiagramObjectCascade( - diagramLayoutModel, diagramObject.getMRID()); - } - } - // if the class belongs to a different package, only delete the DO/DOP in the diagram - else { + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var diagramLayoutModel = ctx.getDiagramLayout().getDiagramLayoutModel(); + + for (var cimClassOrEnum : classesCIMCollection.getClassesAndEnums()) { var diagramObject = DLObjectFetcher.fetchDiagramDOForClass( diagramLayoutModel, packageUUID, cimClassOrEnum.getUuid()); DLUpdates.deleteDiagramObjectCascade(diagramLayoutModel, diagramObject.getMRID()); } - } - DLUpdates.deleteDiagram(diagramLayoutModel, new MRID(packageUUID)); + DLUpdates.deleteDiagram(diagramLayoutModel, new MRID(packageUUID)); + ctx.commit(); + } } @Override public void replaceDiagram( GraphIdentifier graphIdentifier, UUID packageUUID, String packageName) { - var diagramLayoutModel = - databasePort - .getGraphWithContext(graphIdentifier) - .getDiagramLayout() - .getDiagramLayoutModel(); - var diagramMRID = new MRID(packageUUID); - - var newDiagram = - Diagram.builder() - .mRID(diagramMRID) - .name(packageName) - .orientation(OrientationKind.NEGATIVE) - .build(); - - DLUpdates.replaceDiagram(diagramLayoutModel, diagramMRID, newDiagram); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var diagramLayoutModel = ctx.getDiagramLayout().getDiagramLayoutModel(); + var diagramMRID = new MRID(packageUUID); + var newDiagram = + Diagram.builder() + .mRID(diagramMRID) + .name(packageName) + .orientation(OrientationKind.NEGATIVE) + .build(); + DLUpdates.replaceDiagram(diagramLayoutModel, diagramMRID, newDiagram); + ctx.commit(); + } } } diff --git a/backend/src/main/java/org/rdfarchitect/services/rendering/DiagramToCIMCollectionConverterService.java b/backend/src/main/java/org/rdfarchitect/services/rendering/DiagramToCIMCollectionConverterService.java index 5325d046..b196f1e5 100644 --- a/backend/src/main/java/org/rdfarchitect/services/rendering/DiagramToCIMCollectionConverterService.java +++ b/backend/src/main/java/org/rdfarchitect/services/rendering/DiagramToCIMCollectionConverterService.java @@ -40,8 +40,7 @@ public class DiagramToCIMCollectionConverterService @Override public CIMCollection convert(GraphIdentifier graphIdentifier, String diagramId) { - var graphWithContext = databasePort.getGraphWithContext(graphIdentifier); - var diagrams = graphWithContext.getCustomDiagrams(); + var diagrams = databasePort.getGraphWithContext(graphIdentifier).getCustomDiagrams(); var diagramUUID = UUID.fromString(diagramId); if (!diagrams.containsKey(diagramUUID)) { throw new IllegalArgumentException( diff --git a/backend/src/main/java/org/rdfarchitect/services/rendering/GraphToCIMCollectionConverterService.java b/backend/src/main/java/org/rdfarchitect/services/rendering/GraphToCIMCollectionConverterService.java index 21e36688..5baff55f 100644 --- a/backend/src/main/java/org/rdfarchitect/services/rendering/GraphToCIMCollectionConverterService.java +++ b/backend/src/main/java/org/rdfarchitect/services/rendering/GraphToCIMCollectionConverterService.java @@ -25,7 +25,7 @@ import org.apache.jena.graph.Graph; import org.apache.jena.graph.Node; import org.apache.jena.query.QueryExecutionFactory; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; import org.rdfarchitect.database.DatabasePort; @@ -46,7 +46,6 @@ import org.rdfarchitect.models.cim.rdf.resources.RDFA; import org.rdfarchitect.models.cim.rendering.GraphFilter; import org.rdfarchitect.rdf.graph.GraphUtils; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -62,19 +61,17 @@ public class GraphToCIMCollectionConverterService implements GraphToCIMCollectio @Override public CIMCollection convert(GraphIdentifier graphIdentifier, GraphFilter filter) { - var cimCollection = new CIMCollection(); Graph copiedGraph; - GraphRewindableWithUUIDs graph = null; - try { - graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - graph.begin(TxnType.READ); - copiedGraph = GraphUtils.deepCopy(graph); - } finally { - if (graph != null) { - graph.end(); - } + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + copiedGraph = GraphUtils.deepCopy(ctx.getRdfGraph()); } + return convert(copiedGraph, graphIdentifier, filter); + } + @Override + public CIMCollection convert( + Graph copiedGraph, GraphIdentifier graphIdentifier, GraphFilter filter) { + var cimCollection = new CIMCollection(); fetchAllPackages(copiedGraph, graphIdentifier, cimCollection); fetchClasses(copiedGraph, graphIdentifier, filter, cimCollection); fetchEnums(copiedGraph, graphIdentifier, filter, cimCollection); diff --git a/backend/src/main/java/org/rdfarchitect/services/rendering/GraphToCIMCollectionConverterUseCase.java b/backend/src/main/java/org/rdfarchitect/services/rendering/GraphToCIMCollectionConverterUseCase.java index b27d1653..4346569e 100644 --- a/backend/src/main/java/org/rdfarchitect/services/rendering/GraphToCIMCollectionConverterUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/rendering/GraphToCIMCollectionConverterUseCase.java @@ -17,6 +17,7 @@ package org.rdfarchitect.services.rendering; +import org.apache.jena.graph.Graph; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.models.cim.data.dto.CIMCollection; import org.rdfarchitect.models.cim.rendering.GraphFilter; @@ -25,11 +26,14 @@ public interface GraphToCIMCollectionConverterUseCase { /** - * Converts a Graph to a {@link CIMCollection}. - * - * @param graphIdentifier The graph to getClassDefinition. - * @param filter The filter to apply to the graph. - * @return The {@link CIMCollection}. + * Opens a READ transaction, copies the RDF graph, and converts it to a {@link CIMCollection}. */ CIMCollection convert(GraphIdentifier graphIdentifier, GraphFilter filter); + + /** + * Converts an already-copied RDF graph to a {@link CIMCollection} without opening a + * transaction. Use this when the caller already holds a transaction and has deep-copied the + * graph. + */ + CIMCollection convert(Graph copiedGraph, GraphIdentifier graphIdentifier, GraphFilter filter); } diff --git a/backend/src/main/java/org/rdfarchitect/services/schemamigration/SchemaMigrationService.java b/backend/src/main/java/org/rdfarchitect/services/schemamigration/SchemaMigrationService.java index ebb0a42c..d13b934a 100644 --- a/backend/src/main/java/org/rdfarchitect/services/schemamigration/SchemaMigrationService.java +++ b/backend/src/main/java/org/rdfarchitect/services/schemamigration/SchemaMigrationService.java @@ -20,7 +20,7 @@ import lombok.RequiredArgsConstructor; import org.apache.jena.graph.Graph; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.rdf.model.ModelFactory; import org.rdfarchitect.api.dto.migration.DefaultValueView; import org.rdfarchitect.api.dto.migration.PropertyOverview; @@ -37,7 +37,6 @@ import org.rdfarchitect.models.changes.semanticchanges.SemanticFieldChangeType; import org.rdfarchitect.rdf.graph.GraphUtils; import org.rdfarchitect.rdf.graph.source.builder.implementations.GraphFileSourceBuilderImpl; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; import org.rdfarchitect.services.compare.TripleChangeAnalyser; import org.rdfarchitect.services.schemamigration.defaults.DefaultValueAssigner; import org.rdfarchitect.services.schemamigration.defaults.GetDefaultValueViewsUseCase; @@ -87,13 +86,9 @@ public void setMigrationContext(MultipartFile originalSchema, GraphIdentifier up .graph(); Graph updatedGraph; - GraphRewindableWithUUIDs loadedGraph = - databasePort.getGraphWithContext(updatedSchema).getRdfGraph(); - try { - loadedGraph.begin(TxnType.READ); - updatedGraph = GraphUtils.deepCopy(loadedGraph); - } finally { - loadedGraph.end(); + try (var updatedCtx = + databasePort.getGraphWithContext(updatedSchema).begin(ReadWrite.READ)) { + updatedGraph = GraphUtils.deepCopy(updatedCtx.getRdfGraph()); } initContext(originalGraph, updatedGraph); @@ -102,22 +97,15 @@ public void setMigrationContext(MultipartFile originalSchema, GraphIdentifier up @Override public void setMigrationContext(GraphIdentifier originalSchema, GraphIdentifier updatedSchema) { Graph originalGraph; - GraphRewindableWithUUIDs loadedGraph = - databasePort.getGraphWithContext(originalSchema).getRdfGraph(); - try { - loadedGraph.begin(TxnType.READ); - originalGraph = GraphUtils.deepCopy(loadedGraph); - } finally { - loadedGraph.end(); + try (var originalCtx = + databasePort.getGraphWithContext(originalSchema).begin(ReadWrite.READ)) { + originalGraph = GraphUtils.deepCopy(originalCtx.getRdfGraph()); } Graph updatedGraph; - loadedGraph = databasePort.getGraphWithContext(updatedSchema).getRdfGraph(); - try { - loadedGraph.begin(TxnType.READ); - updatedGraph = GraphUtils.deepCopy(loadedGraph); - } finally { - loadedGraph.end(); + try (var updatedCtx = + databasePort.getGraphWithContext(updatedSchema).begin(ReadWrite.READ)) { + updatedGraph = GraphUtils.deepCopy(updatedCtx.getRdfGraph()); } initContext(originalGraph, updatedGraph); diff --git a/backend/src/main/java/org/rdfarchitect/services/select/QueryClassService.java b/backend/src/main/java/org/rdfarchitect/services/select/QueryClassService.java index c8ef280d..e432f7dd 100644 --- a/backend/src/main/java/org/rdfarchitect/services/select/QueryClassService.java +++ b/backend/src/main/java/org/rdfarchitect/services/select/QueryClassService.java @@ -19,7 +19,7 @@ import lombok.RequiredArgsConstructor; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.rdfarchitect.api.dto.ClassDTO; import org.rdfarchitect.api.dto.ClassMapper; import org.rdfarchitect.api.dto.ClassUMLAdaptedDTO; @@ -31,8 +31,6 @@ import org.rdfarchitect.models.cim.relations.CIMClassRelationFinder; import org.rdfarchitect.models.cim.relations.ClassRelationsDTO; import org.rdfarchitect.models.cim.umladapted.CIMUMLObjectFactory; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindable; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -53,11 +51,8 @@ public class QueryClassService @Override public ClassUMLAdaptedDTO getClassInformation( GraphIdentifier graphIdentifier, String classUUID) { - GraphRewindableWithUUIDs graph = null; - try { - graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - graph.begin(TxnType.READ); - + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + var graph = ctx.getRdfGraph(); var cimClass = CIMUMLObjectFactory.createCIMClassUMLAdapted( graph, @@ -68,20 +63,13 @@ public ClassUMLAdaptedDTO getClassInformation( return null; } return umlAdaptedClassMapper.toDTO(cimClass); - } finally { - if (graph != null) { - graph.end(); - } } } @Override public List listSuperClasses(GraphIdentifier graphIdentifier, UUID classUUID) { - GraphRewindableWithUUIDs graph = null; - try { - graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - graph.begin(TxnType.READ); - + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + var graph = ctx.getRdfGraph(); var superClassList = new ArrayList(); var cimObjectFetcher = new CIMObjectFetcher( @@ -100,25 +88,14 @@ public List listSuperClasses(GraphIdentifier graphIdentifier, UUID cla } } return mapper.toDTOList(superClassList); - } finally { - if (graph != null) { - graph.end(); - } } } @Override public ClassRelationsDTO getClassesReferencingThisClass( GraphIdentifier graphIdentifier, UUID classUUID) { - GraphRewindable graph = null; - try { - graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - graph.begin(TxnType.READ); - return CIMClassRelationFinder.getAllClassRelations(graph, classUUID); - } finally { - if (graph != null) { - graph.end(); - } + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + return CIMClassRelationFinder.getAllClassRelations(ctx.getRdfGraph(), classUUID); } } } diff --git a/backend/src/main/java/org/rdfarchitect/services/select/QueryDatasetService.java b/backend/src/main/java/org/rdfarchitect/services/select/QueryDatasetService.java index 7794862a..f6925328 100644 --- a/backend/src/main/java/org/rdfarchitect/services/select/QueryDatasetService.java +++ b/backend/src/main/java/org/rdfarchitect/services/select/QueryDatasetService.java @@ -20,7 +20,7 @@ import lombok.RequiredArgsConstructor; import org.apache.jena.query.DatasetFactory; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.riot.Lang; @@ -31,7 +31,6 @@ import org.rdfarchitect.models.cim.data.dto.CIMPrefixPair; import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; import org.rdfarchitect.rdf.graph.GraphUtils; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; import org.springframework.stereotype.Service; import java.io.ByteArrayOutputStream; @@ -74,18 +73,11 @@ public ByteArrayOutputStream getDatasetSchema(String datasetName, RDFFormat form } private Model getGraphAsModel(String datasetName, String graphURI) { - GraphRewindableWithUUIDs graph = null; - try { - graph = - databasePort - .getGraphWithContext(new GraphIdentifier(datasetName, graphURI)) - .getRdfGraph(); - graph.begin(TxnType.READ); - return ModelFactory.createModelForGraph(GraphUtils.deepCopy(graph)); - } finally { - if (graph != null) { - graph.end(); - } + try (var ctx = + databasePort + .getGraphWithContext(new GraphIdentifier(datasetName, graphURI)) + .begin(ReadWrite.READ)) { + return ModelFactory.createModelForGraph(GraphUtils.deepCopy(ctx.getRdfGraph())); } } 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 37ebe1bd..b32bbb60 100644 --- a/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java +++ b/backend/src/main/java/org/rdfarchitect/services/select/QueryGraphService.java @@ -19,15 +19,13 @@ 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.correctPackagePrefix; -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.QueryFactory; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.riot.RDFFormat; import org.apache.jena.vocabulary.RDF; @@ -42,6 +40,7 @@ import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.database.inmemory.InMemorySparqlExecutor; import org.rdfarchitect.exception.database.DataAccessException; +import org.rdfarchitect.models.cim.CIMModifyingUtils; import org.rdfarchitect.models.cim.CIMQuerySolutionParser; import org.rdfarchitect.models.cim.data.CIMObjectFactory; import org.rdfarchitect.models.cim.data.dto.CIMPackage; @@ -57,7 +56,6 @@ import org.rdfarchitect.models.cim.umladapted.CIMUMLObjectFactory; import org.rdfarchitect.models.cim.umladapted.data.CIMClassUMLAdapted; import org.rdfarchitect.rdf.graph.GraphUtils; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; import org.rdfarchitect.rdf.model.wrapper.CimSortedModel; import org.springframework.stereotype.Service; @@ -147,7 +145,7 @@ public List getClassList( // execute query var queryResultSet = InMemorySparqlExecutor.executeSingleQuery( - databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), + databasePort.getGraphWithContext(graphIdentifier), QueryFactory.create(query), null); @@ -181,7 +179,7 @@ private List getReferencedClassList(GraphIdentifier graphIde // execute query var queryResultSet = InMemorySparqlExecutor.executeSingleQuery( - databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), + databasePort.getGraphWithContext(graphIdentifier), query, graphIdentifier.graphUri()); @@ -212,7 +210,7 @@ public List listDatatypes(GraphIdentifier graphIdentifier) { // execute query var queryResultSet = InMemorySparqlExecutor.executeSingleQuery( - databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), + databasePort.getGraphWithContext(graphIdentifier), query, graphIdentifier.graphUri()); @@ -225,25 +223,20 @@ public List listDatatypes(GraphIdentifier graphIdentifier) { @Override public ByteArrayOutputStream getSchema(GraphIdentifier graphIdentifier, RDFFormat format) { - GraphRewindableWithUUIDs graph = null; - try (var out = new ByteArrayOutputStream()) { - graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - graph.begin(TxnType.READ); - var copiedGraph = GraphUtils.deepCopy(graph); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ); + var out = new ByteArrayOutputStream()) { + var copiedGraph = GraphUtils.deepCopy(ctx.getRdfGraph()); copiedGraph .getPrefixMapping() .setNsPrefixes(databasePort.getPrefixMapping(graphIdentifier.datasetName())); - removeUUIDs(copiedGraph); - correctPackagePrefix(copiedGraph, UserSettingsContext.get().usePackagePrefix()); + GraphUtils.removeUUIDs(copiedGraph); + CIMModifyingUtils.standardizePackagePrefix( + copiedGraph, UserSettingsContext.get().usePackagePrefix()); var sortedModel = new CimSortedModel(ModelFactory.createModelForGraph(copiedGraph)); sortedModel.write(out, format.getLang().getName()); return out; } catch (IOException e) { throw new UncheckedIOException(e); - } finally { - if (graph != null) { - graph.end(); - } } } @@ -269,7 +262,7 @@ public List listInternalPackages(GraphIdentifier graphIdentifier) { // execute package query var internalPackageQueryResultSet = InMemorySparqlExecutor.executeSingleQuery( - databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), + databasePort.getGraphWithContext(graphIdentifier), internalPackageQuery, graphIdentifier.graphUri()); @@ -306,7 +299,7 @@ public List listExternalPackages(GraphIdentifier graphIdentifier) { // execute external package query var externalPackageQueryResultSet = InMemorySparqlExecutor.executeSingleQuery( - databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), + databasePort.getGraphWithContext(graphIdentifier), externalPackageQuery, graphIdentifier.graphUri()); @@ -339,7 +332,7 @@ public List listPrimitives(GraphIdentifier graphIdentifier) // execute query var queryResultSet = InMemorySparqlExecutor.executeSingleQuery( - databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), + databasePort.getGraphWithContext(graphIdentifier), query, graphIdentifier.graphUri()); @@ -365,7 +358,7 @@ public List listStereotypes(GraphIdentifier graphIdentifier) { // execute query var queryResult = InMemorySparqlExecutor.executeSingleQuery( - databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), + databasePort.getGraphWithContext(graphIdentifier), query, graphIdentifier.graphUri()); @@ -388,21 +381,14 @@ public List listStereotypes(GraphIdentifier graphIdentifier) { @Override public UUID resolveIRI(GraphIdentifier graphIdentifier, String resourceIRI) { - GraphRewindableWithUUIDs graph = null; - try { - graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - graph.begin(TxnType.READ); - var model = ModelFactory.createModelForGraph(graph); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + var model = ModelFactory.createModelForGraph(ctx.getRdfGraph()); var resource = model.getResource(resourceIRI); if (resource == null || !model.contains(resource, RDFA.uuid)) { throw new DataAccessException("Resource with IRI " + resourceIRI + " not found."); } var uuidLiteral = model.getProperty(resource, RDFA.uuid).getObject().asLiteral(); return UUID.fromString(uuidLiteral.getString()); - } finally { - if (graph != null) { - graph.end(); - } } } } diff --git a/backend/src/main/java/org/rdfarchitect/services/select/ontology/GenerateOntologyEntriesService.java b/backend/src/main/java/org/rdfarchitect/services/select/ontology/GenerateOntologyEntriesService.java index 2cb689bf..2ea7509b 100644 --- a/backend/src/main/java/org/rdfarchitect/services/select/ontology/GenerateOntologyEntriesService.java +++ b/backend/src/main/java/org/rdfarchitect/services/select/ontology/GenerateOntologyEntriesService.java @@ -19,14 +19,13 @@ import lombok.RequiredArgsConstructor; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.rdf.model.ModelFactory; +import org.rdfarchitect.api.dto.ChangeLogEntryMapper; import org.rdfarchitect.api.dto.ontology.OntologyEntry; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.models.cim.ontology.OntologyGeneratableEntriesBuilder; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; -import org.rdfarchitect.services.ChangeLogUseCase; import org.springframework.stereotype.Service; import java.util.List; @@ -36,24 +35,19 @@ public class GenerateOntologyEntriesService implements GenerateOntologyEntriesUseCase { private final DatabasePort databasePort; - private final ChangeLogUseCase changeLogUseCase; + + private final ChangeLogEntryMapper changeLogEntryMapper; @Override public List generateOntologyEntries(GraphIdentifier graphIdentifier) { - GraphRewindableWithUUIDs graph = null; - try { - graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - graph.begin(TxnType.READ); - var model = ModelFactory.createModelForGraph(graph); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + var model = ModelFactory.createModelForGraph(ctx.getRdfGraph()); model.setNsPrefixes(databasePort.getPrefixMapping(graphIdentifier.datasetName())); return new OntologyGeneratableEntriesBuilder(model) - .generateDCTModified(changeLogUseCase.listChanges(graphIdentifier)) + .generateDCTModified( + changeLogEntryMapper.toDTOList(ctx.getChangeLog().getUndoHistory())) .generateDCTIssued() .build(); - } finally { - if (graph != null) { - graph.end(); - } } } } diff --git a/backend/src/main/java/org/rdfarchitect/services/select/ontology/ReadOntologyService.java b/backend/src/main/java/org/rdfarchitect/services/select/ontology/ReadOntologyService.java index adabb49f..37356c42 100644 --- a/backend/src/main/java/org/rdfarchitect/services/select/ontology/ReadOntologyService.java +++ b/backend/src/main/java/org/rdfarchitect/services/select/ontology/ReadOntologyService.java @@ -19,7 +19,7 @@ import lombok.RequiredArgsConstructor; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.rdf.model.ModelFactory; import org.rdfarchitect.api.dto.ontology.OntologyDTO; import org.rdfarchitect.api.dto.ontology.OntologyField; @@ -27,7 +27,6 @@ import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.models.cim.ontology.KnownOntologyFields; import org.rdfarchitect.models.cim.ontology.OntologyFacade; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; import org.springframework.stereotype.Service; import java.util.List; @@ -41,19 +40,10 @@ public class ReadOntologyService implements ReadOntologyUseCase, GetKnownOntolog // READ @Override public OntologyDTO getCurrentOntology(GraphIdentifier graphIdentifier) { - GraphRewindableWithUUIDs graph = null; - try { - graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - graph.begin(TxnType.READ); - var model = ModelFactory.createModelForGraph(graph); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + var model = ModelFactory.createModelForGraph(ctx.getRdfGraph()); model.setNsPrefixes(databasePort.getPrefixMapping(graphIdentifier.datasetName())); - - var ontology = new OntologyFacade(model); - return ontology.getOntology(); - } finally { - if (graph != null) { - graph.end(); - } + return new OntologyFacade(model).getOntology(); } } diff --git a/backend/src/main/java/org/rdfarchitect/services/shacl/SHACLGenerateService.java b/backend/src/main/java/org/rdfarchitect/services/shacl/SHACLGenerateService.java index c17427e2..14605857 100644 --- a/backend/src/main/java/org/rdfarchitect/services/shacl/SHACLGenerateService.java +++ b/backend/src/main/java/org/rdfarchitect/services/shacl/SHACLGenerateService.java @@ -19,14 +19,13 @@ import lombok.RequiredArgsConstructor; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.riot.Lang; import org.apache.jena.riot.system.PrefixEntry; import org.apache.jena.shacl.ShaclException; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindable; import org.rdfarchitect.shacl.SHACLFromCIMGenerator; import java.io.ByteArrayOutputStream; @@ -41,22 +40,16 @@ public class SHACLGenerateService implements SHACLGenerateUseCase { @Override public String exportGeneratedSHACLGraph( GraphIdentifier graphIdentifier, PrefixEntry shaclPrefix) { - GraphRewindable ontologyGraph = null; var prefixes = databasePort.getPrefixMapping(graphIdentifier.datasetName()); - try (var outStream = new ByteArrayOutputStream()) { - ontologyGraph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - ontologyGraph.begin(TxnType.READ); - var ontologyModel = ModelFactory.createModelForGraph(ontologyGraph); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ); + var outStream = new ByteArrayOutputStream()) { + var ontologyModel = ModelFactory.createModelForGraph(ctx.getRdfGraph()); ontologyModel.setNsPrefixes(prefixes); var shaclModel = new SHACLFromCIMGenerator(ontologyModel, shaclPrefix, true).generate(); shaclModel.write(outStream, Lang.TTL.getName()); return outStream.toString(StandardCharsets.UTF_8); } catch (IOException e) { throw new ShaclException("Error while writing SHACL model to output stream", e); - } finally { - if (ontologyGraph != null) { - ontologyGraph.end(); - } } } } diff --git a/backend/src/main/java/org/rdfarchitect/services/shacl/SHACLStoringService.java b/backend/src/main/java/org/rdfarchitect/services/shacl/SHACLStoringService.java index 3a105e1a..4d7d22d2 100644 --- a/backend/src/main/java/org/rdfarchitect/services/shacl/SHACLStoringService.java +++ b/backend/src/main/java/org/rdfarchitect/services/shacl/SHACLStoringService.java @@ -20,7 +20,7 @@ import lombok.RequiredArgsConstructor; import org.apache.jena.graph.Graph; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.RDFNode; @@ -37,7 +37,7 @@ import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.exception.database.DataAccessException; import org.rdfarchitect.models.cim.rdf.resources.RDFA; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindable; +import org.rdfarchitect.rdf.graph.GraphUtils; import org.rdfarchitect.rdf.merge.ModelResourceExclusiveMerge; import org.rdfarchitect.shacl.PropertyShapeToClassAssigner; import org.rdfarchitect.shacl.SHACLFromCIMGenerator; @@ -55,7 +55,6 @@ import java.io.StringReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.UUID; @@ -82,31 +81,49 @@ public class SHACLStoringService @Override public void replaceCustomSHACLGraph(GraphIdentifier graphIdentifier, Graph shacl) { - var customSHACL = ModelFactory.createModelForGraph(shacl); - databasePort.getGraphWithContext(graphIdentifier).setCustomSHACL(customSHACL); + var normalized = GraphUtils.normalizeBlankNodes(shacl); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var storedGraph = ctx.getCustomSHACL(); + + var toDelete = storedGraph.find().filterKeep(t -> !normalized.contains(t)).toList(); + var toAdd = normalized.find().filterKeep(t -> !storedGraph.contains(t)).toList(); + + if (toDelete.isEmpty() && toAdd.isEmpty()) { + return; + } + + toDelete.forEach(storedGraph::delete); + toAdd.forEach(storedGraph::add); + + var storedModel = ModelFactory.createModelForGraph(storedGraph); + storedModel.clearNsPrefixMap(); + storedModel.setNsPrefixes(ModelFactory.createModelForGraph(normalized)); + + ctx.commit("Replace custom SHACL"); + } } @Override public ByteArrayOutputStream exportCustomSHACLGraph( GraphIdentifier graphIdentifier, RDFFormat format) { - var customSHACL = databasePort.getGraphWithContext(graphIdentifier).getCustomSHACL(); - try (var outStream = new ByteArrayOutputStream()) { - customSHACL.write(outStream, format.getLang().getName()); - return outStream; - } catch (Exception e) { - logger.warn("Error while writing SHACL graph to output stream", e); - return new ByteArrayOutputStream(); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + var customSHACL = ModelFactory.createModelForGraph(ctx.getCustomSHACL()); + try (var outStream = new ByteArrayOutputStream()) { + customSHACL.write(outStream, format.getLang().getName()); + return outStream; + } catch (Exception e) { + logger.warn("Error while writing SHACL graph to output stream", e); + return new ByteArrayOutputStream(); + } } } @Override public ByteArrayOutputStream exportGeneratedSHACLGraph( GraphIdentifier graphIdentifier, RDFFormat format) { - GraphRewindable ontologyGraph = null; - try (var outStream = new ByteArrayOutputStream()) { - ontologyGraph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - ontologyGraph.begin(TxnType.READ); - var ontologyModel = ModelFactory.createModelForGraph(ontologyGraph); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ); + var outStream = new ByteArrayOutputStream()) { + var ontologyModel = ModelFactory.createModelForGraph(ctx.getRdfGraph()); ontologyModel.setNsPrefixes( databasePort.getPrefixMapping(graphIdentifier.datasetName())); var generatedShacl = @@ -116,10 +133,6 @@ public ByteArrayOutputStream exportGeneratedSHACLGraph( return outStream; } catch (IOException e) { throw new DataAccessException("Error while writing SHACL graph to output stream", e); - } finally { - if (ontologyGraph != null) { - ontologyGraph.end(); - } } } @@ -139,13 +152,9 @@ public ByteArrayOutputStream exportGeneratedSHACLGraph(Graph graph, RDFFormat fo @Override public ByteArrayOutputStream exportCombinedSHACLGraph( GraphIdentifier graphIdentifier, RDFFormat format) { - var graphContext = databasePort.getGraphWithContext(graphIdentifier); - var customSHACL = graphContext.getCustomSHACL(); - GraphRewindable ontologyGraph = null; - try { - ontologyGraph = graphContext.getRdfGraph(); - ontologyGraph.begin(TxnType.READ); - var ontologyModel = ModelFactory.createModelForGraph(ontologyGraph); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + var customSHACL = ModelFactory.createModelForGraph(ctx.getCustomSHACL()); + var ontologyModel = ModelFactory.createModelForGraph(ctx.getRdfGraph()); ontologyModel.setNsPrefixes( databasePort.getPrefixMapping(graphIdentifier.datasetName())); var generatedShacl = @@ -159,28 +168,23 @@ public ByteArrayOutputStream exportCombinedSHACLGraph( throw new DataAccessException( "Error while writing combined shacl graph to output stream", e); } - } finally { - if (ontologyGraph != null) { - ontologyGraph.end(); - } } } @Override public ByteArrayOutputStream exportCustomSHACLNamespaces( GraphIdentifier graphIdentifier, RDFFormat format) { - var customSHACL = databasePort.getGraphWithContext(graphIdentifier).getCustomSHACL(); - if (customSHACL == null) { - return new ByteArrayOutputStream(); - } - try (var outStream = new ByteArrayOutputStream()) { - var prefixModel = ModelFactory.createDefaultModel(); - prefixModel.setNsPrefixes(customSHACL.getNsPrefixMap()); - prefixModel.write(outStream, format.getLang().getName()); - return outStream; - } catch (Exception e) { - logger.warn("Error while writing SHACL prefixes to output stream", e); - return new ByteArrayOutputStream(); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + var customSHACL = ModelFactory.createModelForGraph(ctx.getCustomSHACL()); + try (var outStream = new ByteArrayOutputStream()) { + var prefixModel = ModelFactory.createDefaultModel(); + prefixModel.setNsPrefixes(customSHACL.getNsPrefixMap()); + prefixModel.write(outStream, format.getLang().getName()); + return outStream; + } catch (Exception e) { + logger.warn("Error while writing SHACL prefixes to output stream", e); + return new ByteArrayOutputStream(); + } } } @@ -202,29 +206,19 @@ public ByteArrayOutputStream exportGeneratedSHACLNamespaces( @Override public CustomAndGeneratedTuple getSHACLToClassRelations( GraphIdentifier graphIdentifier, UUID classUUID) { - GraphRewindable ontologyGraph = null; - var customSHACL = databasePort.getGraphWithContext(graphIdentifier).getCustomSHACL(); - try { - ontologyGraph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - ontologyGraph.begin(TxnType.READ); - var ontologyModel = ModelFactory.createModelForGraph(ontologyGraph); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + var customSHACL = ModelFactory.createModelForGraph(ctx.getCustomSHACL()); + var ontologyModel = ModelFactory.createModelForGraph(ctx.getRdfGraph()); ontologyModel.setNsPrefixes( databasePort.getPrefixMapping(graphIdentifier.datasetName())); var generatedSHACL = new SHACLFromCIMGenerator(ontologyModel, SHACL_NAMESPACE, true) .generateForClassOnly(classUUID); var shaclResult = new CustomAndGeneratedTuple(); - if (customSHACL != null) { - shaclResult.setCustom( - getSHACLToClassRelations(ontologyModel, customSHACL, classUUID)); - } + shaclResult.setCustom(getSHACLToClassRelations(ontologyModel, customSHACL, classUUID)); shaclResult.setGenerated( getSHACLToClassRelations(ontologyModel, generatedSHACL, classUUID)); return shaclResult; - } finally { - if (ontologyGraph != null) { - ontologyGraph.end(); - } } } @@ -271,12 +265,9 @@ public CustomAndGeneratedTuple> getPropertyShapesForAssociat private CustomAndGeneratedTuple> getSHACLShapesByProperty( GraphIdentifier graphIdentifier, UUID propertyUUID) { - var customSHACL = databasePort.getGraphWithContext(graphIdentifier).getCustomSHACL(); - GraphRewindable ontologyGraph = null; - try { - ontologyGraph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - ontologyGraph.begin(TxnType.READ); - var ontologyModel = ModelFactory.createModelForGraph(ontologyGraph); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + var customSHACL = ModelFactory.createModelForGraph(ctx.getCustomSHACL()); + var ontologyModel = ModelFactory.createModelForGraph(ctx.getRdfGraph()); var property = ontologyModel .listSubjectsWithProperty( @@ -291,34 +282,43 @@ private CustomAndGeneratedTuple> getSHACLShapesByProperty( new SHACLFromCIMGenerator(ontologyModel, SHACL_NAMESPACE, true) .generateForClassOnly(UUID.fromString(classUUID)); - List customPropertyShapes = Collections.emptyList(); - if (customSHACL != null) { - customPropertyShapes = - new SHACLShapesFetcher(customSHACL) - .getPropertyShapesOfProperty(ontologyModel, property.getURI()); - } + List customPropertyShapes = + getCustomPropertyShapesOfProperty(ontologyModel, customSHACL, propertyUUID); var generatedPropertyShapes = new SHACLShapesFetcher(generatedShacl) .getPropertyShapesOfProperty(ontologyModel, property.getURI()); return new CustomAndGeneratedTuple>() .setCustom(customPropertyShapes) .setGenerated(generatedPropertyShapes); - } finally { - if (ontologyGraph != null) { - ontologyGraph.end(); - } } } + /** + * Retrieves the custom property shapes for a given property UUID without managing its own + * transaction. This allows callers to use it within an existing transaction context. + * + * @param ontologyModel the ontology model + * @param customSHACL the model containing the custom SHACL shapes + * @param propertyUUID the property UUID + * @return a list of custom property shapes for the given property + */ + private List getCustomPropertyShapesOfProperty( + Model ontologyModel, Model customSHACL, UUID propertyUUID) { + var property = + ontologyModel + .listSubjectsWithProperty( + RDFA.uuid, ontologyModel.createLiteral(propertyUUID.toString())) + .next(); + return new SHACLShapesFetcher(customSHACL) + .getPropertyShapesOfProperty(ontologyModel, property.getURI()); + } + @Override public CustomAndGeneratedTuple> getNodeShapesForClass( GraphIdentifier graphIdentifier, UUID classUUID) { - var customSHACL = databasePort.getGraphWithContext(graphIdentifier).getCustomSHACL(); - GraphRewindable ontologyGraph = null; - try { - ontologyGraph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - ontologyGraph.begin(TxnType.READ); - var ontologyModel = ModelFactory.createModelForGraph(ontologyGraph); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + var customSHACL = ModelFactory.createModelForGraph(ctx.getCustomSHACL()); + var ontologyModel = ModelFactory.createModelForGraph(ctx.getRdfGraph()); var classUri = ontologyModel .listSubjectsWithProperty( @@ -335,52 +335,46 @@ public CustomAndGeneratedTuple> getNodeShapesForClass( return new CustomAndGeneratedTuple>() .setCustom(customNodeShapes) .setGenerated(generatedNodeShapes); - } finally { - if (ontologyGraph != null) { - ontologyGraph.end(); - } } } @Override public List getPropertyShapes( GraphIdentifier graphIdentifier, UUID classUUID) { - var customSHACL = databasePort.getGraphWithContext(graphIdentifier).getCustomSHACL(); - GraphRewindable ontologyGraph = null; - try { - ontologyGraph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - ontologyGraph.begin(TxnType.READ); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + var customSHACL = ModelFactory.createModelForGraph(ctx.getCustomSHACL()); var shaclToClassAssigner = new PropertyShapeToClassAssigner( - customSHACL, ModelFactory.createModelForGraph(ontologyGraph)); + customSHACL, ModelFactory.createModelForGraph(ctx.getRdfGraph())); return shaclToClassAssigner.getPropertyShapes(classUUID); - } finally { - if (ontologyGraph != null) { - ontologyGraph.end(); - } } } @Override public void deleteSHACLShape(GraphIdentifier graphIdentifier, String shaclShapeURI) { - var customSHACL = databasePort.getGraphWithContext(graphIdentifier).getCustomSHACL(); - var deleteModel = ModelFactory.createDefaultModel(); - copySHACLShapeToNewModel( - customSHACL, deleteModel, ResourceFactory.createResource(shaclShapeURI)); - customSHACL.remove(deleteModel); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var customSHACL = ModelFactory.createModelForGraph(ctx.getCustomSHACL()); + var deleteModel = ModelFactory.createDefaultModel(); + copySHACLShapeToNewModel( + customSHACL, deleteModel, ResourceFactory.createResource(shaclShapeURI)); + customSHACL.remove(deleteModel); + ctx.commit("Delete SHACL shape"); + } } @Override public void replaceSHACLShape( GraphIdentifier graphIdentifier, String shaclShapeURI, String shaclToInsert) { - var customSHACL = databasePort.getGraphWithContext(graphIdentifier).getCustomSHACL(); - Model insertModel = parseTriplesToModel(shaclToInsert); - Model deleteModel = ModelFactory.createDefaultModel(); - copySHACLShapeToNewModel( - customSHACL, deleteModel, ResourceFactory.createResource(shaclShapeURI)); - - customSHACL.remove(deleteModel); - customSHACL.add(insertModel); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var customSHACL = ModelFactory.createModelForGraph(ctx.getCustomSHACL()); + Model insertModel = parseTriplesToModel(shaclToInsert); + Model deleteModel = ModelFactory.createDefaultModel(); + copySHACLShapeToNewModel( + customSHACL, deleteModel, ResourceFactory.createResource(shaclShapeURI)); + customSHACL.remove(deleteModel); + customSHACL.add(insertModel); + ctx.commit("Replace SHACL shape"); + } } private Model parseTriplesToModel(String triples) { @@ -396,13 +390,9 @@ private Model parseTriplesToModel(String triples) { @Override public void updateClassSHACL( GraphIdentifier graphIdentifier, UUID classUUID, String ttlShaclString) { - var customSHACL = databasePort.getGraphWithContext(graphIdentifier).getCustomSHACL(); - GraphRewindable ontologyGraph = null; - try { - ontologyGraph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - ontologyGraph.begin(TxnType.READ); - var ontologyModel = ModelFactory.createModelForGraph(ontologyGraph); - + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var customSHACL = ModelFactory.createModelForGraph(ctx.getCustomSHACL()); + var ontologyModel = ModelFactory.createModelForGraph(ctx.getRdfGraph()); var insertModel = parseTriplesToModel(ttlShaclString); var deleteModel = getClassShaclModel(ontologyModel, customSHACL, classUUID); @@ -410,31 +400,35 @@ public void updateClassSHACL( customSHACL.clearNsPrefixMap(); customSHACL.setNsPrefixes(insertModel); customSHACL.add(insertModel); - } finally { - if (ontologyGraph != null) { - ontologyGraph.end(); - } + ctx.commit("Update class SHACL"); } } @Override public void updatePropertyShacl( GraphIdentifier graphIdentifier, UUID propertyUUID, String ttlShaclString) { - var customSHACL = databasePort.getGraphWithContext(graphIdentifier).getCustomSHACL(); var insertModel = parseTriplesToModel(ttlShaclString); - var propertyShapesOfProperty = getSHACLShapesByProperty(graphIdentifier, propertyUUID); - var deleteModel = ModelFactory.createDefaultModel(); - for (var propertyShape : propertyShapesOfProperty.getCustom()) { - copySHACLShapeToNewModel( - customSHACL, - deleteModel, - ResourceFactory.createResource(propertyShape.getId())); - } - customSHACL.remove(deleteModel); - customSHACL.clearNsPrefixMap(); - customSHACL.setNsPrefixes(insertModel); - customSHACL.add(insertModel); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var customSHACL = ModelFactory.createModelForGraph(ctx.getCustomSHACL()); + var ontologyModel = ModelFactory.createModelForGraph(ctx.getRdfGraph()); + + var customPropertyShapes = + getCustomPropertyShapesOfProperty(ontologyModel, customSHACL, propertyUUID); + + var deleteModel = ModelFactory.createDefaultModel(); + for (var propertyShape : customPropertyShapes) { + copySHACLShapeToNewModel( + customSHACL, + deleteModel, + ResourceFactory.createResource(propertyShape.getId())); + } + customSHACL.remove(deleteModel); + customSHACL.clearNsPrefixMap(); + customSHACL.setNsPrefixes(insertModel); + customSHACL.add(insertModel); + ctx.commit("Update property SHACL"); + } } /** diff --git a/backend/src/main/java/org/rdfarchitect/services/update/classes/CopyClassService.java b/backend/src/main/java/org/rdfarchitect/services/update/classes/CopyClassService.java new file mode 100644 index 00000000..cc95c7b0 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/services/update/classes/CopyClassService.java @@ -0,0 +1,394 @@ +/* + * 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.update.classes; + +import org.apache.jena.arq.querybuilder.ExprFactory; +import org.apache.jena.arq.querybuilder.SelectBuilder; +import org.apache.jena.graph.Graph; +import org.apache.jena.graph.NodeFactory; +import org.apache.jena.query.QueryExecutionFactory; +import org.apache.jena.query.ReadWrite; +import org.apache.jena.query.ResultSetFactory; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import org.rdfarchitect.api.dto.CopyClassRequestDTO; +import org.rdfarchitect.api.dto.CopyClassResponseDTO; +import org.rdfarchitect.api.dto.packages.PackageMapper; +import org.rdfarchitect.database.DatabasePort; +import org.rdfarchitect.database.GraphIdentifier; +import org.rdfarchitect.database.inmemory.SessionDataStore; +import org.rdfarchitect.models.cim.data.dto.CIMAssociation; +import org.rdfarchitect.models.cim.data.dto.CIMAssociationPair; +import org.rdfarchitect.models.cim.data.dto.CIMAttribute; +import org.rdfarchitect.models.cim.data.dto.CIMClass; +import org.rdfarchitect.models.cim.data.dto.CIMEnumEntry; +import org.rdfarchitect.models.cim.data.dto.CIMPackage; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSBelongsToCategory; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSInverseRoleName; +import org.rdfarchitect.models.cim.data.dto.relations.RDFSComment; +import org.rdfarchitect.models.cim.data.dto.relations.RDFSDomain; +import org.rdfarchitect.models.cim.data.dto.relations.RDFSLabel; +import org.rdfarchitect.models.cim.data.dto.relations.RDFType; +import org.rdfarchitect.models.cim.data.dto.relations.datatype.RDFSRange; +import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; +import org.rdfarchitect.models.cim.queries.update.CIMUpdates; +import org.rdfarchitect.models.cim.rdf.resources.CIMStereotypes; +import org.rdfarchitect.models.cim.umladapted.CIMUMLObjectFactory; +import org.rdfarchitect.models.cim.umladapted.data.CIMClassUMLAdapted; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +@Service +public class CopyClassService implements CopyClassUseCase { + + private final DatabasePort databasePort; + private final PackageMapper packageMapper; + private final boolean newValuesAsBlankNode; + + public CopyClassService( + DatabasePort databasePort, + PackageMapper packageMapper, + @Value("${attributes.newValuesBlankNode:false}") boolean newValuesAsBlankNode) { + this.databasePort = databasePort; + this.packageMapper = packageMapper; + this.newValuesAsBlankNode = newValuesAsBlankNode; + } + + @Override + public CopyClassResponseDTO copyClass( + GraphIdentifier graphIdentifier, + UUID classUUID, + GraphIdentifier targetGraphIdentifier, + CopyClassRequestDTO copyClassRequestDTO) { + + var cimClass = readSourceClass(graphIdentifier, classUUID.toString()); + + try (var ctx = + databasePort.getGraphWithContext(targetGraphIdentifier).begin(ReadWrite.WRITE)) { + var targetGraph = ctx.getRdfGraph(); + + var label = + constructUniqueLabel( + cimClass.getLabel(), + getExistingClassLabels(targetGraph, cimClass.getLabel())); + + var cimPackage = packageMapper.toCIMObject(copyClassRequestDTO.getTargetPackage()); + var newCimClass = + copyCimClass( + cimClass, + cimPackage, + label, + targetGraph, + copyClassRequestDTO.isCopyAsAbstract(), + copyClassRequestDTO.isCopyAttributes(), + copyClassRequestDTO.isCopyAssociations()); + + var newClassUUID = + CIMUpdates.insertUMLAdaptedClass( + targetGraph, + databasePort.getPrefixMapping(targetGraphIdentifier.datasetName()), + newCimClass, + newValuesAsBlankNode); + + ctx.commit(buildCopyMessage(cimClass, copyClassRequestDTO, label)); + return new CopyClassResponseDTO(newClassUUID.toString(), label.getValue()); + } + } + + private CIMClassUMLAdapted readSourceClass(GraphIdentifier graphIdentifier, String classUUID) { + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + return CIMUMLObjectFactory.createCIMClassUMLAdapted( + ctx.getRdfGraph(), + graphIdentifier.graphUri(), + databasePort.getPrefixMapping(graphIdentifier.datasetName()), + classUUID); + } + } + + private String buildCopyMessage( + CIMClassUMLAdapted sourceClass, CopyClassRequestDTO request, RDFSLabel label) { + var sourcePackage = + sourceClass.getBelongsToCategory() != null + ? sourceClass.getBelongsToCategory().getLabel().getValue() + : "default"; + var targetPackage = + request.getTargetPackage() != null + ? request.getTargetPackage().getLabel() + : "default"; + + var suffix = ""; + if (request.isCopyAsAbstract()) { + suffix = " bare"; + } else if (!request.isCopyAttributes()) { + suffix = " without attributes"; + } else if (!request.isCopyAssociations()) { + suffix = " without associations"; + } + + return "Copied class %s from %s to %s as %s%s" + .formatted( + sourceClass.getLabel().getValue(), + sourcePackage, + targetPackage, + label.getValue(), + suffix); + } + + private CIMClassUMLAdapted copyCimClass( + CIMClassUMLAdapted cimClass, + CIMPackage cimPackage, + RDFSLabel label, + Graph targetGraph, + boolean copyAsAbstract, + boolean copyAttributes, + boolean copyAssociations) { + var stereotypes = + copyAsAbstract + ? cimClass.getStereotypes().stream() + .filter( + s -> + !s.getStereotype() + .equals(CIMStereotypes.concreteString)) + .toList() + : cimClass.getStereotypes(); + var newCimClass = + CIMClassUMLAdapted.builder() + .uuid(UUID.randomUUID()) + .uri(new URI(cimClass.getUri().getPrefix() + label.getValue())) + .label(label) + .stereotypes(stereotypes); + if (!copyAsAbstract) { + newCimClass.superClass(cimClass.getSuperClass()); + } + if (cimClass.getComment() != null) { + newCimClass.comment( + new RDFSComment( + cimClass.getComment().getValue(), cimClass.getComment().getFormat())); + } + if (cimPackage != null) { + newCimClass.belongsToCategory( + new CIMSBelongsToCategory( + cimPackage.getUri(), cimPackage.getLabel(), cimPackage.getUuid())); + } + newCimClass.attributes( + copyAttributes(cimClass.getAttributes(), newCimClass.build(), copyAttributes)); + newCimClass.enumEntries( + copyEnumEntries(cimClass.getEnumEntries(), newCimClass.build(), copyAttributes)); + newCimClass.associationPairs( + copyAssociations( + cimClass.getAssociationPairs(), + cimClass, + newCimClass.build(), + targetGraph, + copyAssociations)); + + return newCimClass.build(); + } + + private List copyAttributes( + List attributes, CIMClass cimClass, boolean copyAttributes) { + if (!copyAttributes) { + return List.of(); + } + return attributes.stream() + .map( + attr -> { + var uri = + new URI( + cimClass.getUri().getPrefix() + + cimClass.getUri().getSuffix() + + "." + + attr.getLabel().getValue()); + var domain = + new RDFSDomain( + cimClass.getUri(), + new RDFSLabel(cimClass.getUri().getSuffix(), "en")); + return attr.toBuilder() + .uuid(UUID.randomUUID()) + .uri(uri) + .domain(domain) + .build(); + }) + .toList(); + } + + private List copyEnumEntries( + List enumEntries, CIMClass cimClass, boolean copyEnumEntries) { + if (!copyEnumEntries) { + return List.of(); + } + return enumEntries.stream() + .map( + entry -> { + var rdfType = + new RDFType( + cimClass.getUri(), + new RDFSLabel(cimClass.getUri().getSuffix(), "en")); + var uri = + new URI( + cimClass.getUri().getPrefix() + + cimClass.getUri().getSuffix() + + "." + + entry.getLabel().getValue()); + return entry.toBuilder() + .uuid(UUID.randomUUID()) + .type(rdfType) + .uri(uri) + .build(); + }) + .toList(); + } + + private List copyAssociations( + List pairs, + CIMClass sourceClass, + CIMClass newClass, + Graph graph, + boolean copyAssociations) { + if (!copyAssociations) { + return List.of(); + } + return pairs.stream() + .filter(pair -> pair.getFrom().getDomain().getUri().equals(sourceClass.getUri())) + .map(pair -> copyAssociationPair(pair, newClass, graph)) + .filter(Objects::nonNull) + .toList(); + } + + private CIMAssociationPair copyAssociationPair( + CIMAssociationPair pair, CIMClass newClass, Graph graph) { + var existingToLabels = + getExistingAssociationLabels( + graph, pair.getTo().getDomain().getUri(), pair.getTo().getLabel()); + var newToLabel = constructUniqueLabel(pair.getTo().getLabel(), existingToLabels); + var existingFromLabels = + getExistingAssociationLabels(graph, newClass.getUri(), pair.getFrom().getLabel()); + if (!existingFromLabels.isEmpty()) { + return null; + } + var newFromLabel = constructUniqueLabel(pair.getFrom().getLabel(), existingFromLabels); + + var from = buildFromAssociation(pair.getFrom(), newClass, newToLabel, newFromLabel); + var to = buildToAssociation(pair.getTo(), newClass, newToLabel, newFromLabel); + + return new CIMAssociationPair(from, to); + } + + private CIMAssociation buildFromAssociation( + CIMAssociation original, + CIMClass newClass, + RDFSLabel newToLabel, + RDFSLabel newFromLabel) { + return original.toBuilder() + .uuid(UUID.randomUUID()) + .label(newFromLabel) + .uri(new URI(newClass.getUri() + "." + newFromLabel.getValue())) + .domain( + new RDFSDomain( + newClass.getUri(), + new RDFSLabel(newClass.getUri().getSuffix(), "en"))) + .inverseRoleName( + new CIMSInverseRoleName( + original.getRange().getUri() + "." + newToLabel.getValue())) + .build(); + } + + private CIMAssociation buildToAssociation( + CIMAssociation original, + CIMClass newClass, + RDFSLabel newToLabel, + RDFSLabel newFromLabel) { + return original.toBuilder() + .uuid(UUID.randomUUID()) + .label(newToLabel) + .uri(new URI(original.getDomain().getUri() + "." + newToLabel.getValue())) + .range( + new RDFSRange( + newClass.getUri(), + new RDFSLabel(newClass.getUri().getSuffix(), "en"))) + .inverseRoleName( + new CIMSInverseRoleName(newClass.getUri() + "." + newFromLabel.getValue())) + .build(); + } + + private Set getExistingClassLabels(Graph graph, RDFSLabel label) { + var baseValue = label.getValue(); + var exprFactory = new ExprFactory(); + var query = + new SelectBuilder() + .addVar("?label") + .addWhere("?s", RDF.type, RDFS.Class) + .addWhere("?s", RDFS.label, "?label") + .addFilter(exprFactory.strstarts(exprFactory.str("?label"), baseValue)) + .build(); + + var dataset = SessionDataStore.wrapGraphInDataset(graph, null); + try (var queryExecution = QueryExecutionFactory.create(query, dataset)) { + var resultSet = ResultSetFactory.copyResults(queryExecution.execSelect()); + var existingLabels = new HashSet(); + while (resultSet.hasNext()) { + existingLabels.add(resultSet.nextSolution().getLiteral("label").getString()); + } + return existingLabels; + } + } + + private Set getExistingAssociationLabels(Graph graph, URI domainUri, RDFSLabel label) { + var exprFactory = new ExprFactory(); + var query = + new SelectBuilder() + .addVar("?label") + .addWhere("?s", RDFS.domain, NodeFactory.createURI(domainUri.toString())) + .addWhere("?s", RDFS.label, "?label") + .addFilter( + exprFactory.strstarts(exprFactory.str("?label"), label.getValue())) + .build(); + + var dataset = SessionDataStore.wrapGraphInDataset(graph, null); + try (var queryExecution = QueryExecutionFactory.create(query, dataset)) { + var resultSet = ResultSetFactory.copyResults(queryExecution.execSelect()); + var existingLabels = new HashSet(); + while (resultSet.hasNext()) { + existingLabels.add(resultSet.nextSolution().getLiteral("label").getString()); + } + return existingLabels; + } + } + + private RDFSLabel constructUniqueLabel(RDFSLabel label, Set existingLabels) { + var baseValue = label.getValue(); + if (!existingLabels.contains(baseValue)) { + return new RDFSLabel(baseValue, label.getLang()); + } + baseValue = label.getValue() + "-Copy"; + if (!existingLabels.contains(baseValue)) { + return new RDFSLabel(baseValue, label.getLang()); + } + var counter = 1; + while (existingLabels.contains(baseValue + "(" + counter + ")")) { + counter++; + } + return new RDFSLabel(baseValue + "(" + counter + ")", label.getLang()); + } +} diff --git a/backend/src/main/java/org/rdfarchitect/services/update/classes/DeleteClassUseCase.java b/backend/src/main/java/org/rdfarchitect/services/update/classes/DeleteClassUseCase.java index 68529a4f..2ef2c289 100644 --- a/backend/src/main/java/org/rdfarchitect/services/update/classes/DeleteClassUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/update/classes/DeleteClassUseCase.java @@ -19,6 +19,8 @@ import org.rdfarchitect.database.GraphIdentifier; +import java.util.UUID; + /** Use case for deleting a class from the graph. */ public interface DeleteClassUseCase { @@ -28,5 +30,5 @@ public interface DeleteClassUseCase { * @param graphIdentifier Graph URI and dataset name of the class to be deleted * @param classUUID UUID of the class to be deleted */ - void deleteClass(GraphIdentifier graphIdentifier, String classUUID); + void deleteClass(GraphIdentifier graphIdentifier, UUID classUUID); } diff --git a/backend/src/main/java/org/rdfarchitect/services/update/classes/UpdateClassService.java b/backend/src/main/java/org/rdfarchitect/services/update/classes/UpdateClassService.java index bfd605d8..7debaca2 100644 --- a/backend/src/main/java/org/rdfarchitect/services/update/classes/UpdateClassService.java +++ b/backend/src/main/java/org/rdfarchitect/services/update/classes/UpdateClassService.java @@ -17,45 +17,25 @@ package org.rdfarchitect.services.update.classes; -import org.apache.jena.arq.querybuilder.ExprFactory; -import org.apache.jena.arq.querybuilder.SelectBuilder; -import org.apache.jena.graph.NodeFactory; -import org.apache.jena.query.TxnType; +import org.apache.jena.graph.Graph; +import org.apache.jena.query.ReadWrite; import org.apache.jena.vocabulary.RDF; -import org.apache.jena.vocabulary.RDFS; import org.rdfarchitect.api.dto.ClassUMLAdaptedDTO; import org.rdfarchitect.api.dto.ClassUMLAdaptedMapper; -import org.rdfarchitect.api.dto.CopyClassRequestDTO; -import org.rdfarchitect.api.dto.CopyClassResponseDTO; import org.rdfarchitect.api.dto.dl.ClassLayoutPositionDTO; import org.rdfarchitect.api.dto.packages.PackageDTO; import org.rdfarchitect.api.dto.packages.PackageMapper; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; -import org.rdfarchitect.database.inmemory.InMemorySparqlExecutor; import org.rdfarchitect.exception.database.ResourceConflictException; -import org.rdfarchitect.models.changelog.ChangeLogEntry; -import org.rdfarchitect.models.cim.data.dto.CIMAssociation; -import org.rdfarchitect.models.cim.data.dto.CIMAssociationPair; -import org.rdfarchitect.models.cim.data.dto.CIMAttribute; import org.rdfarchitect.models.cim.data.dto.CIMClass; -import org.rdfarchitect.models.cim.data.dto.CIMEnumEntry; import org.rdfarchitect.models.cim.data.dto.CIMPackage; import org.rdfarchitect.models.cim.data.dto.relations.CIMSBelongsToCategory; -import org.rdfarchitect.models.cim.data.dto.relations.CIMSInverseRoleName; -import org.rdfarchitect.models.cim.data.dto.relations.RDFSComment; -import org.rdfarchitect.models.cim.data.dto.relations.RDFSDomain; import org.rdfarchitect.models.cim.data.dto.relations.RDFSLabel; -import org.rdfarchitect.models.cim.data.dto.relations.RDFType; -import org.rdfarchitect.models.cim.data.dto.relations.datatype.RDFSRange; import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; import org.rdfarchitect.models.cim.queries.update.CIMUpdates; import org.rdfarchitect.models.cim.rdf.resources.CIMS; -import org.rdfarchitect.models.cim.rdf.resources.CIMStereotypes; -import org.rdfarchitect.models.cim.umladapted.CIMUMLObjectFactory; -import org.rdfarchitect.models.cim.umladapted.data.CIMClassUMLAdapted; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; -import org.rdfarchitect.services.ChangeLogUseCase; +import org.rdfarchitect.models.cim.relations.model.CIMResourceUtils; import org.rdfarchitect.services.diagrams.RemoveFromDiagramUseCase; import org.rdfarchitect.services.dl.update.classlayout.CreateClassLayoutDataUseCase; import org.rdfarchitect.services.dl.update.classlayout.DeleteClassLayoutDataUseCase; @@ -63,20 +43,15 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; import java.util.UUID; @Service public class UpdateClassService - implements AddClassUseCase, ReplaceClassUseCase, DeleteClassUseCase, CopyClassUseCase { + implements AddClassUseCase, ReplaceClassUseCase, DeleteClassUseCase { private final DatabasePort databasePort; private final ClassUMLAdaptedMapper classMapper; private final PackageMapper packageMapper; - private final ChangeLogUseCase changeLogUseCase; private final boolean newValuesAsBlankNode; private final CreateClassLayoutDataUseCase createClassLayoutDataUseCase; @@ -88,7 +63,6 @@ public UpdateClassService( DatabasePort databasePort, ClassUMLAdaptedMapper classMapper, PackageMapper packageMapper, - ChangeLogUseCase changeLogUseCase, CreateClassLayoutDataUseCase createClassLayoutDataUseCase, UpdateDiagramObjectNameUseCase updateDiagramObjectNameUseCase, DeleteClassLayoutDataUseCase deleteClassLayoutDataUseCase, @@ -97,7 +71,6 @@ public UpdateClassService( this.databasePort = databasePort; this.classMapper = classMapper; this.packageMapper = packageMapper; - this.changeLogUseCase = changeLogUseCase; this.createClassLayoutDataUseCase = createClassLayoutDataUseCase; this.updateDiagramObjectNameUseCase = updateDiagramObjectNameUseCase; this.deleteClassLayoutDataUseCase = deleteClassLayoutDataUseCase; @@ -107,10 +80,8 @@ public UpdateClassService( @Override public void replaceClass(GraphIdentifier graphIdentifier, ClassUMLAdaptedDTO newClass) { - GraphRewindableWithUUIDs graph = null; - try { - graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - graph.begin(TxnType.WRITE); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var graph = ctx.getRdfGraph(); var cimClass = classMapper.toCIMObject(newClass); assertNoPackageWithSameIri(graph, cimClass); CIMUpdates.replaceClass( @@ -118,19 +89,12 @@ public void replaceClass(GraphIdentifier graphIdentifier, ClassUMLAdaptedDTO new databasePort.getPrefixMapping(graphIdentifier.datasetName()), cimClass, newValuesAsBlankNode); - graph.commit(); - } finally { - if (graph != null) { - graph.end(); - } + ctx.commit( + "Updated class \"%s\" (%s)".formatted(newClass.getLabel(), newClass.getUuid())); } updateDiagramObjectNameUseCase.updateDiagramObjectName( graphIdentifier, newClass.getUuid(), newClass.getLabel()); - - changeLogUseCase.recordChange( - graphIdentifier, - new ChangeLogEntry("Updated class " + newClass.getUuid(), graph.getLastDelta())); } @Override @@ -141,12 +105,9 @@ public UUID addClass( String className, ClassLayoutPositionDTO classLayoutPosition) { var cimPackage = packageMapper.toCIMObject(packageDTO); - GraphRewindableWithUUIDs graph = null; UUID newClassUUID; - try { - graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - graph.begin(TxnType.WRITE); - + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var graph = ctx.getRdfGraph(); var newClass = constructClass(cimPackage, classURIPrefix, className); assertNoPackageWithSameIri(graph, newClass); newClassUUID = @@ -154,20 +115,12 @@ public UUID addClass( graph, databasePort.getPrefixMapping(graphIdentifier.datasetName()), newClass); - graph.commit(); - } finally { - if (graph != null) { - graph.end(); - } + ctx.commit("Added class \"%s\" (%s)".formatted(newClass.getLabel(), newClassUUID)); } createClassLayoutDataUseCase.createClassLayoutData( graphIdentifier, packageDTO, className, newClassUUID, classLayoutPosition); - changeLogUseCase.recordChange( - graphIdentifier, - new ChangeLogEntry("Added class " + className, graph.getLastDelta())); - return newClassUUID; } @@ -187,7 +140,7 @@ private CIMClass constructClass( return cimClass.build(); } - private void assertNoPackageWithSameIri(GraphRewindableWithUUIDs graph, CIMClass newClass) { + private void assertNoPackageWithSameIri(Graph graph, CIMClass newClass) { var classUri = newClass.getUri().toNode(); if (graph.contains(classUri, RDF.type.asNode(), CIMS.classCategory.asNode())) { throw new ResourceConflictException( @@ -198,363 +151,18 @@ private void assertNoPackageWithSameIri(GraphRewindableWithUUIDs graph, CIMClass } @Override - public void deleteClass(GraphIdentifier graphIdentifier, String classUUID) { - GraphRewindableWithUUIDs graph = null; - try { - graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - graph.begin(TxnType.WRITE); + public void deleteClass(GraphIdentifier graphIdentifier, UUID classUUID) { + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var classResource = CIMResourceUtils.findResourceForUuid(ctx.getRdfGraph(), classUUID); + var classLabel = CIMResourceUtils.findLabelForResource(classResource); CIMUpdates.deleteClass( - graph, databasePort.getPrefixMapping(graphIdentifier.datasetName()), classUUID); - graph.commit(); - } finally { - if (graph != null) { - graph.end(); - } - } - - deleteClassLayoutDataUseCase.deleteClassLayoutData( - graphIdentifier, UUID.fromString(classUUID)); - removeFromDiagramUseCase.removeFromAllDiagrams(graphIdentifier, UUID.fromString(classUUID)); - - changeLogUseCase.recordChange( - graphIdentifier, - new ChangeLogEntry("Deleted class: " + classUUID, graph.getLastDelta())); - } - - @Override - public CopyClassResponseDTO copyClass( - GraphIdentifier graphIdentifier, - UUID classUUID, - GraphIdentifier targetGraphIdentifier, - CopyClassRequestDTO copyClassRequestDTO) { - - var cimClass = readSourceClass(graphIdentifier, classUUID.toString()); - - var label = - constructUniqueLabel( - cimClass.getLabel(), - getExistingClassLabels( - databasePort - .getGraphWithContext(targetGraphIdentifier) - .getRdfGraph(), - cimClass.getLabel())); - - var cimPackage = packageMapper.toCIMObject(copyClassRequestDTO.getTargetPackage()); - var newCimClass = - copyCimClass( - targetGraphIdentifier, - cimClass, - cimPackage, - label, - copyClassRequestDTO.isCopyAsAbstract(), - copyClassRequestDTO.isCopyAttributes(), - copyClassRequestDTO.isCopyAssociations()); - - var newClassUUID = insertClass(targetGraphIdentifier, newCimClass); - - changeLogUseCase.recordChange( - targetGraphIdentifier, - new ChangeLogEntry( - "Copied class " - + cimClass.getLabel().getValue() - + " from " - + (cimClass.getBelongsToCategory() != null - ? cimClass.getBelongsToCategory().getLabel().getValue() - : "default") - + " to " - + (copyClassRequestDTO.getTargetPackage() != null - ? copyClassRequestDTO.getTargetPackage().getLabel() - : "default") - + " as " - + label.getValue() - + (copyClassRequestDTO.isCopyAsAbstract() - ? " bare" - : !copyClassRequestDTO.isCopyAttributes() - ? " without attributes" - : !copyClassRequestDTO.isCopyAssociations() - ? " without associations" - : ""), - databasePort - .getGraphWithContext(targetGraphIdentifier) - .getRdfGraph() - .getLastDelta())); - return new CopyClassResponseDTO(newClassUUID.toString(), label.getValue()); - } - - private CIMClassUMLAdapted readSourceClass(GraphIdentifier graphIdentifier, String classUUID) { - GraphRewindableWithUUIDs graph = null; - CIMClassUMLAdapted cimClass; - try { - graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - graph.begin(TxnType.READ); - cimClass = - CIMUMLObjectFactory.createCIMClassUMLAdapted( - graph, - graphIdentifier.graphUri(), - databasePort.getPrefixMapping(graphIdentifier.datasetName()), - classUUID); - } finally { - if (graph != null) { - graph.end(); - } - } - return cimClass; - } - - private UUID insertClass( - GraphIdentifier targetGraphIdentifier, CIMClassUMLAdapted newCimClass) { - GraphRewindableWithUUIDs targetGraph = null; - UUID newClassUUID; - try { - targetGraph = databasePort.getGraphWithContext(targetGraphIdentifier).getRdfGraph(); - targetGraph.begin(TxnType.WRITE); - newClassUUID = - CIMUpdates.insertUMLAdaptedClass( - targetGraph, - databasePort.getPrefixMapping(targetGraphIdentifier.datasetName()), - newCimClass, - newValuesAsBlankNode); - targetGraph.commit(); - } finally { - if (targetGraph != null) { - targetGraph.end(); - } - } - return newClassUUID; - } - - private CIMClassUMLAdapted copyCimClass( - GraphIdentifier targetGraphIdentifier, - CIMClassUMLAdapted cimClass, - CIMPackage cimPackage, - RDFSLabel label, - boolean copyAsAbstract, - boolean copyAttributes, - boolean copyAssociations) { - var targetGraph = databasePort.getGraphWithContext(targetGraphIdentifier).getRdfGraph(); - var stereotypes = - copyAsAbstract - ? cimClass.getStereotypes().stream() - .filter( - s -> - !s.getStereotype() - .equals(CIMStereotypes.concreteString)) - .toList() - : cimClass.getStereotypes(); - var newCimClass = - CIMClassUMLAdapted.builder() - .uuid(UUID.randomUUID()) - .uri(new URI(cimClass.getUri().getPrefix() + label.getValue())) - .label(label) - .stereotypes(stereotypes); - if (!copyAsAbstract) { - newCimClass.superClass(cimClass.getSuperClass()); - } - if (cimClass.getComment() != null) { - newCimClass.comment( - new RDFSComment( - cimClass.getComment().getValue(), cimClass.getComment().getFormat())); - } - if (cimPackage != null) { - newCimClass.belongsToCategory( - new CIMSBelongsToCategory( - cimPackage.getUri(), cimPackage.getLabel(), cimPackage.getUuid())); - } - newCimClass.attributes( - copyAttributes(cimClass.getAttributes(), newCimClass.build(), copyAttributes)); - newCimClass.enumEntries( - copyEnumEntries(cimClass.getEnumEntries(), newCimClass.build(), copyAttributes)); - newCimClass.associationPairs( - copyAssociations( - cimClass.getAssociationPairs(), - cimClass, - newCimClass.build(), - targetGraph, - copyAssociations)); - - return newCimClass.build(); - } - - private List copyAttributes( - List attributes, CIMClass cimClass, boolean copyAttributes) { - if (!copyAttributes) { - return List.of(); - } - return attributes.stream() - .map( - attr -> { - var uri = - new URI( - cimClass.getUri().getPrefix() - + cimClass.getUri().getSuffix() - + "." - + attr.getLabel().getValue()); - var domain = - new RDFSDomain( - cimClass.getUri(), - new RDFSLabel(cimClass.getUri().getSuffix(), "en")); - return attr.toBuilder() - .uuid(UUID.randomUUID()) - .uri(uri) - .domain(domain) - .build(); - }) - .toList(); - } - - private List copyEnumEntries( - List enumEntries, CIMClass cimClass, boolean copyEnumEntries) { - if (!copyEnumEntries) { - return List.of(); - } - return enumEntries.stream() - .map( - entry -> { - var rdfType = - new RDFType( - cimClass.getUri(), - new RDFSLabel(cimClass.getUri().getSuffix(), "en")); - var uri = - new URI( - cimClass.getUri().getPrefix() - + cimClass.getUri().getSuffix() - + "." - + entry.getLabel().getValue()); - return entry.toBuilder() - .uuid(UUID.randomUUID()) - .type(rdfType) - .uri(uri) - .build(); - }) - .toList(); - } - - private List copyAssociations( - List pairs, - CIMClass sourceClass, - CIMClass newClass, - GraphRewindableWithUUIDs graph, - boolean copyAssociations) { - if (!copyAssociations) { - return List.of(); - } - return pairs.stream() - .filter(pair -> pair.getFrom().getDomain().getUri().equals(sourceClass.getUri())) - .map(pair -> copyAssociationPair(pair, newClass, graph)) - .filter(Objects::nonNull) - .toList(); - } - - private CIMAssociationPair copyAssociationPair( - CIMAssociationPair pair, CIMClass newClass, GraphRewindableWithUUIDs graph) { - var existingToLabels = - getExistingAssociationLabels( - graph, pair.getTo().getDomain().getUri(), pair.getTo().getLabel()); - var newToLabel = constructUniqueLabel(pair.getTo().getLabel(), existingToLabels); - var existingFromLabels = - getExistingAssociationLabels(graph, newClass.getUri(), pair.getFrom().getLabel()); - if (!existingFromLabels.isEmpty()) { - return null; - } - var newFromLabel = constructUniqueLabel(pair.getFrom().getLabel(), existingFromLabels); - - var from = buildFromAssociation(pair.getFrom(), newClass, newToLabel, newFromLabel); - var to = buildToAssociation(pair.getTo(), newClass, newToLabel, newFromLabel); - - return new CIMAssociationPair(from, to); - } - - private CIMAssociation buildFromAssociation( - CIMAssociation original, - CIMClass newClass, - RDFSLabel newToLabel, - RDFSLabel newFromLabel) { - return original.toBuilder() - .uuid(UUID.randomUUID()) - .label(newFromLabel) - .uri(new URI(newClass.getUri() + "." + newFromLabel.getValue())) - .domain( - new RDFSDomain( - newClass.getUri(), - new RDFSLabel(newClass.getUri().getSuffix(), "en"))) - .inverseRoleName( - new CIMSInverseRoleName( - original.getRange().getUri() + "." + newToLabel.getValue())) - .build(); - } - - private CIMAssociation buildToAssociation( - CIMAssociation original, - CIMClass newClass, - RDFSLabel newToLabel, - RDFSLabel newFromLabel) { - return original.toBuilder() - .uuid(UUID.randomUUID()) - .label(newToLabel) - .uri(new URI(original.getDomain().getUri() + "." + newToLabel.getValue())) - .range( - new RDFSRange( - newClass.getUri(), - new RDFSLabel(newClass.getUri().getSuffix(), "en"))) - .inverseRoleName( - new CIMSInverseRoleName(newClass.getUri() + "." + newFromLabel.getValue())) - .build(); - } - - private Set getExistingClassLabels(GraphRewindableWithUUIDs graph, RDFSLabel label) { - var baseValue = label.getValue(); - - final var LABEL_VAR = "?label"; - var exprFactory = new ExprFactory(); - var query = - new SelectBuilder() - .addVar(LABEL_VAR) - .addWhere("?s", RDF.type, RDFS.Class) - .addWhere("?s", RDFS.label, LABEL_VAR) - .addFilter(exprFactory.strstarts(exprFactory.str(LABEL_VAR), baseValue)) - .build(); - var resultSet = InMemorySparqlExecutor.executeSingleQuery(graph, query, null); - - var existingLabels = new HashSet(); - while (resultSet.hasNext()) { - var solution = resultSet.nextSolution(); - existingLabels.add(solution.getLiteral("label").getString()); - } - return existingLabels; - } - - private Set getExistingAssociationLabels( - GraphRewindableWithUUIDs graph, URI domainUri, RDFSLabel label) { - var exprFactory = new ExprFactory(); - var query = - new SelectBuilder() - .addVar("?label") - .addWhere("?s", RDFS.domain, NodeFactory.createURI(domainUri.toString())) - .addWhere("?s", RDFS.label, "?label") - .addFilter( - exprFactory.strstarts(exprFactory.str("?label"), label.getValue())) - .build(); - var resultSet = InMemorySparqlExecutor.executeSingleQuery(graph, query, null); - var existingLabels = new HashSet(); - while (resultSet.hasNext()) { - existingLabels.add(resultSet.nextSolution().getLiteral("label").getString()); + ctx.getRdfGraph(), + databasePort.getPrefixMapping(graphIdentifier.datasetName()), + classUUID); + ctx.commit("Deleted class \"%s\" (%s)".formatted(classLabel, classUUID)); } - return existingLabels; - } - private RDFSLabel constructUniqueLabel(RDFSLabel label, Set existingLabels) { - var baseValue = label.getValue(); - if (!existingLabels.contains(baseValue)) { - return new RDFSLabel(baseValue, label.getLang()); - } - baseValue = label.getValue() + "-Copy"; - if (!existingLabels.contains(baseValue)) { - return new RDFSLabel(baseValue, label.getLang()); - } - var counter = 1; - while (existingLabels.contains(baseValue + "(" + counter + ")")) { - counter++; - } - return new RDFSLabel(baseValue + "(" + counter + ")", label.getLang()); + deleteClassLayoutDataUseCase.deleteClassLayoutData(graphIdentifier, classUUID); + removeFromDiagramUseCase.removeFromAllDiagrams(graphIdentifier, classUUID); } } diff --git a/backend/src/main/java/org/rdfarchitect/services/update/classes/associations/AssociationsService.java b/backend/src/main/java/org/rdfarchitect/services/update/classes/associations/AssociationsService.java index 4b7c2100..33d695c8 100644 --- a/backend/src/main/java/org/rdfarchitect/services/update/classes/associations/AssociationsService.java +++ b/backend/src/main/java/org/rdfarchitect/services/update/classes/associations/AssociationsService.java @@ -19,15 +19,19 @@ import lombok.RequiredArgsConstructor; +import org.apache.jena.query.ReadWrite; +import org.apache.jena.update.UpdateAction; +import org.apache.jena.update.UpdateExecutionFactory; import org.apache.jena.update.UpdateRequest; import org.rdfarchitect.api.dto.association.AssociationPairDTO; import org.rdfarchitect.api.dto.association.AssociationPairMapper; import org.rdfarchitect.database.DatabasePort; +import org.rdfarchitect.database.GraphContext; import org.rdfarchitect.database.GraphIdentifier; -import org.rdfarchitect.database.inmemory.InMemorySparqlExecutor; -import org.rdfarchitect.models.changelog.ChangeLogEntry; +import org.rdfarchitect.database.inmemory.SessionDataStore; +import org.rdfarchitect.models.cim.data.dto.CIMAssociationPair; import org.rdfarchitect.models.cim.queries.update.CIMUpdates; -import org.rdfarchitect.services.ChangeLogUseCase; +import org.rdfarchitect.models.cim.relations.model.CIMResourceUtils; import org.springframework.stereotype.Service; import java.util.List; @@ -39,7 +43,6 @@ public class AssociationsService implements CreateAssociationUseCase, UpdateAsso private final DatabasePort databasePort; private final AssociationPairMapper associationPairMapper; - private final ChangeLogUseCase changeLogUseCase; public record AssociationUUIDs(UUID fromUUID, UUID toUUID) {} @@ -61,14 +64,11 @@ public AssociationUUIDs createAssociation( graphIdentifier.graphUri(), cimAssociationPair); - var graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - InMemorySparqlExecutor.executeSingleUpdate( - graph, new UpdateRequest().add(update.build()), graphIdentifier.graphUri()); - changeLogUseCase.recordChange( - graphIdentifier, - new ChangeLogEntry( - "Created association from " + from.getUuid() + " to " + to.getUuid(), - graph.getLastDelta())); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + UpdateAction.execute(new UpdateRequest().add(update.build()), ctx.getRdfGraph()); + ctx.commit( + buildAssociationMessage("Created", ctx, associationPair, cimAssociationPair)); + } return new AssociationUUIDs(from.getUuid(), to.getUuid()); } @@ -82,23 +82,23 @@ public AssociationUUIDs replaceAssociation( graphIdentifier.graphUri(), cimAssociationPair); - var fromUUID = cimAssociationPair.getFrom().getUuid(); - var toUUID = cimAssociationPair.getTo().getUuid(); - var graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - InMemorySparqlExecutor.executeSingleUpdate( - graph, new UpdateRequest().add(update.build()), graphIdentifier.graphUri()); - changeLogUseCase.recordChange( - graphIdentifier, - new ChangeLogEntry( - "Replaced association from " + fromUUID + " to " + toUUID, - graph.getLastDelta())); - return new AssociationUUIDs(fromUUID, toUUID); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + UpdateExecutionFactory.create( + update.buildRequest(), + SessionDataStore.wrapGraphInDataset( + ctx.getRdfGraph(), graphIdentifier.graphUri())) + .execute(); + ctx.commit( + buildAssociationMessage("Replaced", ctx, associationPair, cimAssociationPair)); + } + return new AssociationUUIDs( + cimAssociationPair.getFrom().getUuid(), cimAssociationPair.getTo().getUuid()); } @Override public void replaceAllAssociations( GraphIdentifier graphIdentifier, - String classUUID, + UUID classUUID, List associationPairList) { var cimAssociationPairs = associationPairMapper.toCIMObjectList(associationPairList); var update = @@ -107,12 +107,42 @@ public void replaceAllAssociations( graphIdentifier.graphUri(), classUUID, cimAssociationPairs); - var graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - InMemorySparqlExecutor.executeSingleUpdate( - graph, new UpdateRequest().add(update.build()), graphIdentifier.graphUri()); - changeLogUseCase.recordChange( - graphIdentifier, - new ChangeLogEntry( - "Replaced all associations for class " + classUUID, graph.getLastDelta())); + + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var classResource = CIMResourceUtils.findResourceForUuid(ctx.getRdfGraph(), classUUID); + var classLabel = CIMResourceUtils.findLabelForResource(classResource); + + UpdateAction.execute(new UpdateRequest().add(update.build()), ctx.getRdfGraph()); + + ctx.commit( + "Replaced all associations for class \"%s\" (%s)" + .formatted(classLabel, classUUID)); + } + } + + private String buildAssociationMessage( + String action, + GraphContext session, + AssociationPairDTO dto, + CIMAssociationPair cimPair) { + var from = cimPair.getFrom(); + var to = cimPair.getTo(); + var fromClassLabel = + CIMResourceUtils.findLabelForResource( + CIMResourceUtils.findResourceForUri( + session.getRdfGraph(), dto.getFrom().getDomain())); + var toClassLabel = + CIMResourceUtils.findLabelForResource( + CIMResourceUtils.findResourceForUri( + session.getRdfGraph(), dto.getTo().getDomain())); + return "%s association \"%s.%s\" (%s) → \"%s.%s\" (%s)" + .formatted( + action, + fromClassLabel, + from.getLabel().getValue(), + from.getUuid(), + toClassLabel, + to.getLabel().getValue(), + to.getUuid()); } } diff --git a/backend/src/main/java/org/rdfarchitect/services/update/classes/associations/UpdateAssociationsUseCase.java b/backend/src/main/java/org/rdfarchitect/services/update/classes/associations/UpdateAssociationsUseCase.java index 0a8771e4..82c58cbd 100644 --- a/backend/src/main/java/org/rdfarchitect/services/update/classes/associations/UpdateAssociationsUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/update/classes/associations/UpdateAssociationsUseCase.java @@ -21,12 +21,13 @@ import org.rdfarchitect.database.GraphIdentifier; import java.util.List; +import java.util.UUID; public interface UpdateAssociationsUseCase { void replaceAllAssociations( GraphIdentifier graphIdentifier, - String classUUID, + UUID classUUID, List associationPairList); AssociationsService.AssociationUUIDs replaceAssociation( diff --git a/backend/src/main/java/org/rdfarchitect/services/update/classes/attributes/AttributesService.java b/backend/src/main/java/org/rdfarchitect/services/update/classes/attributes/AttributesService.java index 1180c551..44676f71 100644 --- a/backend/src/main/java/org/rdfarchitect/services/update/classes/attributes/AttributesService.java +++ b/backend/src/main/java/org/rdfarchitect/services/update/classes/attributes/AttributesService.java @@ -17,7 +17,8 @@ package org.rdfarchitect.services.update.classes.attributes; -import org.apache.jena.query.TxnType; +import org.apache.jena.graph.Graph; +import org.apache.jena.query.ReadWrite; import org.apache.jena.update.UpdateExecutionFactory; import org.apache.jena.update.UpdateRequest; import org.rdfarchitect.api.dto.attributes.AttributeDTO; @@ -25,9 +26,8 @@ import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.database.inmemory.SessionDataStore; -import org.rdfarchitect.models.changelog.ChangeLogEntry; import org.rdfarchitect.models.cim.queries.update.CIMUpdates; -import org.rdfarchitect.services.ChangeLogUseCase; +import org.rdfarchitect.models.cim.relations.model.CIMResourceUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -39,17 +39,14 @@ public class AttributesService implements CreateAttributeUseCase, UpdateAttribut private final DatabasePort databasePort; private final AttributeMapper attributeMapper; - private final ChangeLogUseCase changeLogUseCase; private final boolean newValuesAsBlankNode; public AttributesService( DatabasePort databasePort, AttributeMapper attributeMapper, - ChangeLogUseCase changeLogUseCase, @Value("${attributes.newValuesBlankNode:false}") boolean newValuesAsBlankNode) { this.databasePort = databasePort; this.attributeMapper = attributeMapper; - this.changeLogUseCase = changeLogUseCase; this.newValuesAsBlankNode = newValuesAsBlankNode; } @@ -61,9 +58,8 @@ public UUID createAttribute(GraphIdentifier graphIdentifier, AttributeDTO attrib } var prefixMapping = databasePort.getPrefixMapping(graphIdentifier.datasetName()); var graphUri = graphIdentifier.graphUri(); - var graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - try { - graph.begin(TxnType.WRITE); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var graph = ctx.getRdfGraph(); var update = new UpdateRequest() .add( @@ -77,14 +73,12 @@ public UUID createAttribute(GraphIdentifier graphIdentifier, AttributeDTO attrib UpdateExecutionFactory.create( update, SessionDataStore.wrapGraphInDataset(graph, graphUri)) .execute(); - graph.commit(); - } finally { - graph.end(); + var classLabel = findClassLabel(graph, attributeDTO.getDomain()); + ctx.commit( + "Created attribute \"%s.%s\" (%s)" + .formatted( + classLabel, cimAttribute.getLabel(), cimAttribute.getUuid())); } - changeLogUseCase.recordChange( - graphIdentifier, - new ChangeLogEntry( - "Created attribute " + cimAttribute.getUuid(), graph.getLastDelta())); return cimAttribute.getUuid(); } @@ -93,35 +87,33 @@ public UUID replaceAttribute(GraphIdentifier graphIdentifier, AttributeDTO attri var cimAttribute = attributeMapper.toCIMObject(attributeDTO); var prefixMapping = databasePort.getPrefixMapping(graphIdentifier.datasetName()); var graphUri = graphIdentifier.graphUri(); - var graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - try { - graph.begin(TxnType.WRITE); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var graph = ctx.getRdfGraph(); var update = CIMUpdates.replaceAttribute( graph, prefixMapping, graphUri, cimAttribute, newValuesAsBlankNode); UpdateExecutionFactory.create( update, SessionDataStore.wrapGraphInDataset(graph, graphUri)) .execute(); - graph.commit(); - } finally { - graph.end(); + var classLabel = findClassLabel(graph, attributeDTO.getDomain()); + ctx.commit( + "Replaced attribute \"%s.%s\" (%s)" + .formatted( + classLabel, + cimAttribute.getLabel().getValue(), + cimAttribute.getUuid())); } - changeLogUseCase.recordChange( - graphIdentifier, - new ChangeLogEntry( - "Replaced attribute " + cimAttribute.getUuid(), graph.getLastDelta())); return cimAttribute.getUuid(); } @Override public void replaceAllAttributes( - GraphIdentifier graphIdentifier, String classUUID, List attributeList) { + GraphIdentifier graphIdentifier, UUID classUUID, List attributeList) { var attributeCIMObjects = attributeMapper.toCIMObjectList(attributeList); var prefixMapping = databasePort.getPrefixMapping(graphIdentifier.datasetName()); var graphUri = graphIdentifier.graphUri(); - var graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - try { - graph.begin(TxnType.WRITE); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var graph = ctx.getRdfGraph(); var update = CIMUpdates.replaceAttributes( graph, @@ -133,14 +125,16 @@ public void replaceAllAttributes( UpdateExecutionFactory.create( update, SessionDataStore.wrapGraphInDataset(graph, graphUri)) .execute(); - graph.commit(); - } finally { - graph.end(); + var classResource = CIMResourceUtils.findResourceForUuid(graph, classUUID); + var classLabel = CIMResourceUtils.findLabelForResource(classResource); + ctx.commit( + "Replaced all attributes for class \"%s\" (%s)" + .formatted(classLabel, classUUID)); } - changeLogUseCase.recordChange( - graphIdentifier, - new ChangeLogEntry( - "All attributes for class " + classUUID + " replaced", - graph.getLastDelta())); + } + + private String findClassLabel(Graph graph, String domainUri) { + var classResource = CIMResourceUtils.findResourceForUri(graph, domainUri); + return CIMResourceUtils.findLabelForResource(classResource); } } diff --git a/backend/src/main/java/org/rdfarchitect/services/update/classes/attributes/UpdateAttributesUseCase.java b/backend/src/main/java/org/rdfarchitect/services/update/classes/attributes/UpdateAttributesUseCase.java index 3b84fba2..0b58077a 100644 --- a/backend/src/main/java/org/rdfarchitect/services/update/classes/attributes/UpdateAttributesUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/update/classes/attributes/UpdateAttributesUseCase.java @@ -26,7 +26,7 @@ public interface UpdateAttributesUseCase { void replaceAllAttributes( - GraphIdentifier graphIdentifier, String classURI, List attributeList); + GraphIdentifier graphIdentifier, UUID classURI, List attributeList); UUID replaceAttribute(GraphIdentifier graphIdentifier, AttributeDTO attribute); } diff --git a/backend/src/main/java/org/rdfarchitect/services/update/classes/enumentries/UpdateEnumEntriesService.java b/backend/src/main/java/org/rdfarchitect/services/update/classes/enumentries/UpdateEnumEntriesService.java index c5d53e61..e73b5ed7 100644 --- a/backend/src/main/java/org/rdfarchitect/services/update/classes/enumentries/UpdateEnumEntriesService.java +++ b/backend/src/main/java/org/rdfarchitect/services/update/classes/enumentries/UpdateEnumEntriesService.java @@ -19,15 +19,12 @@ import lombok.RequiredArgsConstructor; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.rdfarchitect.api.dto.enumentries.EnumEntryDTO; import org.rdfarchitect.api.dto.enumentries.EnumEntryMapper; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; -import org.rdfarchitect.models.changelog.ChangeLogEntry; import org.rdfarchitect.models.cim.queries.update.CIMUpdates; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindable; -import org.rdfarchitect.services.ChangeLogUseCase; import org.springframework.stereotype.Service; import java.util.UUID; @@ -38,19 +35,15 @@ public class UpdateEnumEntriesService implements ReplaceOrCreateEnumEntryUseCase private final DatabasePort databasePort; private final EnumEntryMapper mapper; - private final ChangeLogUseCase changeLogUseCase; @Override public UUID replaceOrCreateEnumEntry( GraphIdentifier graphIdentifier, EnumEntryDTO enumEntryDTO) { - GraphRewindable graph = null; String message; UUID uuid; - try { - graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - graph.begin(TxnType.WRITE); - + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var graph = ctx.getRdfGraph(); if (enumEntryDTO.getLabel() == null || enumEntryDTO.getLabel().trim().isEmpty()) { throw new IllegalArgumentException("New enum entry label cannot be null or empty"); } @@ -63,30 +56,20 @@ public UUID replaceOrCreateEnumEntry( graph, databasePort.getPrefixMapping(graphIdentifier.datasetName()), cimEnumEntry); - message = "Enum entry " + uuid + " created"; + message = "Created enum entry \"%s\" (%s)".formatted(cimEnumEntry.getLabel(), uuid); } else { uuid = enumEntryDTO.getUuid(); CIMUpdates.replaceEnumEntry( graph, databasePort.getPrefixMapping(graphIdentifier.datasetName()), cimEnumEntry); - message = "Enum entry " + uuid + " replaced"; + message = + "Replaced enum entry \"%s\" (%s)".formatted(cimEnumEntry.getLabel(), uuid); } - graph.commit(); - } catch (Exception e) { - if (graph != null) { - graph.abort(); - } - throw e; - } finally { - if (graph != null) { - graph.end(); - } + ctx.commit(message); } - changeLogUseCase.recordChange( - graphIdentifier, new ChangeLogEntry(message, graph.getLastDelta())); return uuid; } } diff --git a/backend/src/main/java/org/rdfarchitect/services/update/graph/ImportGraphsService.java b/backend/src/main/java/org/rdfarchitect/services/update/graph/ImportGraphsService.java index b0154cd9..46c66f21 100644 --- a/backend/src/main/java/org/rdfarchitect/services/update/graph/ImportGraphsService.java +++ b/backend/src/main/java/org/rdfarchitect/services/update/graph/ImportGraphsService.java @@ -26,10 +26,8 @@ import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.exception.database.DataAccessException; -import org.rdfarchitect.models.changelog.ChangeLogEntry; import org.rdfarchitect.models.cim.rdf.resources.RDFA; import org.rdfarchitect.rdf.graph.source.builder.implementations.GraphFileSourceBuilderImpl; -import org.rdfarchitect.services.ChangeLogUseCase; import org.rdfarchitect.services.dl.update.packagelayout.CreateDiagramLayoutUseCase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -65,7 +63,6 @@ public class ImportGraphsService implements ImportGraphsUseCase { private static final int MAX_ENTRIES = 1000; private static final String FALL_BACK_NAME = "graph"; - private final ChangeLogUseCase changeLogUseCase; private final CreateDiagramLayoutUseCase createDiagramLayoutUseCase; private final DatabasePort databasePort; @@ -108,7 +105,7 @@ private void importSingleFile( graphUri = ensureUniqueGraphUri(graphUri, reservedGraphUris); var graphIdentifier = replaceGraph(datasetName, graphUri, file); result.importedGraphUris().add(graphUri); - recordChange(graphIdentifier, datasetName); + createDiagramLayoutUseCase.createDiagramLayout(graphIdentifier); } catch (RuntimeException _) { result.failedFileNames().add(file.getOriginalFilename()); } @@ -122,22 +119,6 @@ private GraphIdentifier replaceGraph(String datasetName, String graphUri, Multip return graphIdentifier; } - private void recordChange(GraphIdentifier graphIdentifier, String datasetName) { - createDiagramLayoutUseCase.createDiagramLayout(graphIdentifier); - changeLogUseCase.recordChange( - graphIdentifier, - new ChangeLogEntry( - "Imported graph into dataset '" - + datasetName - + "' with graph URI '" - + graphIdentifier.graphUri() - + "'.", - databasePort - .getGraphWithContext(graphIdentifier) - .getRdfGraph() - .getLastDelta())); - } - private void importZipFile( ImportResult result, String datasetName, diff --git a/backend/src/main/java/org/rdfarchitect/services/update/ontology/UpdateOntologyService.java b/backend/src/main/java/org/rdfarchitect/services/update/ontology/UpdateOntologyService.java index 02000fda..9e922c9c 100644 --- a/backend/src/main/java/org/rdfarchitect/services/update/ontology/UpdateOntologyService.java +++ b/backend/src/main/java/org/rdfarchitect/services/update/ontology/UpdateOntologyService.java @@ -19,7 +19,7 @@ import lombok.RequiredArgsConstructor; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.shared.impl.PrefixMappingImpl; import org.apache.jena.vocabulary.DCAT; @@ -28,10 +28,7 @@ import org.rdfarchitect.api.dto.ontology.OntologyDTO; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; -import org.rdfarchitect.models.changelog.ChangeLogEntry; import org.rdfarchitect.models.cim.ontology.OntologyFacade; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; -import org.rdfarchitect.services.ChangeLogUseCase; import org.rdfarchitect.services.ExpandURIUseCase; import org.springframework.stereotype.Service; @@ -44,17 +41,13 @@ public class UpdateOntologyService private final DatabasePort databasePort; private final ExpandURIUseCase expandURIUseCase; - private final ChangeLogUseCase changeLogUseCase; // CREATE @Override public void createOntology(GraphIdentifier graphIdentifier, OntologyDTO ontologyDTO) { expandOntologyIris(graphIdentifier.datasetName(), ontologyDTO); - GraphRewindableWithUUIDs graph = null; - try { - graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - graph.begin(TxnType.WRITE); - var model = ModelFactory.createModelForGraph(graph); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var model = ModelFactory.createModelForGraph(ctx.getRdfGraph()); model.setNsPrefixes(databasePort.getPrefixMapping(graphIdentifier.datasetName())); if (ontologyDTO.getUuid() == null) { @@ -65,8 +58,7 @@ public void createOntology(GraphIdentifier graphIdentifier, OntologyDTO ontology "Invalid UUID for ontology: " + ontologyDTO.getUuid()); } } - var ontology = new OntologyFacade(model); - ontology.createOntology(ontologyDTO); + new OntologyFacade(model).createOntology(ontologyDTO); var pm = new PrefixMappingImpl(); pm.setNsPrefixes(databasePort.getPrefixMapping(graphIdentifier.datasetName())); if (pm.getNsURIPrefix(DCAT.getURI()) == null) { @@ -80,14 +72,7 @@ public void createOntology(GraphIdentifier graphIdentifier, OntologyDTO ontology } databasePort.setPrefixMapping(graphIdentifier.datasetName(), pm); - graph.commit(); - } finally { - if (graph != null) { - graph.end(); - changeLogUseCase.recordChange( - graphIdentifier, - new ChangeLogEntry("Created Ontology", graph.getLastDelta())); - } + ctx.commit("Created Ontology"); } } @@ -95,11 +80,8 @@ public void createOntology(GraphIdentifier graphIdentifier, OntologyDTO ontology @Override public void replaceOntology(GraphIdentifier graphIdentifier, OntologyDTO ontologyDTO) { expandOntologyIris(graphIdentifier.datasetName(), ontologyDTO); - GraphRewindableWithUUIDs graph = null; - try { - graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - graph.begin(TxnType.WRITE); - var model = ModelFactory.createModelForGraph(graph); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var model = ModelFactory.createModelForGraph(ctx.getRdfGraph()); model.setNsPrefixes(databasePort.getPrefixMapping(graphIdentifier.datasetName())); if (ontologyDTO.getUuid() == null) { @@ -110,39 +92,19 @@ public void replaceOntology(GraphIdentifier graphIdentifier, OntologyDTO ontolog "Invalid UUID for ontology: " + ontologyDTO.getUuid()); } } - var ontology = new OntologyFacade(model); - ontology.replaceOntology(ontologyDTO); - graph.commit(); - } finally { - if (graph != null) { - graph.end(); - changeLogUseCase.recordChange( - graphIdentifier, - new ChangeLogEntry("Replaced Ontology", graph.getLastDelta())); - } + new OntologyFacade(model).replaceOntology(ontologyDTO); + ctx.commit("Replaced Ontology"); } } // DELETE @Override public void deleteOntology(GraphIdentifier graphIdentifier) { - GraphRewindableWithUUIDs graph = null; - try { - graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - graph.begin(TxnType.WRITE); - var model = ModelFactory.createModelForGraph(graph); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var model = ModelFactory.createModelForGraph(ctx.getRdfGraph()); model.setNsPrefixes(databasePort.getPrefixMapping(graphIdentifier.datasetName())); - - var ontology = new OntologyFacade(model); - ontology.deleteOntology(); - graph.commit(); - } finally { - if (graph != null) { - graph.end(); - changeLogUseCase.recordChange( - graphIdentifier, - new ChangeLogEntry("Deleted Ontology", graph.getLastDelta())); - } + new OntologyFacade(model).deleteOntology(); + ctx.commit("Deleted Ontology"); } } diff --git a/backend/src/main/java/org/rdfarchitect/services/update/packages/UpdatePackageService.java b/backend/src/main/java/org/rdfarchitect/services/update/packages/UpdatePackageService.java index c0296860..025a699f 100644 --- a/backend/src/main/java/org/rdfarchitect/services/update/packages/UpdatePackageService.java +++ b/backend/src/main/java/org/rdfarchitect/services/update/packages/UpdatePackageService.java @@ -21,7 +21,8 @@ import lombok.RequiredArgsConstructor; -import org.apache.jena.query.TxnType; +import org.apache.jena.graph.Graph; +import org.apache.jena.query.ReadWrite; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; import org.rdfarchitect.api.dto.packages.PackageDTO; @@ -31,7 +32,6 @@ import org.rdfarchitect.database.inmemory.InMemorySparqlExecutor; import org.rdfarchitect.exception.database.DataAccessException; import org.rdfarchitect.exception.database.ResourceConflictException; -import org.rdfarchitect.models.changelog.ChangeLogEntry; import org.rdfarchitect.models.cim.data.CIMObjectFactory; import org.rdfarchitect.models.cim.data.dto.CIMPackage; import org.rdfarchitect.models.cim.queries.select.CIMBaseQueryBuilder; @@ -39,8 +39,6 @@ import org.rdfarchitect.models.cim.queries.update.CIMUpdates; import org.rdfarchitect.models.cim.rdf.resources.CIMS; import org.rdfarchitect.models.cim.rdf.resources.RDFA; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; -import org.rdfarchitect.services.ChangeLogUseCase; import org.rdfarchitect.services.dl.update.ReplaceDiagramUseCase; import org.rdfarchitect.services.dl.update.packagelayout.CreatePackageLayoutDataUseCase; import org.rdfarchitect.services.dl.update.packagelayout.DeletePackageLayoutDataUseCase; @@ -58,7 +56,6 @@ public class UpdatePackageService private final DatabasePort databasePort; private final PackageMapper packageMapper; - private final ChangeLogUseCase changeLogUseCase; private final CreatePackageLayoutDataUseCase createPackageLayoutData; private final ReplaceDiagramUseCase replaceDiagramUseCase; @@ -66,87 +63,56 @@ public class UpdatePackageService @Override public UUID addPackage(GraphIdentifier graphIdentifier, PackageDTO packageDTO) { - GraphRewindableWithUUIDs graph = null; UUID newPackageUUID = UUID.randomUUID(); packageDTO.setUuid(newPackageUUID); - try { - graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - graph.begin(TxnType.WRITE); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var graph = ctx.getRdfGraph(); var newPackage = packageMapper.toCIMObject(packageDTO); assertNoClassWithSameIri(graph, newPackage); CIMUpdates.insertPackage( graph, databasePort.getPrefixMapping(graphIdentifier.datasetName()), newPackage); - graph.commit(); - } finally { - if (graph != null) { - graph.end(); - } + ctx.commit("Added package " + packageDTO.getLabel()); } createPackageLayoutData.createPackageLayoutData( graphIdentifier, packageDTO, newPackageUUID); - changeLogUseCase.recordChange( - graphIdentifier, - new ChangeLogEntry("Added package " + packageDTO.getLabel(), graph.getLastDelta())); return newPackageUUID; } @Override public void replacePackage(GraphIdentifier graphIdentifier, PackageDTO packageDTO) { - GraphRewindableWithUUIDs graph = null; - try { - graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - graph.begin(TxnType.WRITE); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { + var graph = ctx.getRdfGraph(); var newPackage = packageMapper.toCIMObject(packageDTO); assertNoClassWithSameIri(graph, newPackage); CIMUpdates.replacePackage( graph, databasePort.getPrefixMapping(graphIdentifier.datasetName()), newPackage); - graph.commit(); - } finally { - if (graph != null) { - graph.end(); - } + ctx.commit("Replaced package " + packageDTO.getUuid()); } replaceDiagramUseCase.replaceDiagram( graphIdentifier, packageDTO.getUuid(), packageDTO.getLabel()); - - changeLogUseCase.recordChange( - graphIdentifier, - new ChangeLogEntry( - "Replaced package " + packageDTO.getUuid(), graph.getLastDelta())); } @Override public void deletePackage(GraphIdentifier graphIdentifier, UUID packageUUID) { - GraphRewindableWithUUIDs graph = null; - try { - graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - graph.begin(TxnType.WRITE); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { CIMUpdates.deletePackage( - graph, + ctx.getRdfGraph(), databasePort.getPrefixMapping(graphIdentifier.datasetName()), - packageUUID.toString()); - graph.commit(); - } finally { - if (graph != null) { - graph.end(); - } + packageUUID); + ctx.commit("Deleted package " + packageUUID); } deletePackageLayoutDataUseCase.deletePackageLayoutData(graphIdentifier, packageUUID); - - changeLogUseCase.recordChange( - graphIdentifier, - new ChangeLogEntry("Deleted package " + packageUUID, graph.getLastDelta())); } - private void assertNoClassWithSameIri(GraphRewindableWithUUIDs graph, CIMPackage newPackage) { + private void assertNoClassWithSameIri(Graph graph, CIMPackage newPackage) { var packageUri = newPackage.getUri().toNode(); if (graph.contains(packageUri, RDF.type.asNode(), RDFS.Class.asNode())) { throw new ResourceConflictException( @@ -172,10 +138,9 @@ public PackageDTO getPackage(GraphIdentifier graphIdentifier, UUID packageUUID) .appendUUIDQuery(REQUIRED) .appendLabelQuery(REQUIRED) .build(); - var resultSet = InMemorySparqlExecutor.executeSingleQuery( - databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), + databasePort.getGraphWithContext(graphIdentifier), query, graphIdentifier.graphUri()); diff --git a/backend/src/main/java/org/rdfarchitect/services/versioncontrol/RedoUseCase.java b/backend/src/main/java/org/rdfarchitect/services/versioncontrol/RedoUseCase.java index adbf5e9e..e0673d05 100644 --- a/backend/src/main/java/org/rdfarchitect/services/versioncontrol/RedoUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/versioncontrol/RedoUseCase.java @@ -18,8 +18,16 @@ package org.rdfarchitect.services.versioncontrol; import org.rdfarchitect.database.GraphIdentifier; +import org.rdfarchitect.models.changelog.ChangeLogEntry; public interface RedoUseCase { - void redo(GraphIdentifier graphIdentifier); + /** + * Reapplies the most recently undone named commit for the given graph, restoring all associated + * graph participants to the state before the undo. + * + * @param graphIdentifier the identifier of the graph to operate on + * @return the changelog entry that was redone, or {@code null} if no redo was available + */ + ChangeLogEntry redo(GraphIdentifier graphIdentifier); } diff --git a/backend/src/main/java/org/rdfarchitect/services/versioncontrol/UndoUseCase.java b/backend/src/main/java/org/rdfarchitect/services/versioncontrol/UndoUseCase.java index 2431d1d2..f4c9215e 100644 --- a/backend/src/main/java/org/rdfarchitect/services/versioncontrol/UndoUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/versioncontrol/UndoUseCase.java @@ -18,8 +18,16 @@ package org.rdfarchitect.services.versioncontrol; import org.rdfarchitect.database.GraphIdentifier; +import org.rdfarchitect.models.changelog.ChangeLogEntry; public interface UndoUseCase { - void undo(GraphIdentifier graphIdentifier); + /** + * Undoes the most recent named commit for the given graph, reverting all associated graph + * participants to their previous state. + * + * @param graphIdentifier the identifier of the graph to operate on + * @return the changelog entry that was undone, or {@code null} if no undo was available + */ + ChangeLogEntry undo(GraphIdentifier graphIdentifier); } diff --git a/backend/src/main/java/org/rdfarchitect/services/versioncontrol/VersionControlService.java b/backend/src/main/java/org/rdfarchitect/services/versioncontrol/VersionControlService.java index c6a34182..bd475423 100644 --- a/backend/src/main/java/org/rdfarchitect/services/versioncontrol/VersionControlService.java +++ b/backend/src/main/java/org/rdfarchitect/services/versioncontrol/VersionControlService.java @@ -19,10 +19,11 @@ import lombok.RequiredArgsConstructor; +import org.apache.jena.query.ReadWrite; import org.rdfarchitect.database.DatabaseConnection; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; -import org.rdfarchitect.services.ChangeLogUseCase; +import org.rdfarchitect.models.changelog.ChangeLogEntry; import org.springframework.stereotype.Service; import java.util.UUID; @@ -39,28 +40,29 @@ public class VersionControlService private final DatabasePort databasePort; private final DatabaseConnection databaseConnection; - private final ChangeLogUseCase changelogUseCase; @Override public Boolean canRedo(GraphIdentifier graphIdentifier) { - return databasePort.canRedo(graphIdentifier); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + return ctx.canRedo(); + } } @Override public Boolean canUndo(GraphIdentifier graphIdentifier) { - return databasePort.canUndo(graphIdentifier); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + return ctx.canUndo(); + } } @Override - public void redo(GraphIdentifier graphIdentifier) { - databasePort.redo(graphIdentifier); - changelogUseCase.redoChange(graphIdentifier); + public ChangeLogEntry redo(GraphIdentifier graphIdentifier) { + return databasePort.getGraphWithContext(graphIdentifier).redo(); } @Override - public void undo(GraphIdentifier graphIdentifier) { - databasePort.undo(graphIdentifier); - changelogUseCase.undoChange(graphIdentifier); + public ChangeLogEntry undo(GraphIdentifier graphIdentifier) { + return databasePort.getGraphWithContext(graphIdentifier).undo(); } @Override @@ -70,7 +72,6 @@ public void persist(GraphIdentifier graphIdentifier) { @Override public void restoreVersion(GraphIdentifier graphIdentifier, UUID versionId) { - databasePort.restore(graphIdentifier, versionId); - changelogUseCase.restoreVersion(graphIdentifier, versionId); + databasePort.getGraphWithContext(graphIdentifier).restoreToVersion(versionId); } } diff --git a/backend/src/test/java/org/rdfarchitect/api/dto/ChangeLogEntryMapperTest.java b/backend/src/test/java/org/rdfarchitect/api/dto/ChangeLogEntryMapperTest.java index 52dca61e..24c01ca0 100644 --- a/backend/src/test/java/org/rdfarchitect/api/dto/ChangeLogEntryMapperTest.java +++ b/backend/src/test/java/org/rdfarchitect/api/dto/ChangeLogEntryMapperTest.java @@ -20,7 +20,6 @@ import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*; -import org.apache.jena.graph.Graph; import org.apache.jena.graph.NodeFactory; import org.apache.jena.graph.Triple; import org.apache.jena.sparql.graph.GraphFactory; @@ -28,9 +27,12 @@ import org.junit.jupiter.api.Test; import org.mapstruct.factory.Mappers; import org.rdfarchitect.models.changelog.ChangeLogEntry; +import org.rdfarchitect.models.changelog.ContextDelta; import org.rdfarchitect.rdf.graph.DeltaCompressible; +import java.lang.ref.WeakReference; import java.time.LocalDateTime; +import java.util.List; import java.util.UUID; class ChangeLogEntryMapperTest { @@ -51,7 +53,7 @@ class ChangeLogEntryMapperTest { @BeforeAll static void beforeAll() { - Graph baseGraph = GraphFactory.createDefaultGraph(); + var baseGraph = GraphFactory.createDefaultGraph(); baseGraph.add( Triple.create( NodeFactory.createURI(SUB), @@ -62,42 +64,48 @@ static void beforeAll() { NodeFactory.createURI(SUB), NodeFactory.createURI(PRED), NodeFactory.createURI(DELETED))); - DeltaCompressible deltaCompressible = new DeltaCompressible(baseGraph); - deltaCompressible.add( + var delta = new DeltaCompressible(baseGraph); + delta.add( Triple.create( NodeFactory.createURI(SUB), NodeFactory.createURI(PRED), NodeFactory.createURI(ADDED))); - deltaCompressible.delete( + delta.delete( Triple.create( NodeFactory.createURI(SUB), NodeFactory.createURI(PRED), NodeFactory.createURI(DELETED))); - changeLogEntry = new ChangeLogEntry(MESSAGE, deltaCompressible); + + var contextDeltas = + List.of( + new ContextDelta( + "rdf", + new WeakReference<>(delta.getAdditions()), + new WeakReference<>(delta.getDeletions()))); + changeLogEntry = new ChangeLogEntry(MESSAGE, 1, contextDeltas); changeLogEntry.setChangeId(CHANGE_ID); changeLogEntry.setTimestamp(TIMESTAMP); } @Test void toDTO_changeLogEntry() { - // Arrange - - // Act var dto = changeLogEntryMapper.toDTO(changeLogEntry); - // Assert - var addition = assertDoesNotThrow(() -> dto.getAdditions().get(0)); - var deletion = assertDoesNotThrow(() -> dto.getDeletions().get(0)); + assertThat(dto.getContextDeltas()).hasSize(1); + var contextDelta = dto.getContextDeltas().getFirst(); + var addition = assertDoesNotThrow(() -> contextDelta.getAdditions().getFirst()); + var deletion = assertDoesNotThrow(() -> contextDelta.getDeletions().getFirst()); assertAll( () -> assertThat(dto.getChangeId()).isEqualTo(CHANGE_ID.toString()), () -> assertThat(LocalDateTime.parse(dto.getTimestamp())).isEqualTo(TIMESTAMP), () -> assertThat(dto.getMessage()).isEqualTo(MESSAGE), - () -> assertThat(dto.getAdditions()).hasSize(1), + () -> assertThat(contextDelta.getContextName()).isEqualTo("rdf"), + () -> assertThat(contextDelta.getAdditions()).hasSize(1), () -> assertThat(addition.getSubject()).isEqualTo(SUB), () -> assertThat(addition.getPredicate()).isEqualTo(PRED), () -> assertThat(addition.getObject()).isEqualTo(ADDED), - () -> assertThat(dto.getDeletions()).hasSize(1), + () -> assertThat(contextDelta.getDeletions()).hasSize(1), () -> assertThat(deletion.getSubject()).isEqualTo(SUB), () -> assertThat(deletion.getPredicate()).isEqualTo(PRED), () -> assertThat(deletion.getObject()).isEqualTo(DELETED)); diff --git a/backend/src/test/java/org/rdfarchitect/cim/data/CIMCollectionConverter/DiagramToCIMCollectionConverterServiceTest.java b/backend/src/test/java/org/rdfarchitect/cim/data/CIMCollectionConverter/DiagramToCIMCollectionConverterServiceTest.java index 27316037..4f929ea4 100644 --- a/backend/src/test/java/org/rdfarchitect/cim/data/CIMCollectionConverter/DiagramToCIMCollectionConverterServiceTest.java +++ b/backend/src/test/java/org/rdfarchitect/cim/data/CIMCollectionConverter/DiagramToCIMCollectionConverterServiceTest.java @@ -24,8 +24,8 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.rdfarchitect.database.DatabasePort; +import org.rdfarchitect.database.GraphContext; import org.rdfarchitect.database.GraphIdentifier; -import org.rdfarchitect.database.inmemory.GraphWithContext; import org.rdfarchitect.database.inmemory.diagrams.ClassInDiagram; import org.rdfarchitect.database.inmemory.diagrams.CustomDiagram; import org.rdfarchitect.models.cim.data.dto.CIMClass; @@ -71,7 +71,6 @@ void convert_diagramFromGraph_returnsCIMCollection() { map.put(diagramId, diagram); var graph = mockGraph(map); - when(databasePort.getGraphWithContext(graphIdentifier)).thenReturn(graph); var expected = new CIMCollection(); @@ -236,8 +235,8 @@ void convert_invalidUUID_throwsIllegalArgumentException() { .isInstanceOf(IllegalArgumentException.class); } - private GraphWithContext mockGraph(ConcurrentHashMap diagrams) { - GraphWithContext graph = mock(GraphWithContext.class); + private GraphContext mockGraph(ConcurrentHashMap diagrams) { + GraphContext graph = mock(GraphContext.class); when(graph.getCustomDiagrams()).thenReturn(diagrams); return graph; } diff --git a/backend/src/test/java/org/rdfarchitect/cim/data/CIMCollectionConverter/GraphToCIMCollectionConverterServiceFilterTest.java b/backend/src/test/java/org/rdfarchitect/cim/data/CIMCollectionConverter/GraphToCIMCollectionConverterServiceFilterTest.java index 124555b2..9076791b 100644 --- a/backend/src/test/java/org/rdfarchitect/cim/data/CIMCollectionConverter/GraphToCIMCollectionConverterServiceFilterTest.java +++ b/backend/src/test/java/org/rdfarchitect/cim/data/CIMCollectionConverter/GraphToCIMCollectionConverterServiceFilterTest.java @@ -19,7 +19,7 @@ import static org.assertj.core.api.Assertions.*; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.riot.Lang; import org.apache.jena.riot.RDFDataMgr; import org.apache.jena.sparql.graph.GraphFactory; @@ -33,7 +33,6 @@ import org.rdfarchitect.database.inmemory.InMemoryDatabaseAdapter; import org.rdfarchitect.database.inmemory.InMemoryDatabaseImpl; import org.rdfarchitect.models.cim.rendering.GraphFilter; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; import org.rdfarchitect.services.rendering.GraphToCIMCollectionConverterService; import org.rdfarchitect.services.rendering.GraphToCIMCollectionConverterUseCase; @@ -45,13 +44,12 @@ class GraphToCIMCollectionConverterServiceFilterTest { - private final InMemoryDatabase database = new InMemoryDatabaseImpl(); - private final SchemaConfig schemaConfig = new SchemaConfig(); + private final InMemoryDatabase database = new InMemoryDatabaseImpl(schemaConfig); + private final GraphToCIMCollectionConverterUseCase converter = - new GraphToCIMCollectionConverterService( - new InMemoryDatabaseAdapter(database, schemaConfig)); + new GraphToCIMCollectionConverterService(new InMemoryDatabaseAdapter(database)); private final GraphIdentifier graphIdentifier = new GraphIdentifier("default", "default"); @@ -70,22 +68,16 @@ void tearDown() { private void addFileGraphToDatabase(String fileName) throws IOException { if (!database.containsGraph(graphIdentifier)) { - database.create(graphIdentifier, GraphFactory.createDefaultGraph()); + database.createGraph(graphIdentifier, GraphFactory.createDefaultGraph()); } - GraphRewindableWithUUIDs graphRewindable = null; - try { - var graph = GraphFactory.createDefaultGraph(); - InputStream in = Files.newInputStream(Path.of(fileName)); - RDFDataMgr.read(graph, in, Lang.TTL); - graphRewindable = database.begin(graphIdentifier, TxnType.WRITE); + var graph = GraphFactory.createDefaultGraph(); + InputStream in = Files.newInputStream(Path.of(fileName)); + RDFDataMgr.read(graph, in, Lang.TTL); + try (var ctx = database.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { for (var triple : graph.find().toList()) { - graphRewindable.add(triple); - } - graphRewindable.commit(); - } finally { - if (graphRewindable != null) { - graphRewindable.end(); + ctx.getRdfGraph().add(triple); } + ctx.commit(); } } diff --git a/backend/src/test/java/org/rdfarchitect/cim/data/CIMCollectionConverter/GraphToCIMCollectionConverterServiceNoFilterTest.java b/backend/src/test/java/org/rdfarchitect/cim/data/CIMCollectionConverter/GraphToCIMCollectionConverterServiceNoFilterTest.java index 6ccf8017..20f37225 100644 --- a/backend/src/test/java/org/rdfarchitect/cim/data/CIMCollectionConverter/GraphToCIMCollectionConverterServiceNoFilterTest.java +++ b/backend/src/test/java/org/rdfarchitect/cim/data/CIMCollectionConverter/GraphToCIMCollectionConverterServiceNoFilterTest.java @@ -19,7 +19,7 @@ import static org.assertj.core.api.Assertions.*; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.riot.Lang; import org.apache.jena.riot.RDFDataMgr; import org.apache.jena.sparql.graph.GraphFactory; @@ -36,7 +36,6 @@ import org.rdfarchitect.models.cim.data.dto.relations.RDFSLabel; import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; import org.rdfarchitect.models.cim.rendering.GraphFilter; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; import org.rdfarchitect.services.rendering.GraphToCIMCollectionConverterService; import org.rdfarchitect.services.rendering.GraphToCIMCollectionConverterUseCase; @@ -49,11 +48,10 @@ class GraphToCIMCollectionConverterServiceNoFilterTest { - private final InMemoryDatabase database = new InMemoryDatabaseImpl(); + private final InMemoryDatabase database = new InMemoryDatabaseImpl(new SchemaConfig()); private final GraphToCIMCollectionConverterUseCase converter = - new GraphToCIMCollectionConverterService( - new InMemoryDatabaseAdapter(database, new SchemaConfig())); + new GraphToCIMCollectionConverterService(new InMemoryDatabaseAdapter(database)); private final GraphIdentifier graphIdentifier = new GraphIdentifier("default", "default"); @@ -85,23 +83,17 @@ void tearDown() { private void addFileGraphToDatabase(String fileName) throws IOException { if (!database.containsGraph(graphIdentifier)) { - database.create(graphIdentifier, GraphFactory.createDefaultGraph()); + database.createGraph(graphIdentifier, GraphFactory.createDefaultGraph()); } - GraphRewindableWithUUIDs graphRewindable = null; - try { - var graph = GraphFactory.createDefaultGraph(); - InputStream in = Files.newInputStream(Path.of(fileName)); - Lang lang = fileName.endsWith(".rdf") ? Lang.RDFXML : Lang.TTL; - RDFDataMgr.read(graph, in, lang); - graphRewindable = database.begin(graphIdentifier, TxnType.WRITE); + var graph = GraphFactory.createDefaultGraph(); + InputStream in = Files.newInputStream(Path.of(fileName)); + Lang lang = fileName.endsWith(".rdf") ? Lang.RDFXML : Lang.TTL; + RDFDataMgr.read(graph, in, lang); + try (var ctx = database.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) { for (var triple : graph.find().toList()) { - graphRewindable.add(triple); - } - graphRewindable.commit(); - } finally { - if (graphRewindable != null) { - graphRewindable.end(); + ctx.getRdfGraph().add(triple); } + ctx.commit(); } } diff --git a/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/CIMUpdatesTestBase.java b/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/CIMUpdatesTestBase.java index 934e5d22..4451c43f 100644 --- a/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/CIMUpdatesTestBase.java +++ b/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/CIMUpdatesTestBase.java @@ -19,18 +19,19 @@ import org.apache.jena.graph.Graph; import org.apache.jena.graph.Node; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.update.Update; +import org.apache.jena.update.UpdateExecutionFactory; import org.apache.jena.update.UpdateRequest; import org.junit.jupiter.api.BeforeEach; import org.rdfarchitect.config.SchemaConfig; import org.rdfarchitect.database.DatabasePort; +import org.rdfarchitect.database.GraphContext; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.database.inmemory.InMemoryDatabaseAdapter; import org.rdfarchitect.database.inmemory.InMemoryDatabaseImpl; -import org.rdfarchitect.database.inmemory.InMemorySparqlExecutor; +import org.rdfarchitect.database.inmemory.SessionDataStore; import org.rdfarchitect.rdf.graph.source.builder.implementations.GraphFileSourceBuilderImpl; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; import org.springframework.mock.web.MockMultipartFile; import java.io.IOException; @@ -48,7 +49,7 @@ public class CIMUpdatesTestBase { protected static final String GRAPH_URI = "http://graph"; protected static final GraphIdentifier graphIdentifier = new GraphIdentifier("default", GRAPH_URI); - protected GraphRewindableWithUUIDs testGraph; + protected GraphContext testGraph; protected DatabasePort databasePort; // base test constants @@ -113,7 +114,7 @@ public class CIMUpdatesTestBase { @BeforeEach void setUpEnvironment() { - databasePort = new InMemoryDatabaseAdapter(new InMemoryDatabaseImpl(), new SchemaConfig()); + databasePort = new InMemoryDatabaseAdapter(new InMemoryDatabaseImpl(new SchemaConfig())); addGraphFromFile(BASE_FILENAME); } @@ -132,17 +133,13 @@ protected void addGraphFromFile(String fileName) { .build() .graph(); databasePort.createGraph(graphIdentifier, graph); - testGraph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); + testGraph = databasePort.getGraphWithContext(graphIdentifier); } protected void addTriple(Node subject, Node predicate, Node object) { - var graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - try { - graph.begin(TxnType.WRITE); - graph.add(subject, predicate, object); - graph.commit(); - } finally { - graph.end(); + try (var ctx = testGraph.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(subject, predicate, object); + ctx.commit(); } } @@ -153,22 +150,20 @@ protected void executeUpdateOnTestGraph(Update update) { /** Use this method to execute a multi-operation {@link UpdateRequest} on the test graph. */ protected void executeUpdateOnTestGraph(UpdateRequest update) { - InMemorySparqlExecutor.executeSingleUpdate( - databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(), - update, - graphIdentifier.graphUri()); + try (var ctx = testGraph.begin(ReadWrite.WRITE)) { + var dataset = + SessionDataStore.wrapGraphInDataset( + ctx.getRdfGraph(), graphIdentifier.graphUri()); + UpdateExecutionFactory.create(update, dataset).execute(); + ctx.commit(); + } } /** Use this method to execute write actions in a transaction using lambda expression */ protected void executeWriteTransaction(Consumer graphOperation) { - try { - testGraph.begin(TxnType.WRITE); - graphOperation.accept(testGraph); - testGraph.commit(); - } finally { - if (testGraph != null) { - testGraph.end(); - } + try (var ctx = testGraph.begin(ReadWrite.WRITE)) { + graphOperation.accept(ctx.getRdfGraph()); + ctx.commit(); } } @@ -178,18 +173,14 @@ protected void executeWriteTransaction(Consumer graphOperation) { * supplier build the {@link UpdateRequest} against {@code testGraph}, then executes it. */ protected void executeUpdateBuiltAgainstTestGraph( - Function updateBuilder) { - var graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - try { - graph.begin(TxnType.WRITE); - var update = updateBuilder.apply(graph); + Function updateBuilder) { + try (var ctx = testGraph.begin(ReadWrite.WRITE)) { + var update = updateBuilder.apply(ctx.getRdfGraph()); var dataset = org.rdfarchitect.database.inmemory.SessionDataStore.wrapGraphInDataset( - graph, graphIdentifier.graphUri()); + ctx.getRdfGraph(), graphIdentifier.graphUri()); org.apache.jena.update.UpdateExecutionFactory.create(update, dataset).execute(); - graph.commit(); - } finally { - graph.end(); + ctx.commit(); } } } diff --git a/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/associations/CIMUpdatesAssociationsTest.java b/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/associations/CIMUpdatesAssociationsTest.java index e84ee15c..89214559 100644 --- a/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/associations/CIMUpdatesAssociationsTest.java +++ b/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/associations/CIMUpdatesAssociationsTest.java @@ -21,7 +21,7 @@ import org.apache.jena.graph.Node; import org.apache.jena.graph.NodeFactory; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; import org.junit.jupiter.api.BeforeAll; @@ -106,23 +106,22 @@ void deleteAssociation_associationAndInverseExist_deletesAssociations() { .build()); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isFalse assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ASSOC_URI), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ASSOC_URI), + Node.ANY, + Node.ANY)) .isFalse(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_INVERSE_ASSOC_URI), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_INVERSE_ASSOC_URI), + Node.ANY, + Node.ANY)) .isFalse(); - } finally { - testGraph.end(); } } } @@ -145,108 +144,125 @@ void replaceAssociation_associationAndInverseExist_replacesAssociation() { associationRequired, associationInverseRequired)) .build()); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isFalse assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ASSOC_URI), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ASSOC_URI), + Node.ANY, + Node.ANY)) .isFalse(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_INVERSE_ASSOC_URI), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_INVERSE_ASSOC_URI), + Node.ANY, + Node.ANY)) .isFalse(); // isTrue assertThat( - testGraph.contains( - NodeFactory.createURI(ASSOC_URI), - RDF.type.asNode(), - RDF.Property.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ASSOC_URI), + RDF.type.asNode(), + RDF.Property.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ASSOC_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(INVERSE_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ASSOC_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang(INVERSE_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ASSOC_URI), - RDFS.domain.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ASSOC_URI), + RDFS.domain.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ASSOC_URI), - RDFS.range.asNode(), - NodeFactory.createURI(INVERSE_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ASSOC_URI), + RDFS.range.asNode(), + NodeFactory.createURI(INVERSE_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ASSOC_URI), - CIMS.associationUsed.asNode(), - NodeFactory.createLiteralString("Yes"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ASSOC_URI), + CIMS.associationUsed.asNode(), + NodeFactory.createLiteralString("Yes"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ASSOC_URI), - CIMS.inverseRoleName.asNode(), - NodeFactory.createURI(INVERSE_ASSOC_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ASSOC_URI), + CIMS.inverseRoleName.asNode(), + NodeFactory.createURI(INVERSE_ASSOC_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ASSOC_URI), - CIMS.multiplicity.asNode(), - new CIMSMultiplicity(MULTIPLICITY_URI).getUri().toNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ASSOC_URI), + CIMS.multiplicity.asNode(), + new CIMSMultiplicity(MULTIPLICITY_URI) + .getUri() + .toNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(INVERSE_ASSOC_URI), - RDF.type.asNode(), - RDF.Property.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(INVERSE_ASSOC_URI), + RDF.type.asNode(), + RDF.Property.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(INVERSE_ASSOC_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(CLASS_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(INVERSE_ASSOC_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang(CLASS_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(INVERSE_ASSOC_URI), - RDFS.domain.asNode(), - NodeFactory.createURI(INVERSE_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(INVERSE_ASSOC_URI), + RDFS.domain.asNode(), + NodeFactory.createURI(INVERSE_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(INVERSE_ASSOC_URI), - RDFS.range.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(INVERSE_ASSOC_URI), + RDFS.range.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(INVERSE_ASSOC_URI), - CIMS.associationUsed.asNode(), - NodeFactory.createLiteralString("Yes"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(INVERSE_ASSOC_URI), + CIMS.associationUsed.asNode(), + NodeFactory.createLiteralString("Yes"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(INVERSE_ASSOC_URI), - CIMS.inverseRoleName.asNode(), - NodeFactory.createURI(ASSOC_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(INVERSE_ASSOC_URI), + CIMS.inverseRoleName.asNode(), + NodeFactory.createURI(ASSOC_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(INVERSE_ASSOC_URI), - CIMS.multiplicity.asNode(), - new CIMSMultiplicity(MULTIPLICITY_URI).getUri().toNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(INVERSE_ASSOC_URI), + CIMS.multiplicity.asNode(), + new CIMSMultiplicity(MULTIPLICITY_URI) + .getUri() + .toNode())) .isTrue(); - } finally { - testGraph.end(); } } } @@ -266,7 +282,7 @@ void replaceAssociations_associationAndInverseExist_replacesAssociations() { CIMUpdates.replaceAssociations( databasePort.getPrefixMapping(DATASET_NAME), GRAPH_URI, - MY_UUID.toString(), + MY_UUID, List.of( new CIMAssociationPair( associationRequired, @@ -274,108 +290,125 @@ void replaceAssociations_associationAndInverseExist_replacesAssociations() { .build()); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isFalse assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ASSOC_URI), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ASSOC_URI), + Node.ANY, + Node.ANY)) .isFalse(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_INVERSE_ASSOC_URI), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_INVERSE_ASSOC_URI), + Node.ANY, + Node.ANY)) .isFalse(); // isTrue assertThat( - testGraph.contains( - NodeFactory.createURI(ASSOC_URI), - RDF.type.asNode(), - RDF.Property.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ASSOC_URI), + RDF.type.asNode(), + RDF.Property.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ASSOC_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(INVERSE_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ASSOC_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang(INVERSE_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ASSOC_URI), - RDFS.domain.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ASSOC_URI), + RDFS.domain.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ASSOC_URI), - RDFS.range.asNode(), - NodeFactory.createURI(INVERSE_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ASSOC_URI), + RDFS.range.asNode(), + NodeFactory.createURI(INVERSE_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ASSOC_URI), - CIMS.associationUsed.asNode(), - NodeFactory.createLiteralString("Yes"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ASSOC_URI), + CIMS.associationUsed.asNode(), + NodeFactory.createLiteralString("Yes"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ASSOC_URI), - CIMS.inverseRoleName.asNode(), - NodeFactory.createURI(INVERSE_ASSOC_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ASSOC_URI), + CIMS.inverseRoleName.asNode(), + NodeFactory.createURI(INVERSE_ASSOC_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ASSOC_URI), - CIMS.multiplicity.asNode(), - new CIMSMultiplicity(MULTIPLICITY_URI).getUri().toNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ASSOC_URI), + CIMS.multiplicity.asNode(), + new CIMSMultiplicity(MULTIPLICITY_URI) + .getUri() + .toNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(INVERSE_ASSOC_URI), - RDF.type.asNode(), - RDF.Property.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(INVERSE_ASSOC_URI), + RDF.type.asNode(), + RDF.Property.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(INVERSE_ASSOC_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(CLASS_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(INVERSE_ASSOC_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang(CLASS_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(INVERSE_ASSOC_URI), - RDFS.domain.asNode(), - NodeFactory.createURI(INVERSE_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(INVERSE_ASSOC_URI), + RDFS.domain.asNode(), + NodeFactory.createURI(INVERSE_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(INVERSE_ASSOC_URI), - RDFS.range.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(INVERSE_ASSOC_URI), + RDFS.range.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(INVERSE_ASSOC_URI), - CIMS.associationUsed.asNode(), - NodeFactory.createLiteralString("Yes"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(INVERSE_ASSOC_URI), + CIMS.associationUsed.asNode(), + NodeFactory.createLiteralString("Yes"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(INVERSE_ASSOC_URI), - CIMS.inverseRoleName.asNode(), - NodeFactory.createURI(ASSOC_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(INVERSE_ASSOC_URI), + CIMS.inverseRoleName.asNode(), + NodeFactory.createURI(ASSOC_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(INVERSE_ASSOC_URI), - CIMS.multiplicity.asNode(), - new CIMSMultiplicity(MULTIPLICITY_URI).getUri().toNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(INVERSE_ASSOC_URI), + CIMS.multiplicity.asNode(), + new CIMSMultiplicity(MULTIPLICITY_URI) + .getUri() + .toNode())) .isTrue(); - } finally { - testGraph.end(); } } } diff --git a/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/attributes/CIMUpdatesAttributesTest.java b/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/attributes/CIMUpdatesAttributesTest.java index a3bf127f..e2e9c28f 100644 --- a/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/attributes/CIMUpdatesAttributesTest.java +++ b/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/attributes/CIMUpdatesAttributesTest.java @@ -22,7 +22,7 @@ import org.apache.jena.graph.Node; import org.apache.jena.graph.NodeFactory; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; import org.junit.jupiter.api.BeforeEach; @@ -120,54 +120,61 @@ void replaceAttribute_attributeExistsRequired_replacesAttributeRequired() { false)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isFalse assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), + Node.ANY, + Node.ANY)) .isFalse(); // isTrue assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDF.type.asNode(), - RDF.Property.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDF.type.asNode(), + RDF.Property.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(ATTRIBUTE_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang( + ATTRIBUTE_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDFS.domain.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDFS.domain.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDFS.range.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDFS.range.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - CIMS.stereotype.asNode(), - CIMStereotypes.attribute.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + CIMS.stereotype.asNode(), + CIMStereotypes.attribute.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - CIMS.multiplicity.asNode(), - new CIMSMultiplicity(MULTIPLICITY_URI).getUri().toNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + CIMS.multiplicity.asNode(), + new CIMSMultiplicity(MULTIPLICITY_URI) + .getUri() + .toNode())) .isTrue(); - } finally { - testGraph.end(); } } @@ -189,74 +196,84 @@ void replaceAttribute_attributeExistsOptional_replacesAttributeOptional() { false)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isFalse assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), + Node.ANY, + Node.ANY)) .isFalse(); // isTrue assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDF.type.asNode(), - RDF.Property.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDF.type.asNode(), + RDF.Property.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(ATTRIBUTE_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang( + ATTRIBUTE_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDFS.domain.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDFS.domain.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDFS.range.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDFS.range.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - CIMS.stereotype.asNode(), - CIMStereotypes.attribute.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + CIMS.stereotype.asNode(), + CIMStereotypes.attribute.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - CIMS.multiplicity.asNode(), - new CIMSMultiplicity(MULTIPLICITY_URI).getUri().toNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + CIMS.multiplicity.asNode(), + new CIMSMultiplicity(MULTIPLICITY_URI) + .getUri() + .toNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDFS.comment.asNode(), - new RDFSComment(COMMENT, new URI(COMMENT_FORMAT)) - .asTypedLiteral() - .asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDFS.comment.asNode(), + new RDFSComment(COMMENT, new URI(COMMENT_FORMAT)) + .asTypedLiteral() + .asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - CIMS.isFixed.asNode(), - NodeFactory.createLiteralString(IS_FIXED_VALUE))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + CIMS.isFixed.asNode(), + NodeFactory.createLiteralString(IS_FIXED_VALUE))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - CIMS.isDefault.asNode(), - NodeFactory.createLiteralString(IS_DEFAULT_VALUE))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + CIMS.isDefault.asNode(), + NodeFactory.createLiteralString(IS_DEFAULT_VALUE))) .isTrue(); - } finally { - testGraph.end(); } } @@ -300,54 +317,61 @@ void replaceAttribute_attributeExistsPrimitive_replacesAttributePrimitive() { false)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isFalse assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), + Node.ANY, + Node.ANY)) .isFalse(); // isTrue assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDF.type.asNode(), - RDF.Property.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDF.type.asNode(), + RDF.Property.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(ATTRIBUTE_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang( + ATTRIBUTE_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDFS.domain.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDFS.domain.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - CIMS.datatype.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + CIMS.datatype.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - CIMS.stereotype.asNode(), - CIMStereotypes.attribute.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + CIMS.stereotype.asNode(), + CIMStereotypes.attribute.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - CIMS.multiplicity.asNode(), - new CIMSMultiplicity(MULTIPLICITY_URI).getUri().toNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + CIMS.multiplicity.asNode(), + new CIMSMultiplicity(MULTIPLICITY_URI) + .getUri() + .toNode())) .isTrue(); - } finally { - testGraph.end(); } } @@ -369,74 +393,84 @@ void replaceAttribute_attributeExistsRequired_replacesAttributeWithOptional() { false)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isFalse assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), + Node.ANY, + Node.ANY)) .isFalse(); // isTrue assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDF.type.asNode(), - RDF.Property.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDF.type.asNode(), + RDF.Property.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(ATTRIBUTE_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang( + ATTRIBUTE_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDFS.domain.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDFS.domain.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDFS.range.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDFS.range.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - CIMS.stereotype.asNode(), - CIMStereotypes.attribute.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + CIMS.stereotype.asNode(), + CIMStereotypes.attribute.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - CIMS.multiplicity.asNode(), - new CIMSMultiplicity(MULTIPLICITY_URI).getUri().toNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + CIMS.multiplicity.asNode(), + new CIMSMultiplicity(MULTIPLICITY_URI) + .getUri() + .toNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDFS.comment.asNode(), - new RDFSComment(COMMENT, new URI(COMMENT_FORMAT)) - .asTypedLiteral() - .asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDFS.comment.asNode(), + new RDFSComment(COMMENT, new URI(COMMENT_FORMAT)) + .asTypedLiteral() + .asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - CIMS.isFixed.asNode(), - NodeFactory.createLiteralString(IS_FIXED_VALUE))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + CIMS.isFixed.asNode(), + NodeFactory.createLiteralString(IS_FIXED_VALUE))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - CIMS.isDefault.asNode(), - NodeFactory.createLiteralString(IS_DEFAULT_VALUE))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + CIMS.isDefault.asNode(), + NodeFactory.createLiteralString(IS_DEFAULT_VALUE))) .isTrue(); - } finally { - testGraph.end(); } } @@ -462,25 +496,22 @@ void replaceAttribute_blankNodeFixedValue_insertsExactlyOneWrapper() { false)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { var isFixedTriples = - testGraph + ctx.getRdfGraph() .find( NodeFactory.createURI(ATTRIBUTE_URI), CIMS.isFixed.asNode(), Node.ANY) .toList(); assertThat(isFixedTriples).hasSize(1); - var blankNode = isFixedTriples.get(0).getObject(); + var blankNode = isFixedTriples.getFirst().getObject(); assertThat(blankNode.isBlank()).isTrue(); var literalTriples = - testGraph.find(blankNode, RDFS.Literal.asNode(), Node.ANY).toList(); + ctx.getRdfGraph().find(blankNode, RDFS.Literal.asNode(), Node.ANY).toList(); assertThat(literalTriples).hasSize(1); - assertThat(literalTriples.get(0).getObject().getLiteralLexicalForm()) + assertThat(literalTriples.getFirst().getObject().getLiteralLexicalForm()) .isEqualTo(IS_FIXED_VALUE); - } finally { - testGraph.end(); } } } @@ -500,16 +531,14 @@ void deleteAttribute_attributeExists_deletesAttribute() { databasePort.getPrefixMapping(DATASET_NAME), GRAPH_URI, MY_UUID)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), + Node.ANY, + Node.ANY)) .isFalse(); - } finally { - testGraph.end(); } } @@ -528,48 +557,53 @@ void deleteAttribute_attributeDoesNotExist_doesNothing() { nonExistingUUID)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isTrue assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), - RDF.type.asNode(), - RDF.Property.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), + RDF.type.asNode(), + RDF.Property.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang( - EXISTING_ATTRIBUTE_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang( + EXISTING_ATTRIBUTE_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), - RDFS.domain.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), + RDFS.domain.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), - RDFS.range.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), + RDFS.range.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), - CIMS.stereotype.asNode(), - CIMStereotypes.attribute.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), + CIMS.stereotype.asNode(), + CIMStereotypes.attribute.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), - CIMS.multiplicity.asNode(), - new CIMSMultiplicity(MULTIPLICITY_URI).getUri().toNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI), + CIMS.multiplicity.asNode(), + new CIMSMultiplicity(MULTIPLICITY_URI) + .getUri() + .toNode())) .isTrue(); - } finally { - testGraph.end(); } } @@ -587,12 +621,9 @@ void deleteAttribute_emptyGraph_doesNothing() { nonExistingUUID)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isTrue - assertThat(testGraph.isEmpty()).isTrue(); - } finally { - testGraph.end(); + assertThat(ctx.getRdfGraph().isEmpty()).isTrue(); } } } @@ -618,65 +649,73 @@ void replacesAttributes_attributesExist_replacesAttributesWithDifferentLabels() g, databasePort.getPrefixMapping(DATASET_NAME), GRAPH_URI, - MY_UUID.toString(), + MY_UUID, List.of(newAttributeOne), false)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isFalse assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "A"), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "A"), + Node.ANY, + Node.ANY)) .isFalse(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "B"), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "B"), + Node.ANY, + Node.ANY)) .isFalse(); // isTrue assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDF.type.asNode(), - RDF.Property.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDF.type.asNode(), + RDF.Property.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(ATTRIBUTE_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang( + ATTRIBUTE_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDFS.domain.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDFS.domain.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDFS.range.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDFS.range.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - CIMS.stereotype.asNode(), - CIMStereotypes.attribute.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + CIMS.stereotype.asNode(), + CIMStereotypes.attribute.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - CIMS.multiplicity.asNode(), - new CIMSMultiplicity(MULTIPLICITY_URI).getUri().toNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + CIMS.multiplicity.asNode(), + new CIMSMultiplicity(MULTIPLICITY_URI) + .getUri() + .toNode())) .isTrue(); - } finally { - testGraph.end(); } } @@ -701,101 +740,118 @@ void replacesAttributes_attributesExist_replacesAttributesWithDifferentLabels() g, databasePort.getPrefixMapping(DATASET_NAME), GRAPH_URI, - MY_UUID.toString(), + MY_UUID, List.of(newAttributeOne), false)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isFalse assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "A"), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "A"), + Node.ANY, + Node.ANY)) .isFalse(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "B"), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "B"), + Node.ANY, + Node.ANY)) .isFalse(); // isTrue assertThat( - testGraph.contains( - NodeFactory.createURI(OTHER_ATTRIBUTE_URI), - RDF.type.asNode(), - RDF.Property.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(OTHER_ATTRIBUTE_URI), + RDF.type.asNode(), + RDF.Property.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(OTHER_ATTRIBUTE_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(OTHER_ATTRIBUTE_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(OTHER_ATTRIBUTE_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang( + OTHER_ATTRIBUTE_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(OTHER_ATTRIBUTE_URI), - RDFS.domain.asNode(), - NodeFactory.createURI(OTHER_CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(OTHER_ATTRIBUTE_URI), + RDFS.domain.asNode(), + NodeFactory.createURI(OTHER_CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(OTHER_ATTRIBUTE_URI), - RDFS.range.asNode(), - NodeFactory.createURI(OTHER_CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(OTHER_ATTRIBUTE_URI), + RDFS.range.asNode(), + NodeFactory.createURI(OTHER_CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(OTHER_ATTRIBUTE_URI), - CIMS.stereotype.asNode(), - CIMStereotypes.attribute.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(OTHER_ATTRIBUTE_URI), + CIMS.stereotype.asNode(), + CIMStereotypes.attribute.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(OTHER_ATTRIBUTE_URI), - CIMS.multiplicity.asNode(), - new CIMSMultiplicity(MULTIPLICITY_URI).getUri().toNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(OTHER_ATTRIBUTE_URI), + CIMS.multiplicity.asNode(), + new CIMSMultiplicity(MULTIPLICITY_URI) + .getUri() + .toNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDF.type.asNode(), - RDF.Property.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDF.type.asNode(), + RDF.Property.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(ATTRIBUTE_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang( + ATTRIBUTE_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDFS.domain.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDFS.domain.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - RDFS.range.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + RDFS.range.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - CIMS.stereotype.asNode(), - CIMStereotypes.attribute.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + CIMS.stereotype.asNode(), + CIMStereotypes.attribute.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ATTRIBUTE_URI), - CIMS.multiplicity.asNode(), - new CIMSMultiplicity(MULTIPLICITY_URI).getUri().toNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ATTRIBUTE_URI), + CIMS.multiplicity.asNode(), + new CIMSMultiplicity(MULTIPLICITY_URI) + .getUri() + .toNode())) .isTrue(); - } finally { - testGraph.end(); } } @@ -812,28 +868,27 @@ void replacesAttributes_attributesExist_deletesAttributes() { g, databasePort.getPrefixMapping(DATASET_NAME), GRAPH_URI, - MY_UUID.toString(), + MY_UUID, List.of(), false)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isFalse assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "A"), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "A"), + Node.ANY, + Node.ANY)) .isFalse(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "B"), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "B"), + Node.ANY, + Node.ANY)) .isFalse(); - } finally { - testGraph.end(); } } @@ -851,90 +906,103 @@ void replacesAttributes_classDoesNotExist_doesNothing() { g, databasePort.getPrefixMapping(DATASET_NAME), GRAPH_URI, - "does not exist", + UUID.randomUUID(), List.of(), false)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isTrue assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "A"), - RDF.type.asNode(), - RDF.Property.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "A"), + RDF.type.asNode(), + RDF.Property.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "A"), - RDFS.label.asNode(), - NodeFactory.createLiteralLang( - EXISTING_ATTRIBUTE_LABEL + "A", "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "A"), + RDFS.label.asNode(), + NodeFactory.createLiteralLang( + EXISTING_ATTRIBUTE_LABEL + "A", "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "A"), - RDFS.domain.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "A"), + RDFS.domain.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "A"), - RDFS.range.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "A"), + RDFS.range.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "A"), - CIMS.stereotype.asNode(), - CIMStereotypes.attribute.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "A"), + CIMS.stereotype.asNode(), + CIMStereotypes.attribute.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "A"), - CIMS.multiplicity.asNode(), - new CIMSMultiplicity(MULTIPLICITY_URI).getUri().toNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "A"), + CIMS.multiplicity.asNode(), + new CIMSMultiplicity(MULTIPLICITY_URI) + .getUri() + .toNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "B"), - RDF.type.asNode(), - RDF.Property.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "B"), + RDF.type.asNode(), + RDF.Property.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "B"), - RDFS.label.asNode(), - NodeFactory.createLiteralLang( - EXISTING_ATTRIBUTE_LABEL + "B", "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "B"), + RDFS.label.asNode(), + NodeFactory.createLiteralLang( + EXISTING_ATTRIBUTE_LABEL + "B", "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "B"), - RDFS.domain.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "B"), + RDFS.domain.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "B"), - RDFS.range.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "B"), + RDFS.range.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "B"), - CIMS.stereotype.asNode(), - CIMStereotypes.attribute.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "B"), + CIMS.stereotype.asNode(), + CIMStereotypes.attribute.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "B"), - CIMS.multiplicity.asNode(), - new CIMSMultiplicity(MULTIPLICITY_URI).getUri().toNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "B"), + CIMS.multiplicity.asNode(), + new CIMSMultiplicity(MULTIPLICITY_URI) + .getUri() + .toNode())) .isTrue(); - } finally { - testGraph.end(); } } } @@ -957,23 +1025,22 @@ void deleteAttributesOfType_attributesExist_deletesAttributes() { .build()); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isFalse assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "A"), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "A"), + Node.ANY, + Node.ANY)) .isFalse(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "B"), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ATTRIBUTE_URI + "B"), + Node.ANY, + Node.ANY)) .isFalse(); - } finally { - testGraph.end(); } } } diff --git a/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/classes/CIMUpdatesClassesTest.java b/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/classes/CIMUpdatesClassesTest.java index 1df69bb8..3b7755cd 100644 --- a/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/classes/CIMUpdatesClassesTest.java +++ b/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/classes/CIMUpdatesClassesTest.java @@ -21,7 +21,7 @@ import org.apache.jena.graph.Node; import org.apache.jena.graph.NodeFactory; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.RDFNode; import org.apache.jena.vocabulary.RDF; @@ -112,42 +112,40 @@ void replaceClass_classExists_replacesClassUpdatesReferences() { false)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { + var graph = ctx.getRdfGraph(); // isFalse assertThat( - testGraph.contains( + graph.contains( NodeFactory.createURI(EXISTING_CLASS_URI), Node.ANY, Node.ANY)) .isFalse(); assertThat( - testGraph.contains( + graph.contains( NodeFactory.createURI(SUB_CLASS_URI), RDFS.subClassOf.asNode(), NodeFactory.createURI(EXISTING_CLASS_URI))) .isFalse(); // isTrue assertThat( - testGraph.contains( + graph.contains( NodeFactory.createURI(CLASS_URI), RDF.type.asNode(), RDFS.Class.asNode())) .isTrue(); assertThat( - testGraph.contains( + graph.contains( NodeFactory.createURI(CLASS_URI), RDFS.label.asNode(), NodeFactory.createLiteralLang(CLASS_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( + graph.contains( NodeFactory.createURI(SUB_CLASS_URI), RDFS.subClassOf.asNode(), NodeFactory.createURI(CLASS_URI))) .isTrue(); - } finally { - testGraph.end(); } } @@ -167,36 +165,36 @@ void replaceClass_classExists_replacesClassOptionals() { false)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { + var graph = ctx.getRdfGraph(); // isFalse assertThat( - testGraph.contains( + graph.contains( NodeFactory.createURI(EXISTING_CLASS_URI), Node.ANY, Node.ANY)) .isFalse(); // isTrue assertThat( - testGraph.contains( + graph.contains( NodeFactory.createURI(CLASS_URI), RDF.type.asNode(), RDFS.Class.asNode())) .isTrue(); assertThat( - testGraph.contains( + graph.contains( NodeFactory.createURI(CLASS_URI), RDFS.label.asNode(), NodeFactory.createLiteralLang(CLASS_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( + graph.contains( NodeFactory.createURI(CLASS_URI), RDFS.subClassOf.asNode(), NodeFactory.createURI(SUPER_CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( + graph.contains( NodeFactory.createURI(CLASS_URI), RDFS.comment.asNode(), new RDFSComment(COMMENT, new URI(COMMENT_FORMAT)) @@ -204,19 +202,17 @@ void replaceClass_classExists_replacesClassOptionals() { .asNode())) .isTrue(); assertThat( - testGraph.contains( + graph.contains( NodeFactory.createURI(CLASS_URI), CIMS.belongsToCategory.asNode(), NodeFactory.createURI(PACKAGE_URI))) .isTrue(); assertThat( - testGraph.contains( + graph.contains( NodeFactory.createURI(CLASS_URI), CIMS.stereotype.asNode(), CIMStereotypes.concrete.asNode())) .isTrue(); - } finally { - testGraph.end(); } } } @@ -238,23 +234,22 @@ void insertClass_emptyGraph_insertsClass() { classRequired)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isTrue assertThat( - testGraph.contains( - NodeFactory.createURI(CLASS_URI), - RDF.type.asNode(), - RDFS.Class.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(CLASS_URI), + RDF.type.asNode(), + RDFS.Class.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(CLASS_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(CLASS_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(CLASS_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang(CLASS_LABEL, "en"))) .isTrue(); - } finally { - testGraph.end(); } } @@ -272,49 +267,52 @@ void insertClass_emptyGraph_insertsClassOptionals() { classOptional)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isTrue assertThat( - testGraph.contains( - NodeFactory.createURI(CLASS_URI), - RDF.type.asNode(), - RDFS.Class.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(CLASS_URI), + RDF.type.asNode(), + RDFS.Class.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(CLASS_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(CLASS_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(CLASS_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang(CLASS_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(CLASS_URI), - RDFS.subClassOf.asNode(), - NodeFactory.createURI(SUPER_CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(CLASS_URI), + RDFS.subClassOf.asNode(), + NodeFactory.createURI(SUPER_CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(CLASS_URI), - RDFS.comment.asNode(), - new RDFSComment(COMMENT, new URI(COMMENT_FORMAT)) - .asTypedLiteral() - .asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(CLASS_URI), + RDFS.comment.asNode(), + new RDFSComment(COMMENT, new URI(COMMENT_FORMAT)) + .asTypedLiteral() + .asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(CLASS_URI), - CIMS.belongsToCategory.asNode(), - NodeFactory.createURI(PACKAGE_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(CLASS_URI), + CIMS.belongsToCategory.asNode(), + NodeFactory.createURI(PACKAGE_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(CLASS_URI), - CIMS.stereotype.asNode(), - CIMStereotypes.concrete.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(CLASS_URI), + CIMS.stereotype.asNode(), + CIMStereotypes.concrete.asNode())) .isTrue(); - } finally { - testGraph.end(); } } @@ -342,16 +340,14 @@ void insertClass_packageWithSameIriExists_throwsException() { .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("package with the same IRI"); - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { assertThat( - testGraph.contains( - NodeFactory.createURI(PACKAGE_URI), - RDF.type.asNode(), - RDFS.Class.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PACKAGE_URI), + RDF.type.asNode(), + RDFS.Class.asNode())) .isFalse(); - } finally { - testGraph.end(); } } } @@ -369,22 +365,18 @@ void deleteClass_classExists_deletesClass() { executeWriteTransaction( graph -> CIMUpdates.deleteClass( - graph, - databasePort.getPrefixMapping(DATASET_NAME), - MY_UUID.toString())); + graph, databasePort.getPrefixMapping(DATASET_NAME), MY_UUID)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isFalse assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_CLASS_URI), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_CLASS_URI), + Node.ANY, + Node.ANY)) .isFalse(); - } finally { - testGraph.end(); } } @@ -398,22 +390,18 @@ void deleteClass_classExists_deletesClassOptionals() { executeWriteTransaction( graph -> CIMUpdates.deleteClass( - graph, - databasePort.getPrefixMapping(DATASET_NAME), - MY_UUID.toString())); + graph, databasePort.getPrefixMapping(DATASET_NAME), MY_UUID)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isFalse assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_CLASS_URI), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_CLASS_URI), + Node.ANY, + Node.ANY)) .isFalse(); - } finally { - testGraph.end(); } } @@ -427,28 +415,25 @@ void deleteClass_enumClassAndEnumEntriesExist_deletesEnumClassAndEnumEntries() { executeWriteTransaction( graph -> CIMUpdates.deleteClass( - graph, - databasePort.getPrefixMapping(DATASET_NAME), - MY_UUID.toString())); + graph, databasePort.getPrefixMapping(DATASET_NAME), MY_UUID)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isFalse assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_CLASS_URI), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_CLASS_URI), + Node.ANY, + Node.ANY)) .isFalse(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ENUM_ENTRY_URI), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ENUM_ENTRY_URI), + Node.ANY, + Node.ANY)) .isFalse(); - } finally { - testGraph.end(); } } @@ -466,28 +451,24 @@ void deleteClass_classAndReferenceExists_deletesClassAndKeepsReference() { executeWriteTransaction( graph -> CIMUpdates.deleteClass( - graph, - databasePort.getPrefixMapping(DATASET_NAME), - MY_UUID.toString())); + graph, databasePort.getPrefixMapping(DATASET_NAME), MY_UUID)); // Assert - var model = ModelFactory.createModelForGraph(testGraph); - var classResource = model.createResource(EXISTING_CLASS_URI); - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { + var model = ModelFactory.createModelForGraph(ctx.getRdfGraph()); + var classResource = model.createResource(EXISTING_CLASS_URI); // only the uuid triple remains for the deleted class assertThat(model.listStatements(classResource, null, (RDFNode) null).toList()) .hasSize(1) .allMatch(stmt -> stmt.getPredicate().equals(RDFA.uuid)); // subClassOf reference is kept assertThat( - testGraph.contains( - NodeFactory.createURI(SUB_CLASS_URI), - RDFS.subClassOf.asNode(), - NodeFactory.createURI(EXISTING_CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(SUB_CLASS_URI), + RDFS.subClassOf.asNode(), + NodeFactory.createURI(EXISTING_CLASS_URI))) .isTrue(); - } finally { - testGraph.end(); } } } @@ -503,23 +484,19 @@ void deleteBase_classExists_deletesClassBase() { // Act executeUpdateOnTestGraph( CIMUpdates.deleteBase( - databasePort.getPrefixMapping(DATASET_NAME), - GRAPH_URI, - MY_UUID.toString()) + databasePort.getPrefixMapping(DATASET_NAME), GRAPH_URI, MY_UUID) .build()); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isFalse assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_CLASS_URI), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_CLASS_URI), + Node.ANY, + Node.ANY)) .isFalse(); - } finally { - testGraph.end(); } } @@ -532,16 +509,13 @@ void deleteBase_twoClassExists_deletesClassBaseOtherClassUntouched() { // Act executeUpdateOnTestGraph( CIMUpdates.deleteBase( - databasePort.getPrefixMapping(DATASET_NAME), - GRAPH_URI, - MY_UUID.toString()) + databasePort.getPrefixMapping(DATASET_NAME), GRAPH_URI, MY_UUID) .build()); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isFalse - var model = ModelFactory.createModelForGraph(testGraph); + var model = ModelFactory.createModelForGraph(ctx.getRdfGraph()); var classResource = model.createResource(EXISTING_CLASS_URI); assertThat( @@ -551,19 +525,20 @@ void deleteBase_twoClassExists_deletesClassBaseOtherClassUntouched() { .isFalse(); // isTrue assertThat( - testGraph.contains( - NodeFactory.createURI(OTHER_CLASS_URI), - RDF.type.asNode(), - RDFS.Class.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(OTHER_CLASS_URI), + RDF.type.asNode(), + RDFS.Class.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(OTHER_CLASS_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(OTHER_CLASS_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(OTHER_CLASS_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang( + OTHER_CLASS_LABEL, "en"))) .isTrue(); - } finally { - testGraph.end(); } } } diff --git a/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/enumentries/CIMUpdatesEnumEntriesTest.java b/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/enumentries/CIMUpdatesEnumEntriesTest.java index 778984a2..0ada2846 100644 --- a/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/enumentries/CIMUpdatesEnumEntriesTest.java +++ b/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/enumentries/CIMUpdatesEnumEntriesTest.java @@ -22,7 +22,7 @@ import org.apache.jena.graph.Node; import org.apache.jena.graph.NodeFactory; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; import org.junit.jupiter.api.BeforeAll; @@ -85,22 +85,22 @@ void insertEnumEntry_emptyGraph_addsEnumEntryRequired() { enumEntryRequired)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { assertThat( - testGraph.contains( - NodeFactory.createURI(ENUM_ENTRY_URI), - RDF.type.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ENUM_ENTRY_URI), + RDF.type.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ENUM_ENTRY_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(ENUM_ENTRY_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ENUM_ENTRY_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang( + ENUM_ENTRY_LABEL, "en"))) .isTrue(); - } finally { - testGraph.end(); } } @@ -118,36 +118,38 @@ void insertEnumEntry_emptyGraph_addsEnumEntryOptional() { enumEntryOptional)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { assertThat( - testGraph.contains( - NodeFactory.createURI(ENUM_ENTRY_URI), - RDF.type.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ENUM_ENTRY_URI), + RDF.type.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ENUM_ENTRY_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(ENUM_ENTRY_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ENUM_ENTRY_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang( + ENUM_ENTRY_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ENUM_ENTRY_URI), - RDFS.comment.asNode(), - new RDFSComment(COMMENT, new URI(COMMENT_FORMAT)) - .asTypedLiteral() - .asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ENUM_ENTRY_URI), + RDFS.comment.asNode(), + new RDFSComment(COMMENT, new URI(COMMENT_FORMAT)) + .asTypedLiteral() + .asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ENUM_ENTRY_URI), - CIMS.stereotype.asNode(), - CIMStereotypes.enumLiteral.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ENUM_ENTRY_URI), + CIMS.stereotype.asNode(), + CIMStereotypes.enumLiteral.asNode())) .isTrue(); - } finally { - testGraph.end(); } } @@ -186,22 +188,22 @@ void insertEnumEntry_enumEntryExistsWithRequired_doesNothing() { enumEntryRequired)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { assertThat( - testGraph.contains( - NodeFactory.createURI(ENUM_ENTRY_URI), - RDF.type.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ENUM_ENTRY_URI), + RDF.type.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ENUM_ENTRY_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(ENUM_ENTRY_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ENUM_ENTRY_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang( + ENUM_ENTRY_LABEL, "en"))) .isTrue(); - } finally { - testGraph.end(); } } @@ -221,36 +223,38 @@ void insertEnumEntry_enumEntryExistsWithRequired_addsOptionals() { enumEntryOptional)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { assertThat( - testGraph.contains( - NodeFactory.createURI(ENUM_ENTRY_URI), - RDF.type.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ENUM_ENTRY_URI), + RDF.type.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ENUM_ENTRY_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(ENUM_ENTRY_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ENUM_ENTRY_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang( + ENUM_ENTRY_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ENUM_ENTRY_URI), - RDFS.comment.asNode(), - new RDFSComment(COMMENT, new URI(COMMENT_FORMAT)) - .asTypedLiteral() - .asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ENUM_ENTRY_URI), + RDFS.comment.asNode(), + new RDFSComment(COMMENT, new URI(COMMENT_FORMAT)) + .asTypedLiteral() + .asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ENUM_ENTRY_URI), - CIMS.stereotype.asNode(), - CIMStereotypes.enumLiteral.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ENUM_ENTRY_URI), + CIMS.stereotype.asNode(), + CIMStereotypes.enumLiteral.asNode())) .isTrue(); - } finally { - testGraph.end(); } } @@ -276,36 +280,38 @@ void insertEnumEntry_emptyGraphInvalidStereotypeGiven_insertsWithCorrectStereoty newEnumEntry)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { assertThat( - testGraph.contains( - NodeFactory.createURI(ENUM_ENTRY_URI), - RDF.type.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ENUM_ENTRY_URI), + RDF.type.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ENUM_ENTRY_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(ENUM_ENTRY_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ENUM_ENTRY_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang( + ENUM_ENTRY_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ENUM_ENTRY_URI), - RDFS.comment.asNode(), - new RDFSComment(COMMENT, new URI(COMMENT_FORMAT)) - .asTypedLiteral() - .asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ENUM_ENTRY_URI), + RDFS.comment.asNode(), + new RDFSComment(COMMENT, new URI(COMMENT_FORMAT)) + .asTypedLiteral() + .asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ENUM_ENTRY_URI), - CIMS.stereotype.asNode(), - CIMStereotypes.enumLiteral.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ENUM_ENTRY_URI), + CIMS.stereotype.asNode(), + CIMStereotypes.enumLiteral.asNode())) .isTrue(); - } finally { - testGraph.end(); } } } @@ -324,35 +330,36 @@ void replaceEnumEntries_enumClassAndEntriesExist_replacesEnumEntries() { CIMUpdates.replaceEnumEntries( databasePort.getPrefixMapping(DATASET_NAME), GRAPH_URI, - MY_UUID.toString(), + MY_UUID, List.of(enumEntryRequired)) .build()); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isFalse assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_ENUM_ENTRY_URI), - Node.ANY, - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_ENUM_ENTRY_URI), + Node.ANY, + Node.ANY)) .isFalse(); // isTrue assertThat( - testGraph.contains( - NodeFactory.createURI(ENUM_ENTRY_URI), - RDF.type.asNode(), - NodeFactory.createURI(CLASS_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ENUM_ENTRY_URI), + RDF.type.asNode(), + NodeFactory.createURI(CLASS_URI))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(ENUM_ENTRY_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(ENUM_ENTRY_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(ENUM_ENTRY_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang( + ENUM_ENTRY_LABEL, "en"))) .isTrue(); - } finally { - testGraph.end(); } } } diff --git a/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/packages/CIMUpdatesPackagesTest.java b/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/packages/CIMUpdatesPackagesTest.java index d27c9e12..86a87ac2 100644 --- a/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/packages/CIMUpdatesPackagesTest.java +++ b/backend/src/test/java/org/rdfarchitect/cim/data/queries/update/cimupdates/packages/CIMUpdatesPackagesTest.java @@ -22,7 +22,7 @@ import org.apache.jena.graph.Node; import org.apache.jena.graph.NodeFactory; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; import org.junit.jupiter.api.BeforeAll; @@ -85,48 +85,52 @@ void replacePackage_packageAndClassExists_replacesPackage() { otherPackage)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { // isFalse assertThat( - testGraph.contains( - NodeFactory.createURI(PACKAGE_URI), - RDF.type.asNode(), - CIMS.classCategory.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PACKAGE_URI), + RDF.type.asNode(), + CIMS.classCategory.asNode())) .isFalse(); assertThat( - testGraph.contains( - NodeFactory.createURI(PACKAGE_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(PACKAGE_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PACKAGE_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang(PACKAGE_LABEL, "en"))) .isFalse(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_CLASS_URI), - CIMS.belongsToCategory.asNode(), - NodeFactory.createURI(PACKAGE_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_CLASS_URI), + CIMS.belongsToCategory.asNode(), + NodeFactory.createURI(PACKAGE_URI))) .isFalse(); // isTrue assertThat( - testGraph.contains( - NodeFactory.createURI(OTHER_PACKAGE_URI), - RDF.type.asNode(), - CIMS.classCategory.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(OTHER_PACKAGE_URI), + RDF.type.asNode(), + CIMS.classCategory.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(OTHER_PACKAGE_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(OTHER_PACKAGE_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(OTHER_PACKAGE_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang( + OTHER_PACKAGE_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_CLASS_URI), - CIMS.belongsToCategory.asNode(), - NodeFactory.createURI(OTHER_PACKAGE_URI))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_CLASS_URI), + CIMS.belongsToCategory.asNode(), + NodeFactory.createURI(OTHER_PACKAGE_URI))) .isTrue(); - } finally { - testGraph.end(); } } } @@ -148,22 +152,21 @@ void insertPackage_emptyGraph_addsPackageRequired() { packageRequired)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { assertThat( - testGraph.contains( - NodeFactory.createURI(PACKAGE_URI), - RDF.type.asNode(), - CIMS.classCategory.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PACKAGE_URI), + RDF.type.asNode(), + CIMS.classCategory.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(PACKAGE_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(PACKAGE_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PACKAGE_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang(PACKAGE_LABEL, "en"))) .isTrue(); - } finally { - testGraph.end(); } } @@ -181,30 +184,30 @@ void insertPackage_emptyGraph_addsPackageOptional() { packageOptional)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { assertThat( - testGraph.contains( - NodeFactory.createURI(PACKAGE_URI), - RDF.type.asNode(), - CIMS.classCategory.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PACKAGE_URI), + RDF.type.asNode(), + CIMS.classCategory.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(PACKAGE_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(PACKAGE_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PACKAGE_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang(PACKAGE_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(PACKAGE_URI), - RDFS.comment.asNode(), - new RDFSComment(COMMENT, new URI(COMMENT_FORMAT)) - .asTypedLiteral() - .asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PACKAGE_URI), + RDFS.comment.asNode(), + new RDFSComment(COMMENT, new URI(COMMENT_FORMAT)) + .asTypedLiteral() + .asNode())) .isTrue(); - } finally { - testGraph.end(); } } @@ -252,16 +255,14 @@ void insertPackage_classWithSameIriExists_throwsException() { .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("class with the same IRI"); - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { assertThat( - testGraph.contains( - NodeFactory.createURI(EXISTING_CLASS_URI), - RDF.type.asNode(), - CIMS.classCategory.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(EXISTING_CLASS_URI), + RDF.type.asNode(), + CIMS.classCategory.asNode())) .isFalse(); - } finally { - testGraph.end(); } } @@ -280,22 +281,21 @@ void insertPackage_packageAlreadyExistsWithoutComment_doesNothing() { packageRequired)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { assertThat( - testGraph.contains( - NodeFactory.createURI(PACKAGE_URI), - RDF.type.asNode(), - CIMS.classCategory.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PACKAGE_URI), + RDF.type.asNode(), + CIMS.classCategory.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(PACKAGE_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(PACKAGE_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PACKAGE_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang(PACKAGE_LABEL, "en"))) .isTrue(); - } finally { - testGraph.end(); } } @@ -314,30 +314,30 @@ void insertPackage_packageAlreadyExistsWithoutCommentAddWithComment_addsComment( packageOptional)); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { assertThat( - testGraph.contains( - NodeFactory.createURI(PACKAGE_URI), - RDF.type.asNode(), - CIMS.classCategory.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PACKAGE_URI), + RDF.type.asNode(), + CIMS.classCategory.asNode())) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(PACKAGE_URI), - RDFS.label.asNode(), - NodeFactory.createLiteralLang(PACKAGE_LABEL, "en"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PACKAGE_URI), + RDFS.label.asNode(), + NodeFactory.createLiteralLang(PACKAGE_LABEL, "en"))) .isTrue(); assertThat( - testGraph.contains( - NodeFactory.createURI(PACKAGE_URI), - RDFS.comment.asNode(), - new RDFSComment(COMMENT, new URI(COMMENT_FORMAT)) - .asTypedLiteral() - .asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PACKAGE_URI), + RDFS.comment.asNode(), + new RDFSComment(COMMENT, new URI(COMMENT_FORMAT)) + .asTypedLiteral() + .asNode())) .isTrue(); - } finally { - testGraph.end(); } } } @@ -357,19 +357,17 @@ void deletePackage_packageExists_deletesPackage() { CIMUpdates.deletePackage( graph, databasePort.getPrefixMapping(DATASET_NAME), - packageRequired.getUuid().toString())); + packageRequired.getUuid())); // Assert - try { - testGraph.begin(TxnType.READ); + try (var ctx = testGraph.begin(ReadWrite.READ)) { assertThat( - testGraph.contains( - NodeFactory.createURI(PACKAGE_URI), - RDF.type.asNode(), - Node.ANY)) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PACKAGE_URI), + RDF.type.asNode(), + Node.ANY)) .isFalse(); - } finally { - testGraph.end(); } } } diff --git a/backend/src/test/java/org/rdfarchitect/database/inmemory/GraphWithContextCollectionTest.java b/backend/src/test/java/org/rdfarchitect/database/inmemory/GraphWithContextCollectionTest.java index 72a36203..755bf41e 100644 --- a/backend/src/test/java/org/rdfarchitect/database/inmemory/GraphWithContextCollectionTest.java +++ b/backend/src/test/java/org/rdfarchitect/database/inmemory/GraphWithContextCollectionTest.java @@ -22,7 +22,6 @@ import org.apache.jena.graph.Graph; import org.apache.jena.query.DatasetFactory; import org.apache.jena.query.ReadWrite; -import org.apache.jena.query.TxnType; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.shared.impl.PrefixMappingImpl; import org.apache.jena.sparql.graph.GraphFactory; @@ -30,9 +29,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; import org.rdfarchitect.rdf.TestRDFUtils; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; import java.util.List; import java.util.Map; @@ -116,12 +115,12 @@ void constructor_nonEmptyDataset_returnsCollectionWithGraphs() { // Assert assertThat(size).isEqualTo(3); for (String uri : graphUris) { - GraphRewindableWithUUIDs graph = collection.begin(uri, TxnType.READ); - assertThat(graph).isNotNull(); - assertThat(graph.find().toList()).hasSize(9); - assertThat(graph.transactionType()).isEqualTo(TxnType.READ); - assertThat(graph.transactionMode()).isEqualTo(ReadWrite.READ); - graph.end(); + var graphWithCtx = collection.getGraphWithContext(uri); + try (var ctx = graphWithCtx.begin(ReadWrite.READ)) { + assertThat(ctx).isNotNull(); + assertThat(ctx.getRdfGraph().find().toList()).hasSize(9); + assertThat(ctx.transactionMode()).isEqualTo(ReadWrite.READ); + } } } @@ -139,19 +138,20 @@ void constructor_defaultGraphOnlyDataset_returnCollectionWith1Graph() { // Act List graphUris = collection.listGraphUris(); int size = graphUris.size(); - GraphRewindableWithUUIDs graph = collection.begin(DEFAULT_GRAPH_NAME, TxnType.READ); // Assert assertThat(size).isEqualTo(1); - assertThat(graph).isNotNull(); - assertThat(graph.find().toList()).hasSize(3); - assertThat(graph.transactionType()).isEqualTo(TxnType.READ); - assertThat(graph.transactionMode()).isEqualTo(ReadWrite.READ); - graph.end(); + var graphWithCtx = collection.getGraphWithContext(DEFAULT_GRAPH_NAME); + try (var ctx = graphWithCtx.begin(ReadWrite.READ)) { + assertThat(ctx).isNotNull(); + assertThat(ctx.getRdfGraph().find().toList()).hasSize(3); + assertThat(ctx.transactionMode()).isEqualTo(ReadWrite.READ); + } } - @Test - void begin_existingGraphUri_returnsGraphRewindable() { + @ParameterizedTest + @EnumSource(ReadWrite.class) + void begin_existingGraphUri_returnsGraphRewindable(ReadWrite mode) { // Arrange GraphWithContextCollection collection = new GraphWithContextCollection(); @@ -159,16 +159,14 @@ void begin_existingGraphUri_returnsGraphRewindable() { collection.create("http://example.org/graph1", exampleGraphs.getFirst()); // Act - GraphRewindableWithUUIDs graph = - collection.begin("http://example.org/graph1", TxnType.READ); - - // Assert - assertThat(graph).isOfAnyClassIn(GraphRewindableWithUUIDs.class); - assertThat(graph.isInTransaction()).isTrue(); - assertThat(graph.isIsomorphicWith(exampleGraphs.get(1))).isTrue(); - assertThat(graph.transactionType()).isEqualTo(TxnType.READ); - assertThat(graph.transactionMode()).isEqualTo(ReadWrite.READ); - graph.end(); + var graphWithCtx = collection.getGraphWithContext("http://example.org/graph1"); + try (var ctx = graphWithCtx.begin(mode)) { + // Assert + assertThat(ctx).isNotNull(); + assertThat(ctx.isInTransaction()).isTrue(); + assertThat(ctx.getRdfGraph().isIsomorphicWith(exampleGraphs.get(1))).isTrue(); + assertThat(ctx.transactionMode()).isEqualTo(mode); + } } @Test @@ -177,15 +175,14 @@ void begin_nonExistingDefaultGraphUri_returnsEmptyDefaultGraph() { GraphWithContextCollection collection = new GraphWithContextCollection(); // Act - GraphRewindableWithUUIDs graph = collection.begin(DEFAULT_GRAPH_NAME, TxnType.READ); - - // Assert - assertThat(graph).isOfAnyClassIn(GraphRewindableWithUUIDs.class); - assertThat(graph.isInTransaction()).isTrue(); - assertThat(graph.find().toList()).isEmpty(); - assertThat(graph.transactionType()).isEqualTo(TxnType.READ); - assertThat(graph.transactionMode()).isEqualTo(ReadWrite.READ); - graph.end(); + var graphWithCtx = collection.getGraphWithContext(DEFAULT_GRAPH_NAME); + try (var ctx = graphWithCtx.begin(ReadWrite.READ)) { + // Assert + assertThat(ctx).isNotNull(); + assertThat(ctx.isInTransaction()).isTrue(); + assertThat(ctx.getRdfGraph().find().toList()).isEmpty(); + assertThat(ctx.transactionMode()).isEqualTo(ReadWrite.READ); + } } @Test @@ -195,7 +192,7 @@ void begin_nonExistingNamedGraphUri_throwsException() { // Act/Assert assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> collection.begin("http://example.org/nonexistent", TxnType.READ)) + .isThrownBy(() -> collection.getGraphWithContext("http://example.org/nonexistent")) .withMessage("Graph URI http://example.org/nonexistent does not exist."); } @@ -207,68 +204,59 @@ void begin_invalidUri_throwsException(String graphUri) { // Act/Assert assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> collection.begin(graphUri, TxnType.READ)) + .isThrownBy(() -> collection.getGraphWithContext(graphUri)) .withMessage("Graph Uri " + graphUri + " is not a valid URI"); } @Test - void begin_existingNamedGraphWithWrite_graphRewindableInWriteTransaction() { + void begin_readThenEndThenBeginWrite_allowsWriteAfterReadEnds() { // Arrange GraphWithContextCollection collection = new GraphWithContextCollection(); - exampleGraphs = List.of(createExampleGraph(), createExampleGraph()); - collection.create("http://example.org/graph1", exampleGraphs.getFirst()); - - // Act - GraphRewindableWithUUIDs graph = - collection.begin("http://example.org/graph1", TxnType.WRITE); - - // Assert - assertThat(graph).isOfAnyClassIn(GraphRewindableWithUUIDs.class); - assertThat(graph.isInTransaction()).isTrue(); - assertThat(graph.isIsomorphicWith(exampleGraphs.get(1))).isTrue(); - assertThat(graph.transactionType()).isEqualTo(TxnType.WRITE); - assertThat(graph.transactionMode()).isEqualTo(ReadWrite.WRITE); - graph.end(); - } - - @Test - void begin_existingNamedGraphWithReadAndEndTransaction_graphRewindableInReadPromote() { - // Arrange - GraphWithContextCollection collection = new GraphWithContextCollection(); - exampleGraphs = List.of(createExampleGraph(), createExampleGraph()); + exampleGraphs = List.of(createExampleGraph()); collection.create("http://example.org/graph1", exampleGraphs.getFirst()); + var graphWithCtx = collection.getGraphWithContext("http://example.org/graph1"); - // Act - GraphRewindableWithUUIDs graph = - collection.begin("http://example.org/graph1", TxnType.READ_PROMOTE); + // Act - begin READ, end it, then begin WRITE + try (var readCtx = graphWithCtx.begin(ReadWrite.READ)) { + assertThat(readCtx.getRdfGraph().find().toList()).hasSize(9); + } + try (var writeCtx = graphWithCtx.begin(ReadWrite.WRITE)) { + // Assert + assertThat(writeCtx).isNotNull(); + assertThat(writeCtx.isInTransaction()).isTrue(); + assertThat(writeCtx.transactionMode()).isEqualTo(ReadWrite.WRITE); + writeCtx.getRdfGraph().add(TestRDFUtils.triple("a a d")); + writeCtx.commit("add triple"); + } - // Assert - assertThat(graph).isOfAnyClassIn(GraphRewindableWithUUIDs.class); - assertThat(graph.isInTransaction()).isTrue(); - assertThat(graph.isIsomorphicWith(exampleGraphs.get(1))).isTrue(); - assertThat(graph.transactionType()).isEqualTo(TxnType.READ_PROMOTE); - assertThat(graph.transactionMode()).isEqualTo(ReadWrite.READ); - graph.end(); + // Verify the write persisted + try (var verifyCtx = graphWithCtx.begin(ReadWrite.READ)) { + assertThat(verifyCtx.getRdfGraph().find().toList()).hasSize(10); + } } @Test - void begin_existingNamedGraphWithReadCommitedPromote_graphRewindableInReadCommitedPromote() { + void begin_writeCommitThenRead_seesCommittedChanges() { // Arrange GraphWithContextCollection collection = new GraphWithContextCollection(); - exampleGraphs = List.of(createExampleGraph(), createExampleGraph()); + exampleGraphs = List.of(createExampleGraph()); collection.create("http://example.org/graph1", exampleGraphs.getFirst()); + var graphWithCtx = collection.getGraphWithContext("http://example.org/graph1"); - // Act - GraphRewindableWithUUIDs graph = - collection.begin("http://example.org/graph1", TxnType.READ_COMMITTED_PROMOTE); + // Act - begin WRITE, commit changes, end, then begin READ + try (var writeCtx = graphWithCtx.begin(ReadWrite.WRITE)) { + writeCtx.getRdfGraph().add(TestRDFUtils.triple("a a d")); + writeCtx.getRdfGraph().add(TestRDFUtils.triple("a a e")); + writeCtx.commit("add two triples"); + } - // Assert - assertThat(graph).isOfAnyClassIn(GraphRewindableWithUUIDs.class); - assertThat(graph.isInTransaction()).isTrue(); - assertThat(graph.isIsomorphicWith(exampleGraphs.get(1))).isTrue(); - assertThat(graph.transactionType()).isEqualTo(TxnType.READ_COMMITTED_PROMOTE); - assertThat(graph.transactionMode()).isEqualTo(ReadWrite.READ); - graph.end(); + // Assert - READ sees the committed changes + try (var readCtx = graphWithCtx.begin(ReadWrite.READ)) { + assertThat(readCtx).isNotNull(); + assertThat(readCtx.isInTransaction()).isTrue(); + assertThat(readCtx.transactionMode()).isEqualTo(ReadWrite.READ); + assertThat(readCtx.getRdfGraph().find().toList()).hasSize(11); + } } @ParameterizedTest @@ -286,15 +274,14 @@ void create_validName_returnsGraphRewindable(String graphUri) { // Act collection.create(graphUri, exampleGraphs.getFirst()); - GraphRewindableWithUUIDs graph = collection.begin(graphUri, TxnType.READ); - - // Assert - assertThat(graph).isOfAnyClassIn(GraphRewindableWithUUIDs.class); - assertThat(graph.isInTransaction()).isTrue(); - assertThat(graph.isIsomorphicWith(exampleGraphs.get(1))).isTrue(); - assertThat(graph.transactionType()).isEqualTo(TxnType.READ); - assertThat(graph.transactionMode()).isEqualTo(ReadWrite.READ); - graph.end(); + var graphWithCtx = collection.getGraphWithContext(graphUri); + try (var ctx = graphWithCtx.begin(ReadWrite.READ)) { + // Assert + assertThat(ctx).isNotNull(); + assertThat(ctx.isInTransaction()).isTrue(); + assertThat(ctx.getRdfGraph().isIsomorphicWith(exampleGraphs.get(1))).isTrue(); + assertThat(ctx.transactionMode()).isEqualTo(ReadWrite.READ); + } } @ParameterizedTest @@ -336,43 +323,6 @@ void remove_nonExistingGraphUri_doesNothing() { assertThat(collection.listGraphUris()).isEmpty(); } - @Test - void containsGraph_existingGraphUri_returnsTrue() { - // Arrange - GraphWithContextCollection collection = new GraphWithContextCollection(); - exampleGraphs = List.of(createExampleGraph()); - collection.create("http://example.org/graph1", exampleGraphs.getFirst()); - - // Act - boolean containsGraph = collection.containsGraph("http://example.org/graph1"); - - // Assert - assertThat(containsGraph).isTrue(); - } - - @Test - void containsGraph_nonExistingGraphUri_returnsFalse() { - // Arrange - GraphWithContextCollection collection = new GraphWithContextCollection(); - - // Act - boolean containsGraph = collection.containsGraph("http://example.org/nonexistent"); - - // Assert - assertThat(containsGraph).isFalse(); - } - - @ParameterizedTest - @ValueSource(strings = {"", "foo", "bar", "otherInvalidUri"}) - void containsGraph_invalidGraphUri_throwsException(String graphUri) { - // Arrange - GraphWithContextCollection collection = new GraphWithContextCollection(); - - // Act/Assert - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> collection.containsGraph(graphUri)); - } - @Test void listGraphUris_emptyCollection_returnsEmptyList() { // Arrange @@ -625,89 +575,4 @@ void setPrefixMapping_nonEmptyPrefixMappingIntoNonEmptyGraph_overridesPrefixes() "rdfs", "http://www.w3.org/2000/01/rdf-schema#", "owl", "http://www.w3.org/2002/07/owl#")); } - - @Test - void undo_existingGraphUri_undoesLastChange() { - // Arrange - GraphWithContextCollection collection = new GraphWithContextCollection(); - exampleGraphs = List.of(createExampleGraph()); - collection.create("http://example.org/graph1", exampleGraphs.getFirst()); - GraphRewindableWithUUIDs graph = - collection.begin("http://example.org/graph1", TxnType.WRITE); - graph.add(TestRDFUtils.triple("a a d")); - graph.commit(); - graph.end(); - - // Act - collection.undo("http://example.org/graph1"); - - // Assert - graph = collection.begin("http://example.org/graph1", TxnType.READ); - assertThat(graph.find().toList()).hasSize(9); - graph.end(); - } - - @Test - void undo_nonExistingGraphUri_throwsException() { - // Arrange - GraphWithContextCollection collection = new GraphWithContextCollection(); - - // Act/Assert - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> collection.undo("http://example.org/nonexistent")); - } - - @ParameterizedTest - @ValueSource(strings = {"", "foo", "bar", "otherInvalidUri"}) - void undo_invalidGraphUri_throwsException(String graphUri) { - // Arrange - GraphWithContextCollection collection = new GraphWithContextCollection(); - - // Act/Assert - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> collection.undo(graphUri)); - } - - @Test - void redo_existingGraphUri_restoresLastChange() { - // Arrange - GraphWithContextCollection collection = new GraphWithContextCollection(); - exampleGraphs = List.of(createExampleGraph()); - collection.create("http://example.org/graph1", exampleGraphs.getFirst()); - GraphRewindableWithUUIDs graph = - collection.begin("http://example.org/graph1", TxnType.WRITE); - graph.add(TestRDFUtils.triple("a a d")); - graph.commit(); - graph.end(); - collection.undo("http://example.org/graph1"); - - // Act - collection.redo("http://example.org/graph1"); - - // Assert - graph = collection.begin("http://example.org/graph1", TxnType.READ); - assertThat(graph.find().toList()).hasSize(10); - graph.end(); - } - - @Test - void redo_nonExistingGraphUri_throwsException() { - // Arrange - GraphWithContextCollection collection = new GraphWithContextCollection(); - - // Act/Assert - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> collection.redo("http://example.org/nonexistent")); - } - - @ParameterizedTest - @ValueSource(strings = {"", "foo", "bar", "otherInvalidUri"}) - void redo_invalidGraphUri_throwsException(String graphUri) { - // Arrange - GraphWithContextCollection collection = new GraphWithContextCollection(); - - // Act/Assert - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> collection.redo(graphUri)); - } } diff --git a/backend/src/test/java/org/rdfarchitect/database/inmemory/GraphWithContextTransactionalTest.java b/backend/src/test/java/org/rdfarchitect/database/inmemory/GraphWithContextTransactionalTest.java new file mode 100644 index 00000000..42e62e2b --- /dev/null +++ b/backend/src/test/java/org/rdfarchitect/database/inmemory/GraphWithContextTransactionalTest.java @@ -0,0 +1,679 @@ +/* + * 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.database.inmemory; + +import static org.assertj.core.api.Assertions.*; + +import org.apache.jena.graph.Triple; +import org.apache.jena.query.ReadWrite; +import org.apache.jena.sparql.graph.GraphFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.rdfarchitect.exception.graph.GraphNotInATransactionException; +import org.rdfarchitect.exception.graph.GraphTransactionException; +import org.rdfarchitect.exception.graph.GraphVersionControlException; +import org.rdfarchitect.rdf.TestRDFUtils; + +import java.util.UUID; + +class GraphWithContextTransactionalTest { + + private GraphWithContextTransactional ctx; + private Triple triple; + private Triple triple2; + + @BeforeEach + void setUp() { + ctx = new GraphWithContextTransactional(GraphFactory.createDefaultGraph()); + triple = TestRDFUtils.triple("s p o"); + triple2 = TestRDFUtils.triple("s2 p2 o2"); + } + + @AfterEach + void tearDown() { + if (ctx.isInTransaction()) { + ctx.end(); + } + } + + // ------------------------------------------------------------------------- + // begin + // ------------------------------------------------------------------------- + + @Test + void begin_read_isInReadTransaction() { + ctx.begin(ReadWrite.READ); + + assertThat(ctx.isInTransaction()).isTrue(); + assertThat(ctx.transactionMode()).isEqualTo(ReadWrite.READ); + } + + @Test + void begin_write_isInWriteTransaction() { + ctx.begin(ReadWrite.WRITE); + + assertThat(ctx.isInTransaction()).isTrue(); + assertThat(ctx.transactionMode()).isEqualTo(ReadWrite.WRITE); + } + + @Test + void begin_returnsThis() { + var result = ctx.begin(ReadWrite.WRITE); + + assertThat(result).isSameAs(ctx); + } + + @Test + void begin_whileAlreadyInTransaction_throwsException() { + ctx.begin(ReadWrite.WRITE); + + assertThatExceptionOfType(GraphTransactionException.class) + .isThrownBy(() -> ctx.begin(ReadWrite.WRITE)); + } + + // ------------------------------------------------------------------------- + // isInTransaction / transactionMode + // ------------------------------------------------------------------------- + + @Test + void isInTransaction_initialState_returnsFalse() { + assertThat(ctx.isInTransaction()).isFalse(); + } + + @Test + void transactionMode_whenNotInTransaction_throwsException() { + assertThatExceptionOfType(GraphNotInATransactionException.class) + .isThrownBy(() -> ctx.transactionMode()); + } + + // ------------------------------------------------------------------------- + // end / close + // ------------------------------------------------------------------------- + + @Test + void end_afterReadTransaction_isNoLongerInTransaction() { + ctx.begin(ReadWrite.READ); + + ctx.end(); + + assertThat(ctx.isInTransaction()).isFalse(); + } + + @Test + void end_afterWriteCommit_isNoLongerInTransaction() { + ctx.begin(ReadWrite.WRITE); + ctx.commit(); + + ctx.end(); + + assertThat(ctx.isInTransaction()).isFalse(); + } + + @Test + void end_whenNotInTransaction_throwsException() { + assertThatExceptionOfType(GraphNotInATransactionException.class) + .isThrownBy(() -> ctx.end()); + } + + @Test + void end_withUncommittedWriteChanges_abortsAutomatically() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + // no commit — close() via try-with-resources triggers auto-abort + } + + try (var _ = ctx.begin(ReadWrite.READ)) { + assertThat(ctx.getRdfGraph().contains(triple)).isFalse(); + } + } + + @Test + void close_whenInTransaction_endsTransaction() { + ctx.begin(ReadWrite.READ); + + ctx.close(); + + assertThat(ctx.isInTransaction()).isFalse(); + } + + @Test + void close_whenNotInTransaction_doesNotThrow() { + assertThatNoException().isThrownBy(() -> ctx.close()); + } + + // ------------------------------------------------------------------------- + // commit + // ------------------------------------------------------------------------- + + @Test + void commit_whenNotInTransaction_throwsException() { + assertThatExceptionOfType(GraphNotInATransactionException.class) + .isThrownBy(() -> ctx.commit()); + } + + @Test + void commit_duringReadTransaction_throwsException() { + ctx.begin(ReadWrite.READ); + + assertThatExceptionOfType(GraphTransactionException.class).isThrownBy(() -> ctx.commit()); + } + + @Test + void commit_writtenTripleIsVisibleInSubsequentReadTransaction() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit(); + } + + try (var _ = ctx.begin(ReadWrite.READ)) { + assertThat(ctx.getRdfGraph().contains(triple)).isTrue(); + } + } + + @Test + void commit_withoutMessage_doesNotAddToChangeLog() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit(); + } + + try (var _ = ctx.begin(ReadWrite.READ)) { + assertThat(ctx.getChangeLog().getUndoHistory()).size().isEqualTo(1); + assertThat(ctx.canUndo()).isFalse(); + } + } + + @Test + void commitWithMessage_addsEntryToChangeLog() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("my change"); + } + + try (var _ = ctx.begin(ReadWrite.READ)) { + var history = ctx.getChangeLog().getUndoHistory(); + assertThat(history).hasSize(2); + assertThat(history.getFirst().getMessage()).isEqualTo("my change"); + assertThat(history.get(1).getMessage()).isEqualTo("imported graph"); + } + } + + @Test + void commitWithMessage_enablesUndo() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("step 1"); + } + + try (var _ = ctx.begin(ReadWrite.READ)) { + assertThat(ctx.canUndo()).isTrue(); + } + } + + @Test + void commitWithMessage_multipleCommits_historyIsNewestFirst() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("first"); + } + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple2); + ctx.commit("second"); + } + + try (var _ = ctx.begin(ReadWrite.READ)) { + var history = ctx.getChangeLog().getUndoHistory(); + assertThat(history).hasSize(3); + assertThat(history.getFirst().getMessage()).isEqualTo("second"); + assertThat(history.get(1).getMessage()).isEqualTo("first"); + assertThat(history.get(2).getMessage()).isEqualTo("imported graph"); + } + } + + // ------------------------------------------------------------------------- + // abort + // ------------------------------------------------------------------------- + + @Test + void abort_whenNotInTransaction_throwsException() { + assertThatExceptionOfType(GraphNotInATransactionException.class) + .isThrownBy(() -> ctx.abort()); + } + + @Test + void abort_duringReadTransaction_throwsException() { + ctx.begin(ReadWrite.READ); + + assertThatExceptionOfType(GraphTransactionException.class).isThrownBy(() -> ctx.abort()); + } + + @Test + void abort_writtenTripleIsNotVisibleAfterAbort() { + ctx.begin(ReadWrite.WRITE); + ctx.getRdfGraph().add(triple); + ctx.abort(); + ctx.end(); + + try (var _ = ctx.begin(ReadWrite.READ)) { + assertThat(ctx.getRdfGraph().contains(triple)).isFalse(); + } + } + + // ------------------------------------------------------------------------- + // canUndo / canRedo + // ------------------------------------------------------------------------- + + @Test + void canUndo_initialState_returnsFalse() { + try (var _ = ctx.begin(ReadWrite.READ)) { + assertThat(ctx.canUndo()).isFalse(); + } + } + + @Test + void canRedo_initialState_returnsFalse() { + try (var _ = ctx.begin(ReadWrite.READ)) { + assertThat(ctx.canRedo()).isFalse(); + } + } + + // ------------------------------------------------------------------------- + // undo + // ------------------------------------------------------------------------- + + @Test + void undo_whenNothingToUndo_throwsException() { + assertThatExceptionOfType(GraphVersionControlException.class).isThrownBy(() -> ctx.undo()); + } + + @Test + void undo_whileInTransaction_throwsException() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("step 1"); + } + ctx.begin(ReadWrite.READ); + + assertThatExceptionOfType(GraphTransactionException.class).isThrownBy(() -> ctx.undo()); + } + + @Test + void undo_revertsLastCommit() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("added triple"); + } + + ctx.undo(); + + try (var _ = ctx.begin(ReadWrite.READ)) { + assertThat(ctx.getRdfGraph().contains(triple)).isFalse(); + } + } + + @Test + void undo_enablesRedo() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("step 1"); + } + + ctx.undo(); + + try (var _ = ctx.begin(ReadWrite.READ)) { + assertThat(ctx.canRedo()).isTrue(); + } + } + + @Test + void undo_singleCommit_disablesUndo() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("step 1"); + } + + ctx.undo(); + + try (var _ = ctx.begin(ReadWrite.READ)) { + assertThat(ctx.canUndo()).isFalse(); + } + } + + // ------------------------------------------------------------------------- + // redo + // ------------------------------------------------------------------------- + + @Test + void redo_whenNothingToRedo_throwsException() { + assertThatExceptionOfType(GraphVersionControlException.class).isThrownBy(() -> ctx.redo()); + } + + @Test + void redo_whileInTransaction_throwsException() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("step 1"); + } + ctx.undo(); + ctx.begin(ReadWrite.READ); + + assertThatExceptionOfType(GraphTransactionException.class).isThrownBy(() -> ctx.redo()); + } + + @Test + void redo_reappliesUndoneCommit() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("added triple"); + } + ctx.undo(); + + ctx.redo(); + + try (var _ = ctx.begin(ReadWrite.READ)) { + assertThat(ctx.getRdfGraph().contains(triple)).isTrue(); + } + } + + @Test + void redo_enablesUndo() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("step 1"); + } + ctx.undo(); + + ctx.redo(); + + try (var _ = ctx.begin(ReadWrite.READ)) { + assertThat(ctx.canUndo()).isTrue(); + } + } + + @Test + void redo_disablesRedo() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("step 1"); + } + ctx.undo(); + + ctx.redo(); + + try (var _ = ctx.begin(ReadWrite.READ)) { + assertThat(ctx.canRedo()).isFalse(); + } + } + + // ------------------------------------------------------------------------- + // undo return value + // ------------------------------------------------------------------------- + + @Test + void undo_returnsUndoneChangeLogEntry() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("added triple"); + } + + var entry = ctx.undo(); + + assertThat(entry).isNotNull(); + assertThat(entry.getMessage()).isEqualTo("added triple"); + } + + @Test + void undo_multipleCommits_returnsNewestEntry() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("first"); + } + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple2); + ctx.commit("second"); + } + + var entry = ctx.undo(); + + assertThat(entry.getMessage()).isEqualTo("second"); + } + + @Test + void undo_twice_returnsEntriesInReverseOrder() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("first"); + } + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple2); + ctx.commit("second"); + } + + var second = ctx.undo(); + var first = ctx.undo(); + + assertThat(second.getMessage()).isEqualTo("second"); + assertThat(first.getMessage()).isEqualTo("first"); + } + + // ------------------------------------------------------------------------- + // redo return value + // ------------------------------------------------------------------------- + + @Test + void redo_returnsRedoneChangeLogEntry() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("added triple"); + } + ctx.undo(); + + var entry = ctx.redo(); + + assertThat(entry).isNotNull(); + assertThat(entry.getMessage()).isEqualTo("added triple"); + } + + @Test + void redo_afterMultipleUndos_returnsEntriesInCorrectOrder() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("first"); + } + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple2); + ctx.commit("second"); + } + ctx.undo(); + ctx.undo(); + + var first = ctx.redo(); + var second = ctx.redo(); + + assertThat(first.getMessage()).isEqualTo("first"); + assertThat(second.getMessage()).isEqualTo("second"); + } + + @Test + void undo_thenRedo_returnsSameEntry() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("my change"); + } + + var undone = ctx.undo(); + var redone = ctx.redo(); + + assertThat(undone.getChangeId()).isEqualTo(redone.getChangeId()); + } + + // ------------------------------------------------------------------------- + // restoreToVersion + // ------------------------------------------------------------------------- + + @Test + void restoreToVersion_whileInTransaction_throwsException() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("step 1"); + } + ctx.begin(ReadWrite.READ); + + assertThatExceptionOfType(GraphTransactionException.class) + .isThrownBy(() -> ctx.restoreToVersion(UUID.randomUUID())); + } + + @Test + void restoreToVersion_unknownVersion_throwsException() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("step 1"); + } + + assertThatExceptionOfType(GraphVersionControlException.class) + .isThrownBy(() -> ctx.restoreToVersion(UUID.randomUUID())); + } + + @Test + void restoreToVersion_restoresToTargetVersion() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("first"); + } + + UUID targetVersionId; + try (var _ = ctx.begin(ReadWrite.READ)) { + targetVersionId = ctx.getChangeLog().getUndoHistory().getFirst().getChangeId(); + } + + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple2); + ctx.commit("second"); + } + + ctx.restoreToVersion(targetVersionId); + + try (var _ = ctx.begin(ReadWrite.READ)) { + assertThat(ctx.getRdfGraph().contains(triple)).isTrue(); + assertThat(ctx.getRdfGraph().contains(triple2)).isFalse(); + } + } + + @Test + void restoreToVersion_enablesRedo() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("first"); + } + + UUID targetVersionId; + try (var _ = ctx.begin(ReadWrite.READ)) { + targetVersionId = ctx.getChangeLog().getUndoHistory().getFirst().getChangeId(); + } + + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple2); + ctx.commit("second"); + } + + ctx.restoreToVersion(targetVersionId); + + try (var _ = ctx.begin(ReadWrite.READ)) { + assertThat(ctx.canRedo()).isTrue(); + } + } + + @Test + void restoreToVersion_multipleVersionsBack() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("first"); + } + + UUID targetVersionId; + try (var _ = ctx.begin(ReadWrite.READ)) { + targetVersionId = ctx.getChangeLog().getUndoHistory().getFirst().getChangeId(); + } + + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple2); + ctx.commit("second"); + } + + var triple3 = TestRDFUtils.triple("s3 p3 o3"); + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple3); + ctx.commit("third"); + } + + ctx.restoreToVersion(targetVersionId); + + try (var _ = ctx.begin(ReadWrite.READ)) { + assertThat(ctx.getRdfGraph().contains(triple)).isTrue(); + assertThat(ctx.getRdfGraph().contains(triple2)).isFalse(); + assertThat(ctx.getRdfGraph().contains(triple3)).isFalse(); + } + } + + @Test + void restoreToVersion_currentVersion_doesNothing() { + try (var _ = ctx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph().add(triple); + ctx.commit("first"); + } + + UUID currentVersionId; + try (var _ = ctx.begin(ReadWrite.READ)) { + currentVersionId = ctx.getChangeLog().getUndoHistory().getFirst().getChangeId(); + } + + ctx.restoreToVersion(currentVersionId); + + try (var _ = ctx.begin(ReadWrite.READ)) { + assertThat(ctx.getRdfGraph().contains(triple)).isTrue(); + assertThat(ctx.canRedo()).isFalse(); + } + } + + // ------------------------------------------------------------------------- + // getters + // ------------------------------------------------------------------------- + + @Test + void getRdfGraph_returnsNonNull() { + assertThat(ctx.getRdfGraph()).isNotNull(); + } + + @Test + void getDiagramLayout_returnsNonNull() { + assertThat(ctx.getDiagramLayout()).isNotNull(); + } + + @Test + void getCustomSHACL_returnsNonNull() { + assertThat(ctx.getCustomSHACL()).isNotNull(); + } + + @Test + void getChangeLog_initialState_isEmpty() { + try (var _ = ctx.begin(ReadWrite.READ)) { + assertThat(ctx.getChangeLog().getUndoHistory()).size().isEqualTo(1); + } + } +} diff --git a/backend/src/test/java/org/rdfarchitect/database/inmemory/InMemoryDatabaseAdapterTest.java b/backend/src/test/java/org/rdfarchitect/database/inmemory/InMemoryDatabaseAdapterTest.java index 5875a816..e39fc1b1 100644 --- a/backend/src/test/java/org/rdfarchitect/database/inmemory/InMemoryDatabaseAdapterTest.java +++ b/backend/src/test/java/org/rdfarchitect/database/inmemory/InMemoryDatabaseAdapterTest.java @@ -17,17 +17,12 @@ package org.rdfarchitect.database.inmemory; -import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -import org.apache.jena.shared.PrefixMapping; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.rdfarchitect.config.SchemaConfig; import org.rdfarchitect.database.GraphIdentifier; -import java.util.List; - class InMemoryDatabaseAdapterTest { private InMemoryDatabase database; @@ -36,40 +31,24 @@ class InMemoryDatabaseAdapterTest { @BeforeEach void setUp() { database = mock(InMemoryDatabase.class); - adapter = new InMemoryDatabaseAdapter(database, new SchemaConfig()); + adapter = new InMemoryDatabaseAdapter(database); } @Test - void createEmptyGraph_newDataset_addsDefaultPrefixMapping() { + void createEmptyGraph_newDataset_delegatesToDatabase() { var graphIdentifier = new GraphIdentifier("new-dataset", "http://example.com/graph"); - when(database.listDatasets()).thenReturn(List.of("existing-dataset")); adapter.createEmptyGraph(graphIdentifier); - verify(database).create(eq(graphIdentifier), any()); - verify(database).enableEditing("new-dataset"); - verify(database) - .setPrefixMapping( - eq("new-dataset"), - argThat( - prefixMapping -> - prefixMapping - .getNsPrefixMap() - .entrySet() - .containsAll( - PrefixMapping.Standard.getNsPrefixMap() - .entrySet()))); + verify(database).createEmptyGraph(graphIdentifier); } @Test - void createEmptyGraph_existingDataset_doesNotAddStandardPrefixMapping() { + void createEmptyGraph_existingDataset_delegatesToDatabase() { var graphIdentifier = new GraphIdentifier("existing-dataset", "http://example.com/graph"); - when(database.listDatasets()).thenReturn(List.of("existing-dataset")); adapter.createEmptyGraph(graphIdentifier); - verify(database).create(eq(graphIdentifier), any()); - verify(database, never()).enableEditing(anyString()); - verify(database, never()).setPrefixMapping(anyString(), any(PrefixMapping.class)); + verify(database).createEmptyGraph(graphIdentifier); } } diff --git a/backend/src/test/java/org/rdfarchitect/database/inmemory/InMemorySparqlExecutorTest.java b/backend/src/test/java/org/rdfarchitect/database/inmemory/InMemorySparqlExecutorTest.java index bbefe3e7..96f12faa 100644 --- a/backend/src/test/java/org/rdfarchitect/database/inmemory/InMemorySparqlExecutorTest.java +++ b/backend/src/test/java/org/rdfarchitect/database/inmemory/InMemorySparqlExecutorTest.java @@ -23,10 +23,14 @@ import org.apache.jena.arq.querybuilder.UpdateBuilder; import org.apache.jena.graph.Graph; import org.apache.jena.graph.Node; +import org.apache.jena.query.Query; +import org.apache.jena.query.QueryExecutionFactory; import org.apache.jena.query.QueryFactory; +import org.apache.jena.query.ResultSet; +import org.apache.jena.query.ResultSetFactory; import org.apache.jena.query.TxnType; import org.apache.jena.sparql.graph.GraphFactory; -import org.apache.jena.update.UpdateRequest; +import org.apache.jena.update.UpdateExecutionFactory; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -34,7 +38,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.rdf.TestRDFUtils; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; +import org.rdfarchitect.rdf.graph.wrapper.GraphRewindable; import java.util.List; import java.util.UUID; @@ -79,17 +83,30 @@ Graph createExampleGraph() { return graph; } + private ResultSet executeSingleQuery(GraphRewindable graph, Query query, String graphUri) { + graph.begin(TxnType.READ); + try { + var dataset = SessionDataStore.wrapGraphInDataset(graph, graphUri); + try (var queryExecution = QueryExecutionFactory.create(query, dataset)) { + var resultSet = queryExecution.execSelect(); + return ResultSetFactory.copyResults(resultSet); + } + } finally { + graph.end(); + } + } + @Test void executeSingleQuery_onDefaultWithValidAllQuery_shouldReturnAllResults() { // Arrange String graphUri = "default"; exampleGraphs = List.of(createExampleGraph()); - var graphRewindable = new GraphRewindableWithUUIDs(exampleGraphs.get(0), 20, 5); + var graphRewindable = new GraphRewindable(exampleGraphs.getFirst(), 20, 5); String queryString = "SELECT * WHERE { ?s ?p ?o }"; var query = QueryFactory.create(queryString); // Act - var resultSet = InMemorySparqlExecutor.executeSingleQuery(graphRewindable, query, graphUri); + var resultSet = executeSingleQuery(graphRewindable, query, graphUri); // Assert assertNotNull(resultSet); @@ -108,12 +125,12 @@ void executeSingleQuery_onDefaultWithValidAllQuery_shouldReturnAllResults() { void executeSingleQuery_onNamedWithValidAllQuery_shouldReturnAllResults(String graphUri) { // Arrange exampleGraphs = List.of(createExampleGraph()); - var graphRewindable = new GraphRewindableWithUUIDs(exampleGraphs.get(0), 20, 5); + var graphRewindable = new GraphRewindable(exampleGraphs.getFirst(), 20, 5); String queryString = "SELECT * FROM <" + graphUri + "> WHERE { ?s ?p ?o }"; var query = QueryFactory.create(queryString); // Act - var resultSet = InMemorySparqlExecutor.executeSingleQuery(graphRewindable, query, graphUri); + var resultSet = executeSingleQuery(graphRewindable, query, graphUri); // Assert assertNotNull(resultSet); @@ -132,12 +149,12 @@ void executeSingleQuery_onDefaultWithValidNoResultQuery_shouldReturnEmptyResultS // Arrange String graphUri = "default"; exampleGraphs = List.of(createExampleGraph()); - var graphRewindable = new GraphRewindableWithUUIDs(exampleGraphs.get(0), 20, 5); + var graphRewindable = new GraphRewindable(exampleGraphs.getFirst(), 20, 5); String queryString = "SELECT * WHERE { ?s ?p ?o . FILTER(?p = ) }"; var query = QueryFactory.create(queryString); // Act - var resultSet = InMemorySparqlExecutor.executeSingleQuery(graphRewindable, query, graphUri); + var resultSet = executeSingleQuery(graphRewindable, query, graphUri); // Assert assertNotNull(resultSet); @@ -151,7 +168,7 @@ void executeSingleQuery_onNamedWithValidNoResultQuery_shouldReturnEmptyResultSet // Arrange String graphUri = "http://example.org/graph"; exampleGraphs = List.of(createExampleGraph()); - var graphRewindable = new GraphRewindableWithUUIDs(exampleGraphs.get(0), 20, 5); + var graphRewindable = new GraphRewindable(exampleGraphs.getFirst(), 20, 5); String queryString = "SELECT * FROM <" + graphUri @@ -159,7 +176,7 @@ void executeSingleQuery_onNamedWithValidNoResultQuery_shouldReturnEmptyResultSet var query = QueryFactory.create(queryString); // Act - var resultSet = InMemorySparqlExecutor.executeSingleQuery(graphRewindable, query, graphUri); + var resultSet = executeSingleQuery(graphRewindable, query, graphUri); // Assert assertNotNull(resultSet); @@ -173,12 +190,12 @@ void executeSingleQuery_queryNotSelectedExistentGraph_emptyResult() { // Arrange String graphUri = "default"; exampleGraphs = List.of(createExampleGraph()); - var graphRewindable = new GraphRewindableWithUUIDs(exampleGraphs.get(0), 20, 5); + var graphRewindable = new GraphRewindable(exampleGraphs.getFirst(), 20, 5); String queryString = "SELECT * FROM WHERE { ?s ?p ?o }"; var query = QueryFactory.create(queryString); // Act - var resultSet = InMemorySparqlExecutor.executeSingleQuery(graphRewindable, query, graphUri); + var resultSet = executeSingleQuery(graphRewindable, query, graphUri); // Assert assertNotNull(resultSet); @@ -192,7 +209,7 @@ void executeSingleUpdate_onDefaultWithValidUpdate_shouldUpdateGraph() { // Arrange String graphUri = "default"; exampleGraphs = List.of(createExampleGraph(), GraphFactory.createDefaultGraph()); - var graphRewindable = new GraphRewindableWithUUIDs(exampleGraphs.get(1), 20, 5); + var graphRewindable = new GraphRewindable(exampleGraphs.get(1), 20, 5); // Act for (var t : exampleGraphs.get(0).find().toList()) { @@ -201,14 +218,20 @@ void executeSingleUpdate_onDefaultWithValidUpdate_shouldUpdateGraph() { .addInsert(t) .addOptional(Node.ANY, Node.ANY, Node.ANY) .build(); - InMemorySparqlExecutor.executeSingleUpdate( - graphRewindable, new UpdateRequest().add(update), graphUri); + graphRewindable.begin(TxnType.WRITE); + try { + var dataset = SessionDataStore.wrapGraphInDataset(graphRewindable, graphUri); + UpdateExecutionFactory.create(update, dataset).execute(); + graphRewindable.commit(); + } finally { + graphRewindable.end(); + } } // Assert try { graphRewindable.begin(TxnType.READ); - assertThat(graphRewindable.isIsomorphicWith(exampleGraphs.get(0))).isTrue(); + assertThat(graphRewindable.isIsomorphicWith(exampleGraphs.getFirst())).isTrue(); } finally { graphRewindable.end(); } diff --git a/backend/src/test/java/org/rdfarchitect/database/inmemory/SessionDataStoreImplTest.java b/backend/src/test/java/org/rdfarchitect/database/inmemory/SessionDataStoreImplTest.java index 5fe73ff4..937a17f2 100644 --- a/backend/src/test/java/org/rdfarchitect/database/inmemory/SessionDataStoreImplTest.java +++ b/backend/src/test/java/org/rdfarchitect/database/inmemory/SessionDataStoreImplTest.java @@ -20,7 +20,6 @@ import static org.assertj.core.api.Assertions.*; import org.apache.jena.graph.Graph; -import org.apache.jena.query.TxnType; import org.apache.jena.shared.impl.PrefixMappingImpl; import org.apache.jena.sparql.graph.GraphFactory; import org.apache.jena.sparql.graph.PrefixMappingMem; @@ -143,23 +142,6 @@ void deleteDataset_nonExistingName_doesNothing() { assertThat(inMemoryDatabase.listDatasets()).containsExactly("any"); } - @Test - void deleteDataset_validName_graphsAreClosed() { - // Arrange - exampleGraphs = List.of(createExampleGraph()); - inMemoryDatabase.create(GRAPH_IDENTIFIER, exampleGraphs.getFirst()); - var graph = inMemoryDatabase.begin(GRAPH_IDENTIFIER, TxnType.READ); - graph.end(); - - // Act - inMemoryDatabase.deleteDataset(NAME); - graph.begin(TxnType.READ); - - // Assert - assertThat(graph.isClosed()).isTrue(); - graph.end(); - } - @Test void listDatasets_emptyDatabase_returnsEmptyList() { // Arrange @@ -187,40 +169,27 @@ void listDatasets_nonEmptyDatabase_returnsAllDatasets() { } @Test - void begin_validNameAndGraph_returnsGraph() { + void getGraphWithContext_existingDataset_returnsGraphContext() { // Arrange - exampleGraphs = List.of(createExampleGraph(), createExampleGraph()); + exampleGraphs = List.of(createExampleGraph()); inMemoryDatabase.create(GRAPH_IDENTIFIER, exampleGraphs.getFirst()); // Act - var graph = inMemoryDatabase.begin(GRAPH_IDENTIFIER, TxnType.READ); + var graphContext = inMemoryDatabase.getGraphWithContext(GRAPH_IDENTIFIER); // Assert - assertThat(graph.isInTransaction()).isTrue(); - assertThat(graph.isIsomorphicWith(exampleGraphs.get(1))).isTrue(); - graph.end(); + assertThat(graphContext).isNotNull(); } @Test - void begin_nonExistingDatasetAndDefaultGraph_returnsEmptyGraph() { + void getGraphWithContext_nonExistingDataset_returnsGraphContext() { // Arrange // Act - var graph = inMemoryDatabase.begin(GRAPH_IDENTIFIER, TxnType.READ); + var graphContext = inMemoryDatabase.getGraphWithContext(GRAPH_IDENTIFIER); // Assert - assertThat(graph.isEmpty()).isTrue(); - graph.end(); - } - - @Test - void begin_nonExistingDatasetAndNamedGraph_throwsException() { - // Arrange - - // Act + Assert - var identifier = new GraphIdentifier("a", "http://example.com/graph"); - assertThatThrownBy(() -> inMemoryDatabase.begin(identifier, TxnType.READ)) - .isInstanceOf(IllegalArgumentException.class); + assertThat(graphContext).isNotNull(); } @Test @@ -450,198 +419,4 @@ void setPrefixMapping_nonEmptyPrefixMappingIntoNonEmptyGraph_overridesPrefixes() assertThat(storedPrefixes.getNsPrefixMap()) .containsExactlyEntriesOf(Map.of("ex2", "http://example2.com/")); } - - @Test - void undo_nonExistingDataset_throwsException() { - // Arrange - - // Act - assertThatExceptionOfType(DataAccessException.class) - .isThrownBy(() -> inMemoryDatabase.undo(GRAPH_IDENTIFIER)); - } - - @Test - void undo_nonExistingGraph_doesNothing() { - // Arrange - exampleGraphs = List.of(GraphFactory.createDefaultGraph()); - inMemoryDatabase.create(GRAPH_IDENTIFIER, exampleGraphs.getFirst()); - - // Act - assertThatExceptionOfType(Exception.class) - .isThrownBy(() -> inMemoryDatabase.undo(GRAPH_IDENTIFIER)); - } - - @Test - void undo_validGraph_undoesLastOperation() { - // Arrange - exampleGraphs = List.of(createExampleGraph()); - inMemoryDatabase.create(GRAPH_IDENTIFIER, exampleGraphs.getFirst()); - var graphRewindable = inMemoryDatabase.begin(GRAPH_IDENTIFIER, TxnType.WRITE); - graphRewindable.add(TestRDFUtils.triple("a a d")); - graphRewindable.commit(); - graphRewindable.end(); - - // Act - inMemoryDatabase.undo(GRAPH_IDENTIFIER); - - // Assert - graphRewindable = inMemoryDatabase.begin(GRAPH_IDENTIFIER, TxnType.READ); - assertThat(graphRewindable.isIsomorphicWith(exampleGraphs.getFirst())).isTrue(); - graphRewindable.end(); - } - - @Test - void redo_nonExistingDataset_throwsException() { - // Arrange - - // Act - assertThatExceptionOfType(DataAccessException.class) - .isThrownBy(() -> inMemoryDatabase.redo(GRAPH_IDENTIFIER)); - } - - @Test - void redo_nonExistingGraph_doesNothing() { - // Arrange - exampleGraphs = List.of(GraphFactory.createDefaultGraph()); - inMemoryDatabase.create(GRAPH_IDENTIFIER, exampleGraphs.getFirst()); - - // Act - assertThatExceptionOfType(Exception.class) - .isThrownBy(() -> inMemoryDatabase.redo(GRAPH_IDENTIFIER)); - } - - @Test - void redo_validGraph_restoresLastUndoneOperation() { - // Arrange - exampleGraphs = List.of(GraphFactory.createDefaultGraph()); - inMemoryDatabase.create(GRAPH_IDENTIFIER, exampleGraphs.getFirst()); - var graphRewindable = inMemoryDatabase.begin(GRAPH_IDENTIFIER, TxnType.WRITE); - graphRewindable.add(TestRDFUtils.triple("a a d")); - graphRewindable.commit(); - graphRewindable.end(); - inMemoryDatabase.undo(GRAPH_IDENTIFIER); - - // Act - inMemoryDatabase.redo(GRAPH_IDENTIFIER); - - // Assert - graphRewindable = inMemoryDatabase.begin(GRAPH_IDENTIFIER, TxnType.READ); - assertThat(graphRewindable.isIsomorphicWith(exampleGraphs.getFirst())).isFalse(); - assertThat(graphRewindable.contains(TestRDFUtils.triple("a a d"))).isTrue(); - graphRewindable.end(); - } - - @Test - void canUndo_nonExistingDataset_throwsException() { - // Arrange - - // Act - assertThatExceptionOfType(DataAccessException.class) - .isThrownBy(() -> inMemoryDatabase.canUndo(GRAPH_IDENTIFIER)); - } - - @Test - void canUndo_nonExistingGraph_throwsException() { - // Arrange - exampleGraphs = List.of(GraphFactory.createDefaultGraph()); - inMemoryDatabase.create(GRAPH_IDENTIFIER, exampleGraphs.getFirst()); - - // Act + Assert - assertThatExceptionOfType(Exception.class) - .isThrownBy( - () -> - inMemoryDatabase.canUndo( - new GraphIdentifier("a", "http://example.com/graph"))); - } - - @Test - void canUndo_validGraphWithNoCommit_returnsFalse() { - // Arrange - exampleGraphs = List.of(createExampleGraph()); - inMemoryDatabase.create(GRAPH_IDENTIFIER, exampleGraphs.getFirst()); - var graphRewindable = inMemoryDatabase.begin(GRAPH_IDENTIFIER, TxnType.WRITE); - graphRewindable.add(TestRDFUtils.triple("a a d")); - graphRewindable.end(); - - // Act - var canUndo = inMemoryDatabase.canUndo(GRAPH_IDENTIFIER); - - // Assert - assertThat(canUndo).isFalse(); - } - - @Test - void canUndo_validGraphWithCommit_returnsTrue() { - // Arrange - exampleGraphs = List.of(createExampleGraph()); - inMemoryDatabase.create(GRAPH_IDENTIFIER, exampleGraphs.getFirst()); - var graphRewindable = inMemoryDatabase.begin(GRAPH_IDENTIFIER, TxnType.WRITE); - graphRewindable.add(TestRDFUtils.triple("a a d")); - graphRewindable.commit(); - graphRewindable.end(); - - // Act - var canUndo = inMemoryDatabase.canUndo(GRAPH_IDENTIFIER); - - // Assert - assertThat(canUndo).isTrue(); - } - - @Test - void canRedo_nonExistingDataset_throwsException() { - // Arrange - - // Act - assertThatExceptionOfType(DataAccessException.class) - .isThrownBy(() -> inMemoryDatabase.canRedo(GRAPH_IDENTIFIER)); - } - - @Test - void canRedo_nonExistingGraph_throwsException() { - // Arrange - exampleGraphs = List.of(GraphFactory.createDefaultGraph()); - inMemoryDatabase.create(GRAPH_IDENTIFIER, exampleGraphs.getFirst()); - - // Act - assertThatExceptionOfType(Exception.class) - .isThrownBy( - () -> - inMemoryDatabase.canRedo( - new GraphIdentifier("a", "http://example.com/graph"))); - } - - @Test - void canRedo_validGraphWithNoUndoneChange_returnsFalse() { - // Arrange - exampleGraphs = List.of(createExampleGraph()); - inMemoryDatabase.create(GRAPH_IDENTIFIER, exampleGraphs.getFirst()); - var graphRewindable = inMemoryDatabase.begin(GRAPH_IDENTIFIER, TxnType.WRITE); - graphRewindable.add(TestRDFUtils.triple("a a d")); - graphRewindable.commit(); - graphRewindable.end(); - - // Act - var canRedo = inMemoryDatabase.canRedo(GRAPH_IDENTIFIER); - - // Assert - assertThat(canRedo).isFalse(); - } - - @Test - void canRedo_validGraphAfterUndoneChange_returnsTrue() { - // Arrange - exampleGraphs = List.of(createExampleGraph()); - inMemoryDatabase.create(GRAPH_IDENTIFIER, exampleGraphs.getFirst()); - var graphRewindable = inMemoryDatabase.begin(GRAPH_IDENTIFIER, TxnType.WRITE); - graphRewindable.add(TestRDFUtils.triple("a a d")); - graphRewindable.commit(); - graphRewindable.end(); - inMemoryDatabase.undo(GRAPH_IDENTIFIER); - - // Act - var canRedo = inMemoryDatabase.canRedo(GRAPH_IDENTIFIER); - - // Assert - assertThat(canRedo).isTrue(); - } } diff --git a/backend/src/test/java/org/rdfarchitect/rdf/graph/wrapper/GraphRewindableWithUUIDsTest.java b/backend/src/test/java/org/rdfarchitect/rdf/graph/wrapper/GraphRewindableWithUUIDsTest.java index 3f178314..5a5fa907 100644 --- a/backend/src/test/java/org/rdfarchitect/rdf/graph/wrapper/GraphRewindableWithUUIDsTest.java +++ b/backend/src/test/java/org/rdfarchitect/rdf/graph/wrapper/GraphRewindableWithUUIDsTest.java @@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test; import org.rdfarchitect.models.cim.rdf.resources.CIMS; import org.rdfarchitect.models.cim.rdf.resources.RDFA; +import org.rdfarchitect.rdf.graph.GraphUtils; import java.util.UUID; @@ -48,7 +49,7 @@ void enhanceWithUUIDs_subjectWithoutUUID_addsUUID() { NodeFactory.createURI("http://example.com/testSubject"), RDF.type.asNode(), RDFS.Class.asNode()); - GraphRewindableWithUUIDs.enhanceWithUUIDs(graph); + GraphUtils.enhanceWithUUIDs(graph); assertThat(graph.size()).isEqualTo(2); assertThat( @@ -66,7 +67,7 @@ void enhanceWithUUIDs_subjectWithUUID_doesNothing() { graph.add(subject, RDF.type.asNode(), RDFS.Class.asNode()); graph.add(subject, RDFA.uuid.asNode(), uuidNode); - GraphRewindableWithUUIDs.enhanceWithUUIDs(graph); + GraphUtils.enhanceWithUUIDs(graph); assertThat(graph.size()).isEqualTo(2); assertThat(graph.contains(subject, RDFA.uuid.asNode(), uuidNode)).isTrue(); diff --git a/backend/src/test/java/org/rdfarchitect/services/GraphChangeLogTest.java b/backend/src/test/java/org/rdfarchitect/services/GraphChangeLogTest.java index c86ee1a3..9fa7aabd 100644 --- a/backend/src/test/java/org/rdfarchitect/services/GraphChangeLogTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/GraphChangeLogTest.java @@ -18,108 +18,1292 @@ package org.rdfarchitect.services; import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; -import org.apache.jena.graph.Graph; +import org.apache.jena.query.ReadWrite; +import org.apache.jena.sparql.graph.GraphFactory; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.rdfarchitect.exception.graph.GraphNotInATransactionException; +import org.rdfarchitect.exception.graph.GraphNotInAWriteTransactionException; +import org.rdfarchitect.models.changelog.ChangeLog; import org.rdfarchitect.models.changelog.ChangeLogEntry; -import org.rdfarchitect.models.changelog.GraphChangeLog; -import org.rdfarchitect.rdf.graph.DeltaCompressible; +import org.rdfarchitect.models.changelog.ContextDelta; +import org.rdfarchitect.rdf.graph.wrapper.TransactionContext; +import java.lang.ref.WeakReference; +import java.util.List; import java.util.UUID; class GraphChangeLogTest { - private GraphChangeLog changeLog; + private TransactionContext txnContext; + private ChangeLog changeLog; private ChangeLogEntry entry1; private ChangeLogEntry entry2; private ChangeLogEntry entry3; private UUID version1; + private UUID version2; @BeforeEach void setUp() { - changeLog = new GraphChangeLog(); + txnContext = new TransactionContext(); + changeLog = new ChangeLog(txnContext); version1 = UUID.randomUUID(); - UUID version2 = UUID.randomUUID(); - UUID version3 = UUID.randomUUID(); + version2 = UUID.randomUUID(); + var version3 = UUID.randomUUID(); - entry1 = createMockEntry("entry1", version1); - entry2 = createMockEntry("entry2", version2); - entry3 = createMockEntry("entry3", version3); + entry1 = createEntry("entry1", version1); + entry2 = createEntry("entry2", version2); + entry3 = createEntry("entry3", version3); } - private ChangeLogEntry createMockEntry(String message, UUID versionId) { - DeltaCompressible delta = mock(DeltaCompressible.class); - when(delta.getVersionId()).thenReturn(versionId); - when(delta.getAdditions()).thenReturn(mock(Graph.class)); - when(delta.getDeletions()).thenReturn(mock(Graph.class)); - return new ChangeLogEntry(message, delta); + @AfterEach + void tearDown() { + if (txnContext.isInTransaction()) { + txnContext.end(); + } } - @Test - void addEntry_addsEntry() { - changeLog.addEntry(entry1); - assertThat(changeLog.getEntries()).hasSize(1); - assertThat(changeLog.getEntries().get(0)).isEqualTo(entry1); + private ChangeLogEntry createEntry(String message, UUID versionId) { + var contextDeltas = + List.of( + new ContextDelta( + "rdf", + new WeakReference<>(GraphFactory.createDefaultGraph()), + new WeakReference<>(GraphFactory.createDefaultGraph()))); + var entry = new ChangeLogEntry(message, 1, contextDeltas); + entry.setChangeId(versionId); + return entry; } - @Test - void undoChanges_existingChange_undoesChange() { - changeLog.addEntry(entry1); - changeLog.addEntry(entry2); - changeLog.undoChange(); + /** Helper: buffers a push and commits it within a write transaction. */ + private void pushAndCommit(ChangeLogEntry entry) { + txnContext.begin(ReadWrite.WRITE); + changeLog.push(entry); + changeLog.commit(); + txnContext.end(); + } + + /** Helper: begins a write transaction. */ + private void beginWrite() { + txnContext.begin(ReadWrite.WRITE); + } + + /** Helper: begins a read transaction. */ + private void beginRead() { + txnContext.begin(ReadWrite.READ); + } + + /** Helper: ends the current transaction. */ + private void endTxn() { + txnContext.end(); + } + + // ------------------------------------------------------------------------- + // Transaction enforcement + // ------------------------------------------------------------------------- + + @Nested + class TransactionEnforcement { + + @Test + void push_outsideTransaction_throws() { + assertThrows(GraphNotInATransactionException.class, () -> changeLog.push(entry1)); + } + + @Test + void push_inReadTransaction_throws() { + beginRead(); + assertThrows(GraphNotInAWriteTransactionException.class, () -> changeLog.push(entry1)); + } + + @Test + void clearRedo_outsideTransaction_throws() { + assertThrows(GraphNotInATransactionException.class, () -> changeLog.clearRedo()); + } + + @Test + void clearRedo_inReadTransaction_throws() { + beginRead(); + assertThrows(GraphNotInAWriteTransactionException.class, () -> changeLog.clearRedo()); + } + + @Test + void moveToRedo_outsideTransaction_throws() { + assertThrows(GraphNotInATransactionException.class, () -> changeLog.moveToRedo()); + } + + @Test + void moveToRedo_inReadTransaction_throws() { + beginRead(); + assertThrows(GraphNotInAWriteTransactionException.class, () -> changeLog.moveToRedo()); + } + + @Test + void moveToUndo_outsideTransaction_throws() { + assertThrows(GraphNotInATransactionException.class, () -> changeLog.moveToUndo()); + } + + @Test + void moveToUndo_inReadTransaction_throws() { + beginRead(); + assertThrows(GraphNotInAWriteTransactionException.class, () -> changeLog.moveToUndo()); + } + + @Test + void restore_outsideTransaction_throws() { + assertThrows(GraphNotInATransactionException.class, () -> changeLog.restore(version1)); + } + + @Test + void restore_inReadTransaction_throws() { + beginRead(); + assertThrows( + GraphNotInAWriteTransactionException.class, () -> changeLog.restore(version1)); + } + + @Test + void peekUndo_outsideTransaction_throws() { + assertThrows(GraphNotInATransactionException.class, () -> changeLog.peekUndo()); + } + + @Test + void peekRedo_outsideTransaction_throws() { + assertThrows(GraphNotInATransactionException.class, () -> changeLog.peekRedo()); + } + + @Test + void canUndo_outsideTransaction_throws() { + assertThrows(GraphNotInATransactionException.class, () -> changeLog.canUndo()); + } + + @Test + void canRedo_outsideTransaction_throws() { + assertThrows(GraphNotInATransactionException.class, () -> changeLog.canRedo()); + } + + @Test + void getUndoHistory_outsideTransaction_throws() { + assertThrows(GraphNotInATransactionException.class, () -> changeLog.getUndoHistory()); + } + + @Test + void getRedoHistory_outsideTransaction_throws() { + assertThrows(GraphNotInATransactionException.class, () -> changeLog.getRedoHistory()); + } + + @Test + void peekUndo_inReadTransaction_succeeds() { + pushAndCommit(entry1); + beginRead(); + assertDoesNotThrow(() -> changeLog.peekUndo()); + } + + @Test + void peekRedo_inReadTransaction_succeeds() { + pushAndCommit(entry1); + pushAndCommit(entry2); + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginRead(); + assertDoesNotThrow(() -> changeLog.peekRedo()); + } + + @Test + void getUndoHistory_inReadTransaction_succeeds() { + beginRead(); + assertDoesNotThrow(() -> changeLog.getUndoHistory()); + } + + @Test + void getRedoHistory_inReadTransaction_succeeds() { + beginRead(); + assertDoesNotThrow(() -> changeLog.getRedoHistory()); + } + + @Test + void canUndo_inReadTransaction_succeeds() { + beginRead(); + assertDoesNotThrow(() -> changeLog.canUndo()); + } + + @Test + void canRedo_inReadTransaction_succeeds() { + beginRead(); + assertDoesNotThrow(() -> changeLog.canRedo()); + } + } + + // ------------------------------------------------------------------------- + // commit / abort / hasChanges + // ------------------------------------------------------------------------- + + @Nested + class TransactionParticipantBehavior { + + @Test + void push_doesNotApplyBeforeCommit() { + beginWrite(); + changeLog.push(entry1); + + assertThat(changeLog.getUndoHistory()).isEmpty(); + assertTrue(changeLog.hasChanges()); + } + + @Test + void commit_appliesPendingPush() { + pushAndCommit(entry1); + + beginRead(); + assertThat(changeLog.getUndoHistory()).hasSize(1); + assertThat(changeLog.getUndoHistory().getFirst()).isEqualTo(entry1); + assertFalse(changeLog.hasChanges()); + } + + @Test + void abort_discardsPendingPush() { + beginWrite(); + changeLog.push(entry1); + changeLog.abort(); + + assertThat(changeLog.getUndoHistory()).isEmpty(); + assertFalse(changeLog.hasChanges()); + } + + @Test + void abort_discardsPendingMoveToRedo() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.abort(); + + assertThat(changeLog.getUndoHistory()).hasSize(2); + assertThat(changeLog.getRedoHistory()).isEmpty(); + } + + @Test + void abort_discardsPendingMoveToUndo() { + pushAndCommit(entry1); + pushAndCommit(entry2); + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginWrite(); + changeLog.moveToUndo(); + changeLog.abort(); + + assertThat(changeLog.getUndoHistory()).hasSize(1); + assertThat(changeLog.getRedoHistory()).hasSize(1); + } + + @Test + void abort_discardsPendingRestore() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginWrite(); + changeLog.restore(version1); + changeLog.abort(); + + assertThat(changeLog.getUndoHistory()).hasSize(3); + } + + @Test + void abort_discardsPendingClearRedo() { + pushAndCommit(entry1); + pushAndCommit(entry2); + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginWrite(); + changeLog.clearRedo(); + changeLog.abort(); + + assertThat(changeLog.getRedoHistory()).hasSize(1); + } + + @Test + void hasChanges_noMutations_returnsFalse() { + assertFalse(changeLog.hasChanges()); + } + + @Test + void hasChanges_afterCommit_returnsFalse() { + pushAndCommit(entry1); + assertFalse(changeLog.hasChanges()); + } + + @Test + void hasChanges_afterAbort_returnsFalse() { + beginWrite(); + changeLog.push(entry1); + changeLog.abort(); + assertFalse(changeLog.hasChanges()); + } + + @Test + void hasChanges_multiplePendingMutations_returnsTrue() { + beginWrite(); + changeLog.push(entry1); + changeLog.push(entry2); + changeLog.clearRedo(); + assertTrue(changeLog.hasChanges()); + } + } + + // ------------------------------------------------------------------------- + // push + // ------------------------------------------------------------------------- + + @Nested + class PushTests { + + @Test + void push_singleEntry() { + pushAndCommit(entry1); + + beginRead(); + assertThat(changeLog.getUndoHistory()).containsExactly(entry1); + assertThat(changeLog.getRedoHistory()).isEmpty(); + } - assertThat(changeLog.getEntries()).hasSize(1); - assertThat(changeLog.getEntries().get(0)).isEqualTo(entry1); + @Test + void push_multipleEntries_orderIsNewestFirst() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginRead(); + assertThat(changeLog.getUndoHistory()).containsExactly(entry3, entry2, entry1); + } + + @Test + void push_multipleInSameTransaction_orderIsNewestFirst() { + beginWrite(); + changeLog.push(entry1); + changeLog.push(entry2); + changeLog.push(entry3); + changeLog.commit(); + + assertThat(changeLog.getUndoHistory()).containsExactly(entry3, entry2, entry1); + } + + @Test + void push_clearsRedoStack() { + pushAndCommit(entry1); + pushAndCommit(entry2); + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginRead(); + assertTrue(changeLog.canRedo()); + endTxn(); + + pushAndCommit(entry3); + + beginRead(); + assertFalse(changeLog.canRedo()); + assertThat(changeLog.getRedoHistory()).isEmpty(); + assertThat(changeLog.getUndoHistory()).containsExactly(entry3, entry1); + } + } + + // ------------------------------------------------------------------------- + // clearRedo + // ------------------------------------------------------------------------- + + @Nested + class ClearRedoTests { + + @Test + void clearRedo_emptyRedoStack_noEffect() { + pushAndCommit(entry1); + + beginWrite(); + changeLog.clearRedo(); + changeLog.commit(); + endTxn(); + + beginRead(); + assertThat(changeLog.getRedoHistory()).isEmpty(); + assertThat(changeLog.getUndoHistory()).containsExactly(entry1); + } + + @Test + void clearRedo_nonEmptyRedoStack_clearsAll() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginRead(); + assertThat(changeLog.getRedoHistory()).hasSize(2); + endTxn(); + + beginWrite(); + changeLog.clearRedo(); + changeLog.commit(); + endTxn(); + + beginRead(); + assertThat(changeLog.getRedoHistory()).isEmpty(); + assertFalse(changeLog.canRedo()); + } + } + + // ------------------------------------------------------------------------- + // moveToRedo + // ------------------------------------------------------------------------- + + @Nested + class MoveToRedoTests { + + @Test + void moveToRedo_movesTopUndoToRedo() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + + assertThat(changeLog.getUndoHistory()).containsExactly(entry1); + assertThat(changeLog.getRedoHistory()).containsExactly(entry2); + } + + @Test + void moveToRedo_twice_movesTopTwoEntries() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.moveToRedo(); + changeLog.commit(); + + assertThat(changeLog.getUndoHistory()).containsExactly(entry1); + assertThat(changeLog.getRedoHistory()).containsExactly(entry2, entry3); + } + + @Test + void moveToRedo_inSeparateTransactions_redoOrderIsCorrect() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginRead(); + assertThat(changeLog.getRedoHistory()).containsExactly(entry2, entry3); + } + + @Test + void moveToRedo_notAppliedBeforeCommit() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginWrite(); + changeLog.moveToRedo(); + + assertThat(changeLog.getUndoHistory()).hasSize(2); + assertThat(changeLog.getRedoHistory()).isEmpty(); + } } - @Test - void undoChanges_noChanges_doesNothing() { - changeLog.undoChange(); - assertThat(changeLog.getEntries()).isEmpty(); + // ------------------------------------------------------------------------- + // moveToUndo + // ------------------------------------------------------------------------- + + @Nested + class MoveToUndoTests { + + @Test + void moveToUndo_movesTopRedoBackToUndo() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginWrite(); + changeLog.moveToUndo(); + changeLog.commit(); + + assertThat(changeLog.getUndoHistory()).containsExactly(entry2, entry1); + assertThat(changeLog.getRedoHistory()).isEmpty(); + } + + @Test + void moveToUndo_afterMultipleMoveToRedo_restoresOneAtATime() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginWrite(); + changeLog.moveToUndo(); + changeLog.commit(); + + assertThat(changeLog.getUndoHistory()).containsExactly(entry2, entry1); + assertThat(changeLog.getRedoHistory()).containsExactly(entry3); + } + + @Test + void moveToUndo_fullRoundtrip_restoresOriginalState() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginWrite(); + changeLog.moveToUndo(); + changeLog.commit(); + endTxn(); + + beginRead(); + assertThat(changeLog.getUndoHistory()).containsExactly(entry2, entry1); + assertThat(changeLog.getRedoHistory()).isEmpty(); + assertFalse(changeLog.canRedo()); + } + + @Test + void moveToUndo_notAppliedBeforeCommit() { + pushAndCommit(entry1); + pushAndCommit(entry2); + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginWrite(); + changeLog.moveToUndo(); + + assertThat(changeLog.getUndoHistory()).hasSize(1); + assertThat(changeLog.getRedoHistory()).hasSize(1); + } + } + + // ------------------------------------------------------------------------- + // peekUndo / peekRedo + // ------------------------------------------------------------------------- + + @Nested + class PeekTests { + + @Test + void peekUndo_returnsTopWithoutMutating() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginRead(); + var peeked = changeLog.peekUndo(); + + assertThat(peeked).isEqualTo(entry2); + assertThat(changeLog.getUndoHistory()).hasSize(2); + } + + @Test + void peekUndo_emptyStack_returnsNull() { + beginRead(); + assertThat(changeLog.peekUndo()).isNull(); + } + + @Test + void peekUndo_calledTwice_returnsSameInstance() { + pushAndCommit(entry1); + + beginRead(); + var first = changeLog.peekUndo(); + var second = changeLog.peekUndo(); + assertThat(first).isSameAs(second); + } + + @Test + void peekRedo_returnsTopWithoutMutating() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + + var peeked = changeLog.peekRedo(); + + assertThat(peeked).isEqualTo(entry2); + assertThat(changeLog.getRedoHistory()).hasSize(1); + } + + @Test + void peekRedo_emptyStack_returnsNull() { + beginRead(); + assertThat(changeLog.peekRedo()).isNull(); + } + + @Test + void peekRedo_calledTwice_returnsSameInstance() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + + var first = changeLog.peekRedo(); + var second = changeLog.peekRedo(); + assertThat(first).isSameAs(second); + } + + @Test + void peekRedo_afterMultipleMoveToRedo_returnsLastMoved() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.moveToRedo(); + changeLog.commit(); + + assertThat(changeLog.peekRedo()).isEqualTo(entry2); + } } - @Test - void redoChange_undoneChange_redoesChange() { - changeLog.addEntry(entry1); - changeLog.addEntry(entry2); - changeLog.undoChange(); - changeLog.redoChange(); + // ------------------------------------------------------------------------- + // canUndo / canRedo + // ------------------------------------------------------------------------- + + @Nested + class CanUndoRedoTests { + + @Test + void canUndo_emptyLog_returnsFalse() { + beginRead(); + assertFalse(changeLog.canUndo()); + } + + @Test + void canUndo_singleEntry_returnsFalse() { + pushAndCommit(entry1); + beginRead(); + assertFalse(changeLog.canUndo()); + } + + @Test + void canUndo_twoEntries_returnsTrue() { + pushAndCommit(entry1); + pushAndCommit(entry2); + beginRead(); + assertTrue(changeLog.canUndo()); + } + + @Test + void canUndo_threeEntries_returnsTrue() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + beginRead(); + assertTrue(changeLog.canUndo()); + } + + @Test + void canUndo_afterUndoingAllButOne_returnsFalse() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + + assertFalse(changeLog.canUndo()); + } - assertThat(changeLog.getEntries()).hasSize(2); - assertThat(changeLog.getEntries().get(1)).isEqualTo(entry2); + @Test + void canRedo_noUndoneChanges_returnsFalse() { + pushAndCommit(entry1); + beginRead(); + assertFalse(changeLog.canRedo()); + } + + @Test + void canRedo_afterMoveToRedo_returnsTrue() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + + assertTrue(changeLog.canRedo()); + } + + @Test + void canRedo_afterMoveToRedoThenMoveToUndo_returnsFalse() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginWrite(); + changeLog.moveToUndo(); + changeLog.commit(); + + assertFalse(changeLog.canRedo()); + } + + @Test + void canRedo_afterMoveToRedoThenPush_returnsFalse() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + pushAndCommit(entry3); + + beginRead(); + assertFalse(changeLog.canRedo()); + } } - @Test - void redoChange_noUndoneChanges_doesNothing() { - changeLog.addEntry(entry1); - changeLog.redoChange(); + // ------------------------------------------------------------------------- + // getUndoHistory + // ------------------------------------------------------------------------- + + @Nested + class GetUndoHistoryTests { + + @Test + void getUndoHistory_emptyLog_returnsEmptyList() { + beginRead(); + assertThat(changeLog.getUndoHistory()).isEmpty(); + } + + @Test + void getUndoHistory_returnsImmutableCopy() { + pushAndCommit(entry1); + beginRead(); + var history = changeLog.getUndoHistory(); + assertThrows(UnsupportedOperationException.class, () -> history.add(entry2)); + } + + @Test + void getUndoHistory_singleEntry() { + pushAndCommit(entry1); - assertThat(changeLog.getEntries()).hasSize(1); - assertThat(changeLog.getEntries().get(0)).isEqualTo(entry1); + beginRead(); + assertThat(changeLog.getUndoHistory()).containsExactly(entry1); + } + + @Test + void getUndoHistory_multipleEntries_newestFirst() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginRead(); + assertThat(changeLog.getUndoHistory()).containsExactly(entry3, entry2, entry1); + } + + @Test + void getUndoHistory_afterMoveToRedo_excludesMovedEntry() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + + assertThat(changeLog.getUndoHistory()).containsExactly(entry2, entry1); + } + + @Test + void getUndoHistory_afterMoveToRedoThenMoveToUndo_includesRestoredEntry() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginWrite(); + changeLog.moveToUndo(); + changeLog.commit(); + + assertThat(changeLog.getUndoHistory()).containsExactly(entry2, entry1); + } } - @Test - void restore_validVersion_restoresVersion() { - changeLog.addEntry(entry1); - changeLog.addEntry(entry2); - changeLog.addEntry(entry3); + // ------------------------------------------------------------------------- + // getRedoHistory + // ------------------------------------------------------------------------- + + @Nested + class GetRedoHistoryTests { + + @Test + void getRedoHistory_emptyLog_returnsEmptyList() { + beginRead(); + assertThat(changeLog.getRedoHistory()).isEmpty(); + } + + @Test + void getRedoHistory_nothingUndone_returnsEmptyList() { + pushAndCommit(entry1); + pushAndCommit(entry2); + beginRead(); + assertThat(changeLog.getRedoHistory()).isEmpty(); + } + + @Test + void getRedoHistory_returnsImmutableCopy() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + + var history = changeLog.getRedoHistory(); + assertThrows(UnsupportedOperationException.class, () -> history.add(entry3)); + } + + @Test + void getRedoHistory_afterSingleMoveToRedo_containsMovedEntry() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + + assertThat(changeLog.getRedoHistory()).containsExactly(entry2); + } + + @Test + void getRedoHistory_afterMultipleMoveToRedo_containsAllInOrder() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.moveToRedo(); + changeLog.commit(); + + assertThat(changeLog.getRedoHistory()).containsExactly(entry2, entry3); + } + + @Test + void getRedoHistory_afterMoveToRedoInSeparateTransactions_containsAllInOrder() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginRead(); + assertThat(changeLog.getRedoHistory()).containsExactly(entry2, entry3); + } + + @Test + void getRedoHistory_afterMoveToRedoThenPartialMoveToUndo_reflectsCurrentState() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginWrite(); + changeLog.moveToUndo(); + changeLog.commit(); + endTxn(); + + beginRead(); + assertThat(changeLog.getRedoHistory()).containsExactly(entry3); + } + + @Test + void getRedoHistory_afterMoveToRedoThenFullMoveToUndo_isEmpty() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); - changeLog.restore(version1); + beginWrite(); + changeLog.moveToUndo(); + changeLog.commit(); + endTxn(); - assertThat(changeLog.getEntries()).hasSize(1); - assertThat(changeLog.getEntries().get(0).getChangeId()).isEqualTo(entry1.getChangeId()); + beginRead(); + assertThat(changeLog.getRedoHistory()).isEmpty(); + } + + @Test + void getRedoHistory_afterClearRedo_isEmpty() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginRead(); + assertThat(changeLog.getRedoHistory()).hasSize(1); + endTxn(); + + beginWrite(); + changeLog.clearRedo(); + changeLog.commit(); + endTxn(); + + beginRead(); + assertThat(changeLog.getRedoHistory()).isEmpty(); + } + + @Test + void getRedoHistory_afterPush_isEmpty() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginRead(); + assertThat(changeLog.getRedoHistory()).hasSize(1); + endTxn(); + + pushAndCommit(entry3); + + beginRead(); + assertThat(changeLog.getRedoHistory()).isEmpty(); + } + + @Test + void getRedoHistory_afterRestore_containsMovedEntries() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginWrite(); + changeLog.restore(version1); + changeLog.commit(); + + assertThat(changeLog.getRedoHistory()).containsExactly(entry2, entry3); + } + + @Test + void getRedoHistory_afterRestoreToMiddle_containsOnlyNewerEntries() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginWrite(); + changeLog.restore(version2); + changeLog.commit(); + + assertThat(changeLog.getRedoHistory()).containsExactly(entry3); + } + + // ------------------------------------------------------------------------- + // restore + // ------------------------------------------------------------------------- + + @Nested + class RestoreTests { + + @Test + void restore_toOldestVersion_leavesOnlyThatEntry() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginWrite(); + changeLog.restore(version1); + changeLog.commit(); + + assertThat(changeLog.getUndoHistory()).containsExactly(entry1); + assertThat(changeLog.getRedoHistory()).containsExactly(entry2, entry3); + } + + @Test + void restore_toMiddleVersion_keepsOlderEntries() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginWrite(); + changeLog.restore(version2); + changeLog.commit(); + + assertThat(changeLog.getUndoHistory()).containsExactly(entry2, entry1); + assertThat(changeLog.getRedoHistory()).containsExactly(entry3); + } + + @Test + void restore_toCurrentVersion_noChange() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginWrite(); + changeLog.restore(version2); + changeLog.commit(); + + assertThat(changeLog.getUndoHistory()).containsExactly(entry2, entry1); + assertThat(changeLog.getRedoHistory()).isEmpty(); + } + + @Test + void restore_invalidVersion_clearsUndoStack() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginWrite(); + changeLog.restore(UUID.randomUUID()); + changeLog.commit(); + + assertThat(changeLog.getUndoHistory()).isEmpty(); + } + + @Test + void restore_notAppliedBeforeCommit() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginWrite(); + changeLog.restore(version1); + + assertThat(changeLog.getUndoHistory()).hasSize(3); + assertThat(changeLog.getRedoHistory()).isEmpty(); + } + } } - @Test - void restore_invalidVersion_doesNothing() { - changeLog.addEntry(entry1); - UUID invalidVersion = UUID.randomUUID(); + // ------------------------------------------------------------------------- + // Complex scenarios + // ------------------------------------------------------------------------- + + @Nested + class ComplexScenarios { + + @Test + void undoRedoUndoRedo_fullCycle() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + // Undo entry3. + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginRead(); + assertThat(changeLog.getUndoHistory()).containsExactly(entry2, entry1); + assertThat(changeLog.getRedoHistory()).containsExactly(entry3); + endTxn(); + + // Undo entry2. + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginRead(); + assertThat(changeLog.getUndoHistory()).containsExactly(entry1); + assertThat(changeLog.getRedoHistory()).containsExactly(entry2, entry3); + endTxn(); + + // Redo entry2. + beginWrite(); + changeLog.moveToUndo(); + changeLog.commit(); + endTxn(); + + beginRead(); + assertThat(changeLog.getUndoHistory()).containsExactly(entry2, entry1); + assertThat(changeLog.getRedoHistory()).containsExactly(entry3); + endTxn(); + + // Redo entry3. + beginWrite(); + changeLog.moveToUndo(); + changeLog.commit(); + endTxn(); + + beginRead(); + assertThat(changeLog.getUndoHistory()).containsExactly(entry3, entry2, entry1); + assertThat(changeLog.getRedoHistory()).isEmpty(); + } + + @Test + void undoThenPush_invalidatesRedoHistory() { + pushAndCommit(entry1); + pushAndCommit(entry2); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.commit(); + endTxn(); + + beginRead(); + assertThat(changeLog.getRedoHistory()).containsExactly(entry2); + endTxn(); + + pushAndCommit(entry3); + + beginRead(); + assertThat(changeLog.getUndoHistory()).containsExactly(entry3, entry1); + assertThat(changeLog.getRedoHistory()).isEmpty(); + assertFalse(changeLog.canRedo()); + } + + @Test + void restoreThenPush_invalidatesRedoHistory() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginWrite(); + changeLog.restore(version1); + changeLog.commit(); + endTxn(); + + beginRead(); + assertThat(changeLog.getRedoHistory()).hasSize(2); + endTxn(); + + var entry4 = createEntry("entry4", UUID.randomUUID()); + pushAndCommit(entry4); + + beginRead(); + assertThat(changeLog.getUndoHistory()).containsExactly(entry4, entry1); + assertThat(changeLog.getRedoHistory()).isEmpty(); + } + + @Test + void multipleMutationsInSingleTransaction_allApplied() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.moveToRedo(); + changeLog.clearRedo(); + changeLog.commit(); + + assertThat(changeLog.getUndoHistory()).containsExactly(entry1); + assertThat(changeLog.getRedoHistory()).isEmpty(); + assertFalse(changeLog.canUndo()); + assertFalse(changeLog.canRedo()); + } + + @Test + void abortAfterMultipleMutations_discardsAll() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginWrite(); + changeLog.moveToRedo(); + changeLog.moveToRedo(); + changeLog.clearRedo(); + changeLog.abort(); + + assertThat(changeLog.getUndoHistory()).containsExactly(entry3, entry2, entry1); + assertThat(changeLog.getRedoHistory()).isEmpty(); + } + + @Test + void restoreThenMoveToUndo_redoesFromRestoredState() { + pushAndCommit(entry1); + pushAndCommit(entry2); + pushAndCommit(entry3); + + beginWrite(); + changeLog.restore(version1); + changeLog.commit(); + endTxn(); + + beginRead(); + assertThat(changeLog.getRedoHistory()).containsExactly(entry2, entry3); + endTxn(); - changeLog.restore(invalidVersion); + // Redo entry2. + beginWrite(); + changeLog.moveToUndo(); + changeLog.commit(); + endTxn(); - assertThat(changeLog.getEntries()).hasSize(1); + beginRead(); + assertThat(changeLog.getUndoHistory()).containsExactly(entry2, entry1); + assertThat(changeLog.getRedoHistory()).containsExactly(entry3); + } } } 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 c2e7af86..94a6bacf 100644 --- a/backend/src/test/java/org/rdfarchitect/services/delete/DeleteResourcesServiceTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/delete/DeleteResourcesServiceTest.java @@ -21,7 +21,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.RDFNode; @@ -41,10 +41,9 @@ 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.database.inmemory.GraphWithContextTransactional; 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; @@ -91,7 +90,7 @@ class DeleteResourcesServiceTest { @InjectMocks private DeleteResourcesService service; - private GraphRewindableWithUUIDs wrappedGraph; + private GraphWithContextTransactional wrappedContext; @BeforeEach void setUp() throws IOException { @@ -100,20 +99,19 @@ void setUp() throws IOException { RDFDataMgr.read(graph, in, Lang.TTL); in.close(); - wrappedGraph = new GraphRewindableWithUUIDs(graph, 5, 5); - var wrappedContext = new GraphWithContext(wrappedGraph); + wrappedContext = new GraphWithContextTransactional(graph); when(databasePort.getGraphWithContext(any(GraphIdentifier.class))) .thenReturn(wrappedContext); } private Model readModel() { - wrappedGraph.begin(TxnType.READ); - return ModelFactory.createModelForGraph(wrappedGraph); + wrappedContext.begin(ReadWrite.READ); + return ModelFactory.createModelForGraph(wrappedContext.getRdfGraph()); } private void endRead() { - wrappedGraph.end(); + wrappedContext.end(); } private ResourceDeleteRequest request(UUID uuid, DeleteAction action) { 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 af496552..18452919 100644 --- a/backend/src/test/java/org/rdfarchitect/services/delete/FindDeleteDependenciesServiceTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/delete/FindDeleteDependenciesServiceTest.java @@ -36,9 +36,8 @@ 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.database.inmemory.GraphWithContextTransactional; import org.rdfarchitect.models.cim.relations.model.CIMResourceTypeIdentifyingUtils.CimResourceType; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; import java.io.IOException; import java.io.InputStream; @@ -82,8 +81,7 @@ void setUp() throws IOException { RDFDataMgr.read(graph, in, Lang.TTL); in.close(); - var wrappedGraph = new GraphRewindableWithUUIDs(graph, 5, 5); - var wrappedContext = new GraphWithContext(wrappedGraph); + var wrappedContext = new GraphWithContextTransactional(graph); when(databasePort.getGraphWithContext(any(GraphIdentifier.class))) .thenReturn(wrappedContext); diff --git a/backend/src/test/java/org/rdfarchitect/services/diagrams/CustomDiagramsServiceTest.java b/backend/src/test/java/org/rdfarchitect/services/diagrams/CustomDiagramsServiceTest.java index a341b118..27be56ce 100644 --- a/backend/src/test/java/org/rdfarchitect/services/diagrams/CustomDiagramsServiceTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/diagrams/CustomDiagramsServiceTest.java @@ -21,11 +21,12 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +import org.apache.jena.query.ReadWrite; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.rdfarchitect.database.DatabasePort; +import org.rdfarchitect.database.GraphContext; import org.rdfarchitect.database.GraphIdentifier; -import org.rdfarchitect.database.inmemory.GraphWithContext; import org.rdfarchitect.database.inmemory.diagrams.ClassInDiagram; import org.rdfarchitect.database.inmemory.diagrams.CustomDiagram; import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; @@ -59,9 +60,8 @@ void getCustomDiagramsForGraph_diagramExists_returnsDiagram() { var map = new ConcurrentHashMap(); map.put(diagramId, diagram); - var graphWithContext = mockGraph(map); - - when(databasePort.getGraphWithContext(graphIdentifier)).thenReturn(graphWithContext); + var graph = mockGraph(map); + when(databasePort.getGraphWithContext(graphIdentifier)).thenReturn(graph); var result = service.getCustomDiagramsForGraph(graphIdentifier); @@ -102,7 +102,6 @@ void deleteCustomDiagram_diagramExistsInGraph_removesFromMap() { map.put(diagramId, new CustomDiagram(diagramId)); var graph = mockGraph(map); - when(databasePort.getGraphWithContext(graphIdentifier)).thenReturn(graph); service.deleteCustomDiagram(graphIdentifier, diagramId.toString()); @@ -114,7 +113,6 @@ void deleteCustomDiagram_diagramExistsInGraph_removesFromMap() { void replaceCustomDiagram_newDiagramForDataset_replacesInMap() { var diagramId = UUID.randomUUID(); var newDiagram = new CustomDiagram(diagramId); - var map = new ConcurrentHashMap(); when(databasePort.getDatasetDiagrams("dataset")).thenReturn(map); @@ -128,10 +126,9 @@ void replaceCustomDiagram_newDiagramForDataset_replacesInMap() { void replaceCustomDiagram_newDiagramForGraph_replacesInMap() { var diagramId = UUID.randomUUID(); var newDiagram = new CustomDiagram(diagramId); - var map = new ConcurrentHashMap(); - var graph = mockGraph(map); + var graph = mockGraph(map); when(databasePort.getGraphWithContext(graphIdentifier)).thenReturn(graph); service.replaceCustomDiagram(graphIdentifier, diagramId.toString(), newDiagram); @@ -140,41 +137,12 @@ void replaceCustomDiagram_newDiagramForGraph_replacesInMap() { } @Test - void addToDiagram_classAddedToDatasetDiagram_diagramContainsClass() { - var diagramId = UUID.randomUUID(); - var diagram = new CustomDiagram(diagramId); - diagram.setClasses(new ArrayList<>()); - - var map = new ConcurrentHashMap(); - map.put(diagramId, diagram); - - when(databasePort.getDatasetDiagrams("dataset")).thenReturn(map); - - var classInDiagram = - new ClassInDiagram(UUID.randomUUID(), new URI("http://example.org#graph")); - - service.addToDiagram("dataset", diagramId.toString(), List.of(classInDiagram)); - - assertThat(diagram.getClasses()).containsExactly(classInDiagram); - } - - @Test - void addToDiagram_nonExistingDatasetDiagram_doesNotFail() { - when(databasePort.getDatasetDiagrams("dataset")).thenReturn(new ConcurrentHashMap<>()); - - assertDoesNotThrow( - () -> service.addToDiagram("dataset", UUID.randomUUID().toString(), List.of())); - } - - @Test - void removeFromDiagram_classRemovedFromDatasetDiagramByUuid_diagramIsEmpty() { + void removeFromDiagram_classRemovedFromDatasetDiagram_diagramIsEmpty() { var classId = UUID.randomUUID(); var classInDiagram = new ClassInDiagram(classId, new URI("http://example.org#graph")); - var diagramId = UUID.randomUUID(); var diagram = new CustomDiagram(diagramId); diagram.setClasses(new ArrayList<>(List.of(classInDiagram))); - var map = new ConcurrentHashMap(); map.put(diagramId, diagram); @@ -189,16 +157,13 @@ void removeFromDiagram_classRemovedFromDatasetDiagramByUuid_diagramIsEmpty() { void removeFromDiagram_classRemovedFromGraphDiagram_diagramIsEmpty() { var classId = UUID.randomUUID(); var classInDiagram = new ClassInDiagram(classId, new URI("http://example.org#graph")); - var diagramId = UUID.randomUUID(); var diagram = new CustomDiagram(diagramId); diagram.setClasses(new ArrayList<>(List.of(classInDiagram))); - var map = new ConcurrentHashMap(); map.put(diagramId, diagram); var graph = mockGraph(map); - when(databasePort.getGraphWithContext(graphIdentifier)).thenReturn(graph); service.removeFromDiagram(graphIdentifier, diagramId.toString(), classId); @@ -211,24 +176,18 @@ void removeFromAllDiagrams_classInGraphAndDatasetDiagram_removedFromBoth() { var classId = UUID.randomUUID(); var classInDiagram = new ClassInDiagram(classId, new URI("http://example.org#graph")); - var diagramId1 = UUID.randomUUID(); - var graphDiagram = new CustomDiagram(diagramId1); + var graphDiagram = new CustomDiagram(UUID.randomUUID()); graphDiagram.setClasses(new ArrayList<>(List.of(classInDiagram))); - - var diagramId2 = UUID.randomUUID(); - var datasetDiagram = new CustomDiagram(diagramId2); - datasetDiagram.setClasses(new ArrayList<>(List.of(classInDiagram))); - var graphMap = new ConcurrentHashMap(); - graphMap.put(UUID.randomUUID(), graphDiagram); + graphMap.put(graphDiagram.getDiagramId(), graphDiagram); + var datasetDiagram = new CustomDiagram(UUID.randomUUID()); + datasetDiagram.setClasses(new ArrayList<>(List.of(classInDiagram))); var datasetMap = new ConcurrentHashMap(); - datasetMap.put(UUID.randomUUID(), datasetDiagram); + datasetMap.put(datasetDiagram.getDiagramId(), datasetDiagram); var graph = mockGraph(graphMap); - when(databasePort.getGraphWithContext(graphIdentifier)).thenReturn(graph); - when(databasePort.getDatasetDiagrams("dataset")).thenReturn(datasetMap); service.removeFromAllDiagrams(graphIdentifier, classId); @@ -237,8 +196,9 @@ void removeFromAllDiagrams_classInGraphAndDatasetDiagram_removedFromBoth() { assertThat(datasetDiagram.getClasses()).isEmpty(); } - private GraphWithContext mockGraph(ConcurrentHashMap diagrams) { - GraphWithContext graph = mock(GraphWithContext.class); + private GraphContext mockGraph(ConcurrentHashMap diagrams) { + GraphContext graph = mock(GraphContext.class); + when(graph.begin(any(ReadWrite.class))).thenReturn(graph); when(graph.getCustomDiagrams()).thenReturn(diagrams); return graph; } diff --git a/backend/src/test/java/org/rdfarchitect/services/dl/DiagramLayoutServicesTestBase.java b/backend/src/test/java/org/rdfarchitect/services/dl/DiagramLayoutServicesTestBase.java index b292115d..420636b9 100644 --- a/backend/src/test/java/org/rdfarchitect/services/dl/DiagramLayoutServicesTestBase.java +++ b/backend/src/test/java/org/rdfarchitect/services/dl/DiagramLayoutServicesTestBase.java @@ -36,7 +36,7 @@ import org.rdfarchitect.dl.rdf.resources.CIM; import org.rdfarchitect.dl.rdf.resources.DL; import org.rdfarchitect.rdf.graph.source.builder.implementations.GraphFileSourceBuilderImpl; -import org.rdfarchitect.rdf.graph.wrapper.DiagramLayout; +import org.rdfarchitect.rdf.graph.wrapper.DiagramLayoutDelta; import org.rdfarchitect.services.dl.update.UpdateDiagramLayoutService; import org.rdfarchitect.services.rendering.GraphToCIMCollectionConverterService; import org.rdfarchitect.services.rendering.GraphToCIMCollectionConverterUseCase; @@ -70,13 +70,12 @@ public class DiagramLayoutServicesTestBase { public static DatabasePort databasePort; public static PackageMapper packageMapper; public static InMemoryDatabase database; - public static DiagramLayout diagramLayout; + public static DiagramLayoutDelta diagramLayout; public static UpdateDiagramLayoutService updateDiagramLayoutService; @BeforeAll static void setUpEnvironment() { - diagramLayout = new DiagramLayout(); - databasePort = new InMemoryDatabaseAdapter(new InMemoryDatabaseImpl(), new SchemaConfig()); + databasePort = new InMemoryDatabaseAdapter(new InMemoryDatabaseImpl(new SchemaConfig())); converter = new GraphToCIMCollectionConverterService(databasePort); packageMapper = Mappers.getMapper(PackageMapper.class); updateDiagramLayoutService = new UpdateDiagramLayoutService(databasePort, converter); @@ -97,7 +96,7 @@ public static void addGraphFromFile(String fileName) { .build() .graph(); databasePort.createGraph(graphIdentifier, graph); - databasePort.getGraphWithContext(graphIdentifier).setDiagramLayout(diagramLayout); + diagramLayout = databasePort.getGraphWithContext(graphIdentifier).getDiagramLayout(); } public static void assertDiagram(UUID packageUUID, String packageName) { @@ -105,7 +104,7 @@ public static void assertDiagram(UUID packageUUID, String packageName) { databasePort .getGraphWithContext(graphIdentifier) .getDiagramLayout() - .getDiagramLayoutModel(); + .getDiagramLayoutModelDirect(); var diagram = model.getResource(new MRID(packageUUID).getFullMRID()); assertThat(diagram).isNotNull(); assertThat(diagram.hasProperty(RDF.type, DL.diagramType)).isTrue(); @@ -119,7 +118,7 @@ public static void assertDiagramDoesNotExist(UUID packageUUID) { databasePort .getGraphWithContext(graphIdentifier) .getDiagramLayout() - .getDiagramLayoutModel(); + .getDiagramLayoutModelDirect(); var diagramResource = ResourceFactory.createResource(new MRID(packageUUID).getFullMRID()); @@ -137,7 +136,7 @@ public static MRID assertDiagramObject(UUID classUUID, UUID packageUUID, String databasePort .getGraphWithContext(graphIdentifier) .getDiagramLayout() - .getDiagramLayoutModel(); + .getDiagramLayoutModelDirect(); var diagramObjects = model.listSubjectsWithProperty( @@ -171,7 +170,7 @@ public static void assertClassDiagramObjectsDoNotExist(UUID classUUID) { databasePort .getGraphWithContext(graphIdentifier) .getDiagramLayout() - .getDiagramLayoutModel(); + .getDiagramLayoutModelDirect(); var diagramObjects = model.listSubjectsWithProperty( @@ -186,7 +185,7 @@ public static void assertSpecificDiagramObjectDoesNotExist(UUID classUUID, UUID databasePort .getGraphWithContext(graphIdentifier) .getDiagramLayout() - .getDiagramLayoutModel(); + .getDiagramLayoutModelDirect(); var classResource = ResourceFactory.createResource(new MRID(classUUID).getFullMRID()); var packageResource = ResourceFactory.createResource(new MRID(packageUUID).getFullMRID()); @@ -210,7 +209,7 @@ public static void assertDiagramObjectPoint(MRID doMRID, float xPosition, float databasePort .getGraphWithContext(graphIdentifier) .getDiagramLayout() - .getDiagramLayoutModel(); + .getDiagramLayoutModelDirect(); var diagramObjectPoints = model.listSubjectsWithProperty( @@ -239,7 +238,7 @@ public static void assertDiagramObjectPointDoesNotExist(MRID doMRID) { databasePort .getGraphWithContext(graphIdentifier) .getDiagramLayout() - .getDiagramLayoutModel(); + .getDiagramLayoutModelDirect(); var diagramObjectPoints = model.listSubjectsWithProperty( @@ -255,7 +254,7 @@ public static void assertDiagramObjectCoordinates( databasePort .getGraphWithContext(graphIdentifier) .getDiagramLayout() - .getDiagramLayoutModel(); + .getDiagramLayoutModelDirect(); var diagramObjects = model.listSubjectsWithProperty( @@ -293,6 +292,5 @@ public static void assertDiagramObjectCoordinates( @AfterEach void tearDown() { databasePort.deleteGraph(graphIdentifier); - diagramLayout = new DiagramLayout(); } } diff --git a/backend/src/test/java/org/rdfarchitect/services/dl/update/UpdateClassLayoutServiceTest.java b/backend/src/test/java/org/rdfarchitect/services/dl/update/UpdateClassLayoutServiceTest.java index 542c901f..b386b12d 100644 --- a/backend/src/test/java/org/rdfarchitect/services/dl/update/UpdateClassLayoutServiceTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/dl/update/UpdateClassLayoutServiceTest.java @@ -119,7 +119,7 @@ void deleteClassLayoutData_classExists_deletesClassLayoutData() { updateDiagramLayoutService.createDiagramLayout(graphIdentifier); var diagramObjects = diagramLayout - .getDiagramLayoutModel() + .getDiagramLayoutModelDirect() .listSubjectsWithProperty( DL.belongsToIdentifiedObject, ResourceFactory.createResource( diff --git a/backend/src/test/java/org/rdfarchitect/services/dl/update/UpdateDiagramLayoutServiceTest.java b/backend/src/test/java/org/rdfarchitect/services/dl/update/UpdateDiagramLayoutServiceTest.java index 5523c852..5db9b765 100644 --- a/backend/src/test/java/org/rdfarchitect/services/dl/update/UpdateDiagramLayoutServiceTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/dl/update/UpdateDiagramLayoutServiceTest.java @@ -19,19 +19,15 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.rdfarchitect.models.cim.rendering.GraphFilter; import org.rdfarchitect.services.dl.DiagramLayoutServicesTestBase; class UpdateDiagramLayoutServiceTest extends DiagramLayoutServicesTestBase { private static UpdateDiagramLayoutService service; - private static GraphFilter graphFilter; @BeforeAll static void setUpEnvironment() { service = new UpdateDiagramLayoutService(databasePort, converter); - graphFilter = new GraphFilter(true); - graphFilter.setPackageUUID(String.valueOf(PACKAGE_A_UUID)); } @Test @@ -51,30 +47,4 @@ void createDiagramLayout_graphWithTwoPackages_createsLayout() { assertInitialClassLayoutData( CLASS_C_UUID, diagramLayout.getDefaultPackageMRID().getUuid(), CLASS_C_LABEL); } - - @Test - void ensureDiagramLayoutExists_emptyDiagramLayout_createsDiagram() { - // Arrange - addGraphFromFile("package.ttl"); - - // Act - var cimCollection = converter.convert(graphIdentifier, graphFilter); - service.ensureDiagramLayoutExists(graphIdentifier, PACKAGE_A_UUID, cimCollection); - - // Assert - assertDiagram(PACKAGE_A_UUID, PACKAGE_A_LABEL); - } - - @Test - void ensureDiagramLayoutExists_emptyDiagramLayout_createsClassLayout() { - // Arrange - addGraphFromFile("package_and_class.ttl"); - - // Act - var cimCollection = converter.convert(graphIdentifier, graphFilter); - service.ensureDiagramLayoutExists(graphIdentifier, PACKAGE_A_UUID, cimCollection); - - // Assert - assertInitialClassLayoutData(CLASS_A_UUID, PACKAGE_A_UUID, CLASS_A_LABEL); - } } diff --git a/backend/src/test/java/org/rdfarchitect/services/dl/update/UpdatePackageLayoutServiceTest.java b/backend/src/test/java/org/rdfarchitect/services/dl/update/UpdatePackageLayoutServiceTest.java index a9d948c9..ff5d924e 100644 --- a/backend/src/test/java/org/rdfarchitect/services/dl/update/UpdatePackageLayoutServiceTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/dl/update/UpdatePackageLayoutServiceTest.java @@ -61,7 +61,7 @@ void deletePackageLayoutData_fullGraph_deletesSpecificPackageLayoutData() { updateDiagramLayoutService.createDiagramLayout(graphIdentifier); var diagramAobjectsList = diagramLayout - .getDiagramLayoutModel() + .getDiagramLayoutModelDirect() .listSubjectsWithProperty( DL.belongsToDiagram, ResourceFactory.createResource( @@ -76,7 +76,10 @@ void deletePackageLayoutData_fullGraph_deletesSpecificPackageLayoutData() { ResourceFactory.createResource( new MRID(CLASS_A_UUID).getFullMRID()))) .findFirst() - .orElse(null); + .orElseThrow( + () -> + new AssertionError( + "Expected diagram object for CLASS_A_UUID not found")); var doB = diagramAobjectsList.stream() .filter( @@ -86,7 +89,10 @@ void deletePackageLayoutData_fullGraph_deletesSpecificPackageLayoutData() { ResourceFactory.createResource( new MRID(CLASS_B_UUID).getFullMRID()))) .findFirst() - .orElse(null); + .orElseThrow( + () -> + new AssertionError( + "Expected diagram object for CLASS_B_UUID not found")); var doAmRID = new MRID(DLUtils.extractUUIDFromMRID(doA.getURI())); var doBmRID = new MRID(DLUtils.extractUUIDFromMRID(doB.getURI())); diff --git a/backend/src/test/java/org/rdfarchitect/services/extension/ClassExtensionServiceTest.java b/backend/src/test/java/org/rdfarchitect/services/extension/ClassExtensionServiceTest.java index 8938d989..49e342a0 100644 --- a/backend/src/test/java/org/rdfarchitect/services/extension/ClassExtensionServiceTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/extension/ClassExtensionServiceTest.java @@ -18,12 +18,11 @@ package org.rdfarchitect.services.extension; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; import static utils.TestUtils.readMultipartFileFromFile; import org.apache.jena.graph.NodeFactory; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.RDFNode; import org.apache.jena.vocabulary.RDF; @@ -38,7 +37,6 @@ import org.rdfarchitect.database.inmemory.InMemoryDatabaseAdapter; import org.rdfarchitect.database.inmemory.InMemoryDatabaseImpl; import org.rdfarchitect.rdf.graph.source.builder.implementations.GraphFileSourceBuilderImpl; -import org.rdfarchitect.services.ChangeLogService; import org.rdfarchitect.services.ClassExtensionService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -57,11 +55,8 @@ class ClassExtensionServiceTest { @BeforeEach void setUp() { SessionContext.setSessionId(UUID.randomUUID().toString()); - var schemaConfig = new SchemaConfig(); - databasePort = new InMemoryDatabaseAdapter(new InMemoryDatabaseImpl(), schemaConfig); - var mockChangeLogService = mock(ChangeLogService.class); - classExtensionService = - new ClassExtensionService(databasePort, classMapper, mockChangeLogService); + databasePort = new InMemoryDatabaseAdapter(new InMemoryDatabaseImpl(new SchemaConfig())); + classExtensionService = new ClassExtensionService(databasePort, classMapper); } @Test @@ -93,19 +88,18 @@ void extendClass_classWithSuperClass_copiesClasses() { // assert DTO returned assertThat(result).isNotNull(); - var targetGraph = databasePort.getGraphWithContext(targetGraphId).getRdfGraph(); - var sourceGraph = databasePort.getGraphWithContext(sourceGraphId).getRdfGraph(); - var targetModel = ModelFactory.createModelForGraph(targetGraph); - var sourceModel = ModelFactory.createModelForGraph(sourceGraph); - var ex = "http://example.org#"; var cims = "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#"; var rdfa = "http://example.org#uuid"; - try { - sourceGraph.begin(TxnType.READ); - targetGraph.begin(TxnType.READ); + try (var sourceCtx = databasePort.getGraphWithContext(sourceGraphId).begin(ReadWrite.READ); + var targetCtx = + databasePort.getGraphWithContext(targetGraphId).begin(ReadWrite.WRITE)) { + var sourceGraph = sourceCtx.getRdfGraph(); + var sourceModel = ModelFactory.createModelForGraph(sourceGraph); + var targetGraph = targetCtx.getRdfGraph(); + var targetModel = ModelFactory.createModelForGraph(targetGraph); // class exists in target assertThat( targetGraph.contains( @@ -190,10 +184,6 @@ void extendClass_classWithSuperClass_copiesClasses() { NodeFactory.createURI(cims + "belongsToCategory"), NodeFactory.createURI(ex + "CorePackage"))) .isTrue(); - - } finally { - targetGraph.end(); - sourceGraph.end(); } } @@ -225,20 +215,15 @@ void extendClass_whenSuperclassAlreadyPresent_skipsSuperclassInsert_butAddsClass var ex = "http://example.org#"; var rdfa = "http://example.org#uuid"; - var targetGraph = databasePort.getGraphWithContext(targetGraphId).getRdfGraph(); - var targetModel = ModelFactory.createModelForGraph(targetGraph); - String existingBaseUuidBefore; - try { - targetGraph.begin(TxnType.READ); + try (var ctx = databasePort.getGraphWithContext(targetGraphId).begin(ReadWrite.READ)) { + var targetModel = ModelFactory.createModelForGraph(ctx.getRdfGraph()); existingBaseUuidBefore = targetModel .getProperty( targetModel.getResource(ex + "Base"), targetModel.createProperty(rdfa)) .getString(); - } finally { - targetGraph.end(); } // act @@ -247,9 +232,9 @@ void extendClass_whenSuperclassAlreadyPresent_skipsSuperclassInsert_butAddsClass // assert assertThat(result).isNotNull(); - try { - targetGraph.begin(TxnType.READ); - + try (var ctx = databasePort.getGraphWithContext(targetGraphId).begin(ReadWrite.READ)) { + var targetGraph = ctx.getRdfGraph(); + var targetModel = ModelFactory.createModelForGraph(targetGraph); // class got added assertThat( targetGraph.contains( @@ -285,9 +270,6 @@ void extendClass_whenSuperclassAlreadyPresent_skipsSuperclassInsert_butAddsClass (RDFNode) null) .toList(); assertThat(baseUuidStatements).hasSize(1); - - } finally { - targetGraph.end(); } } @@ -321,15 +303,13 @@ void extendClass_whenNoCorePackagePresent_addsClassWithoutBelongsToCategory() { // assert assertThat(result).isNotNull(); - var targetGraph = databasePort.getGraphWithContext(targetGraphId).getRdfGraph(); - var targetModel = ModelFactory.createModelForGraph(targetGraph); - var ex = "http://example.org#"; var cimsBelongsToCategory = "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#belongsToCategory"; - try { - targetGraph.begin(TxnType.READ); + try (var ctx = databasePort.getGraphWithContext(targetGraphId).begin(ReadWrite.READ)) { + var targetGraph = ctx.getRdfGraph(); + var targetModel = ModelFactory.createModelForGraph(targetGraph); // class still gets inserted assertThat( @@ -348,9 +328,6 @@ void extendClass_whenNoCorePackagePresent_addsClassWithoutBelongsToCategory() { (RDFNode) null) .toList(); assertThat(categoryStatements).isEmpty(); - - } finally { - targetGraph.end(); } } } diff --git a/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMCollectionMermaidServiceTest.java b/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMCollectionMermaidServiceTest.java index 530d68e5..0b87463a 100644 --- a/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMCollectionMermaidServiceTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMCollectionMermaidServiceTest.java @@ -43,7 +43,7 @@ void renderUML_emptyCollection_emptyMermaidDiagram() { // Arrange // Act - var result = mermaidRenderer.renderUML(cimCollection, null, null); + var result = mermaidRenderer.renderUML(cimCollection, null); // Assert assertThat(result).isNull(); @@ -52,7 +52,7 @@ void renderUML_emptyCollection_emptyMermaidDiagram() { @Test void renderUML_nullCollection_throwsException() { // Assert/Act - assertThatException().isThrownBy(() -> mermaidRenderer.renderUML(null, null, null)); + assertThatException().isThrownBy(() -> mermaidRenderer.renderUML(null, null)); } @Test @@ -62,7 +62,7 @@ void renderUML_collectionWithOnlyPackages_emptyMermaidDiagram() { addPackage("package_package2"); // Act - var result = mermaidRenderer.renderUML(cimCollection, null, null); + var result = mermaidRenderer.renderUML(cimCollection, null); // Assert assertThat(result).isNull(); @@ -75,8 +75,7 @@ void renderUML_collectionWithOneAbstractClass_MermaidStringContainingOneClass() var class1 = cimCollection.getClasses().getFirst(); // Act - var result = - ((MermaidDTO) mermaidRenderer.renderUML(cimCollection, null, null)).mermaidString(); + var result = ((MermaidDTO) mermaidRenderer.renderUML(cimCollection, null)).mermaidString(); Pattern pattern = Pattern.compile("[a-f0-9-]{36}"); Matcher matcher = pattern.matcher(result); @@ -109,8 +108,7 @@ void renderUML_collectionWithOneConcreteClass_MermaidStringContainingOneClass() new ArrayList<>(List.of(new CIMSStereotype(CIMStereotypes.concrete.getURI())))); // Act - var result = - ((MermaidDTO) mermaidRenderer.renderUML(cimCollection, null, null)).mermaidString(); + var result = ((MermaidDTO) mermaidRenderer.renderUML(cimCollection, null)).mermaidString(); Pattern pattern = Pattern.compile("[a-f0-9-]{36}"); Matcher matcher = pattern.matcher(result); @@ -138,12 +136,11 @@ void renderUML_collectionWithOneConcreteClass_MermaidStringContainingOneClass() // Arrange addClass(null, "class1"); var class1 = cimCollection.getClasses().getFirst(); - addAttribute("class1", "attribute1", XSDDatatype.XSDstring); - addAttribute("class1", "attribute2", XSDDatatype.XSDint); + addAttribute("attribute1", XSDDatatype.XSDstring); + addAttribute("attribute2", XSDDatatype.XSDint); // Act - var result = - ((MermaidDTO) mermaidRenderer.renderUML(cimCollection, null, null)).mermaidString(); + var result = ((MermaidDTO) mermaidRenderer.renderUML(cimCollection, null)).mermaidString(); Pattern pattern = Pattern.compile("[a-f0-9-]{36}"); Matcher matcher = pattern.matcher(result); @@ -177,8 +174,7 @@ void renderUML_collectionWithMultipleClasses_MermaidStringContainingMultipleClas addClass(null, "class3"); // Act - var result = - ((MermaidDTO) mermaidRenderer.renderUML(cimCollection, null, null)).mermaidString(); + var result = ((MermaidDTO) mermaidRenderer.renderUML(cimCollection, null)).mermaidString(); Pattern pattern = Pattern.compile("[a-f0-9-]{36}"); Matcher matcher = pattern.matcher(result); @@ -196,11 +192,10 @@ void renderUML_collectionWithOneAssociation_MermaidStringContainingOneAssociatio // Arrange addClass(null, "class1"); addClass(null, "class2"); - addAssociation("class1", "class2", AssociationUsed.YES, AssociationUsed.YES); + addAssociation("class2", AssociationUsed.YES); // Act - var result = - ((MermaidDTO) mermaidRenderer.renderUML(cimCollection, null, null)).mermaidString(); + var result = ((MermaidDTO) mermaidRenderer.renderUML(cimCollection, null)).mermaidString(); Pattern pattern = Pattern.compile("[a-f0-9-]{36}"); Matcher matcher = pattern.matcher(result); @@ -224,8 +219,7 @@ void renderUML_collectionWithClassesInPackages_MermaidStringContainingClassesInP addClass("package_package2", "class2"); // Act - var result = - ((MermaidDTO) mermaidRenderer.renderUML(cimCollection, null, null)).mermaidString(); + var result = ((MermaidDTO) mermaidRenderer.renderUML(cimCollection, null)).mermaidString(); Pattern pattern = Pattern.compile("[a-f0-9-]{36}"); Matcher matcher = pattern.matcher(result); @@ -241,12 +235,11 @@ void renderUML_collectionWithClassesInPackages_MermaidStringContainingClassesInP @Test void renderUML_collectionWithEnum_MermaidStringContainingEnum() { // Arrange - addEnum(null, "enum1"); + addEnum(null); var enum1 = cimCollection.getEnums().getFirst(); // Act - var result = - ((MermaidDTO) mermaidRenderer.renderUML(cimCollection, null, null)).mermaidString(); + var result = ((MermaidDTO) mermaidRenderer.renderUML(cimCollection, null)).mermaidString(); Pattern pattern = Pattern.compile("[a-f0-9-]{36}"); Matcher matcher = pattern.matcher(result); @@ -282,17 +275,16 @@ void renderUML_complexCollection_ContainsAllElements() { var class1 = classesIterator.next(); var class2 = classesIterator.next(); class1.setSuperClass(new RDFSSubClassOf(class2.getUri(), class2.getLabel())); - addAttribute("class1", "attribute1", XSDDatatype.XSDstring); - addAttribute("class1", "attribute2", XSDDatatype.XSDint); - addAssociation("class1", "class2", AssociationUsed.YES, AssociationUsed.YES); - addAssociation("class1", "enum1", AssociationUsed.YES, AssociationUsed.NO); - addEnum("package_package1", "enum1"); - addEnumEntry("enum1", "enumEntry1"); - addEnumEntry("enum1", "enumEntry2"); + addAttribute("attribute1", XSDDatatype.XSDstring); + addAttribute("attribute2", XSDDatatype.XSDint); + addAssociation("class2", AssociationUsed.YES); + addAssociation("enum1", AssociationUsed.NO); + addEnum("package_package1"); + addEnumEntry("enumEntry1"); + addEnumEntry("enumEntry2"); // Act - var result = - ((MermaidDTO) mermaidRenderer.renderUML(cimCollection, null, null)).mermaidString(); + var result = ((MermaidDTO) mermaidRenderer.renderUML(cimCollection, null)).mermaidString(); // Assert assertThat(result.replaceAll("[a-f0-9-]{36}", "UUID")) diff --git a/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMCollectionSvelteFlowServiceTest.java b/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMCollectionSvelteFlowServiceTest.java index fc39eabe..b443383e 100644 --- a/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMCollectionSvelteFlowServiceTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMCollectionSvelteFlowServiceTest.java @@ -39,7 +39,7 @@ void renderUML_emptyCollection_emptyArrays() { // Arrange // Act - var result = (SvelteFlowDTO) svelteFlowRenderer.renderUML(cimCollection, null, null); + var result = (SvelteFlowDTO) svelteFlowRenderer.renderUML(cimCollection, null); // Assert assertThat(result.getNodes()).isEmpty(); @@ -48,7 +48,7 @@ void renderUML_emptyCollection_emptyArrays() { @Test void renderUML_nullCollection_throwsException() { - assertThatException().isThrownBy(() -> svelteFlowRenderer.renderUML(null, null, null)); + assertThatException().isThrownBy(() -> svelteFlowRenderer.renderUML(null, null)); } @Test @@ -58,7 +58,7 @@ void renderUML_collectionWithOnlyPackages_emptyMermaidDiagram() { addPackage("package_package2"); // Act - var result = (SvelteFlowDTO) svelteFlowRenderer.renderUML(cimCollection, null, null); + var result = (SvelteFlowDTO) svelteFlowRenderer.renderUML(cimCollection, null); // Assert assertThat(result.getNodes()).isEmpty(); @@ -81,8 +81,8 @@ void renderUML_singleClass_createsNodeWithCorrectData() { new URI("http://example.com/Category#package_package1"), new RDFSLabel("package1", "en"), UUID.randomUUID())); - addAttribute("class1", "attribute1", XSDDatatype.XSDstring); - addAttribute("class1", "attribute2", XSDDatatype.XSDint); + addAttribute("attribute1", XSDDatatype.XSDstring); + addAttribute("attribute2", XSDDatatype.XSDint); var attributeIterator = cimCollection.getAttributes().iterator(); var attribute1 = attributeIterator.next(); attribute1.setMultiplicity( @@ -94,7 +94,7 @@ void renderUML_singleClass_createsNodeWithCorrectData() { "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#0...n")); // Act - var result = (SvelteFlowDTO) svelteFlowRenderer.renderUML(cimCollection, null, null); + var result = (SvelteFlowDTO) svelteFlowRenderer.renderUML(cimCollection, null); // Assert var nodeDTO = result.getNodes().getFirst(); @@ -120,12 +120,12 @@ void renderUML_singleClass_createsNodeWithCorrectData() { void renderUML_singleEnum_createsNodeWithCorrectData() { // Arrange addPackage("package_package1"); - addEnum("package_package1", "enum1"); - addEnumEntry("enum1", "enumEntry1"); - addEnumEntry("enum1", "enumEntry2"); + addEnum("package_package1"); + addEnumEntry("enumEntry1"); + addEnumEntry("enumEntry2"); // Act - var result = (SvelteFlowDTO) svelteFlowRenderer.renderUML(cimCollection, null, null); + var result = (SvelteFlowDTO) svelteFlowRenderer.renderUML(cimCollection, null); // Assert var nodeDTO = result.getNodes().getFirst(); @@ -152,7 +152,7 @@ void renderUML_superClassAndSubClass_createsInheritanceEdge() { subClass.setSuperClass(new RDFSSubClassOf(superClass.getUri(), superClass.getLabel())); // Act - var result = (SvelteFlowDTO) svelteFlowRenderer.renderUML(cimCollection, null, null); + var result = (SvelteFlowDTO) svelteFlowRenderer.renderUML(cimCollection, null); // Assert var inheritanceEdgeDTO = result.getEdges().getFirst(); @@ -171,7 +171,7 @@ void renderUML_twoClassesAndAssociation_createsAssociationEdge() { addPackage("package_package1"); addClass("package_package1", "class1"); addClass("package_package2", "class2"); - addAssociation("class1", "class2", AssociationUsed.YES, AssociationUsed.NO); + addAssociation("class2", AssociationUsed.NO); var associationIterator = cimCollection.getAssociations().iterator(); var fromAssoc = associationIterator.next(); var toAssoc = associationIterator.next(); @@ -183,7 +183,7 @@ void renderUML_twoClassesAndAssociation_createsAssociationEdge() { "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#1...1")); // Act - var result = (SvelteFlowDTO) svelteFlowRenderer.renderUML(cimCollection, null, null); + var result = (SvelteFlowDTO) svelteFlowRenderer.renderUML(cimCollection, null); // Assert var associationEdgeDTO = result.getEdges().getFirst(); diff --git a/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMCollectionTestBase.java b/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMCollectionTestBase.java index 0ecce28d..1c115e0a 100644 --- a/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMCollectionTestBase.java +++ b/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMCollectionTestBase.java @@ -17,14 +17,11 @@ package org.rdfarchitect.services.rendering; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; import org.apache.jena.datatypes.xsd.XSDDatatype; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.rdfarchitect.api.dto.dl.RenderingLayoutData; -import org.rdfarchitect.dl.data.dto.DiagramObjectPoint; -import org.rdfarchitect.dl.data.dto.relations.XYZPosition; import org.rdfarchitect.models.cim.data.dto.CIMAssociation; import org.rdfarchitect.models.cim.data.dto.CIMAttribute; import org.rdfarchitect.models.cim.data.dto.CIMClass; @@ -46,11 +43,9 @@ import org.rdfarchitect.models.cim.rendering.mermaid.RenderCIMCollectionMermaidService; import org.rdfarchitect.models.cim.rendering.svelteflow.RenderCIMCollectionSvelteFlowService; import org.rdfarchitect.services.dl.select.FetchRenderingLayoutDataUseCase; -import org.rdfarchitect.services.dl.update.EnsureDiagramLayoutForCIMCollectionUseCase; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.UUID; class RenderCIMCollectionTestBase { @@ -62,33 +57,10 @@ class RenderCIMCollectionTestBase { @BeforeAll static void setUpEnvironment() { - // das anfragen der layout informationen wird weg gemocked - FetchRenderingLayoutDataUseCase fetchRenderingLayoutDataUseCase = - mock(FetchRenderingLayoutDataUseCase.class); - EnsureDiagramLayoutForCIMCollectionUseCase ensureDiagramLayoutForCIMCollectionUseCase = - mock(EnsureDiagramLayoutForCIMCollectionUseCase.class); - - var mockXYPosition = mock(XYZPosition.class); - when(mockXYPosition.getX()).thenReturn(0f); - when(mockXYPosition.getY()).thenReturn(0f); - - var mockDop = mock(DiagramObjectPoint.class); - when(mockDop.getPosition()).thenReturn(mockXYPosition); - - var mockMap = mock(Map.class); - when(mockMap.get(any(UUID.class))).thenReturn(mockDop); - - var mockLayoutData = RenderingLayoutData.builder().classLayoutingData(mockMap).build(); - when(fetchRenderingLayoutDataUseCase.fetchRenderingLayoutData(any(), any())) - .thenReturn(mockLayoutData); - when(fetchRenderingLayoutDataUseCase.fetchGlobalRenderingLayoutData(any(), any())) - .thenReturn(mockLayoutData); - + var fetchRenderingLayoutDataUseCase = mock(FetchRenderingLayoutDataUseCase.class); mermaidRenderer = new RenderCIMCollectionMermaidService(); svelteFlowRenderer = - new RenderCIMCollectionSvelteFlowService( - fetchRenderingLayoutDataUseCase, - ensureDiagramLayoutForCIMCollectionUseCase); + new RenderCIMCollectionSvelteFlowService(fetchRenderingLayoutDataUseCase); } @BeforeEach @@ -125,8 +97,8 @@ protected void addClass(String packageLabel, String classLabel) { cimCollection.getClasses().add(cimClass.build()); } - protected void addAttribute(String classLabel, String attributeLabel, XSDDatatype datatype) { - var uri = new URI(URI_PREFIX + classLabel + "." + attributeLabel); + protected void addAttribute(String attributeLabel, XSDDatatype datatype) { + var uri = new URI(URI_PREFIX + "class1" + "." + attributeLabel); var label = new RDFSLabel(attributeLabel); var dataTypeUri = new URI(datatype.getURI()); @@ -141,8 +113,7 @@ protected void addAttribute(String classLabel, String attributeLabel, XSDDatatyp .dataType(dataType) .domain( new RDFSDomain( - new URI(URI_PREFIX + classLabel), - new RDFSLabel(classLabel))) + new URI(URI_PREFIX + "class1"), new RDFSLabel("class1"))) .multiplicity(new CIMSMultiplicity(URI_PREFIX + "M:1")) .stereotype(new CIMSStereotype(CIMStereotypes.attribute.getURI())) .build(); @@ -150,18 +121,14 @@ protected void addAttribute(String classLabel, String attributeLabel, XSDDatatyp cimCollection.getAttributes().add(attribute); } - protected void addAssociation( - String domainLabel, - String rangeLabel, - AssociationUsed fromAssociationUsed, - AssociationUsed toAssociationUsed) { - var fromUri = new URI(URI_PREFIX + domainLabel + "." + rangeLabel); - var toUri = new URI(URI_PREFIX + rangeLabel + "." + domainLabel); + protected void addAssociation(String rangeLabel, AssociationUsed toAssociationUsed) { + var fromUri = new URI(URI_PREFIX + "class1" + "." + rangeLabel); + var toUri = new URI(URI_PREFIX + rangeLabel + "." + "class1"); var fromLabel = new RDFSLabel(fromUri.getSuffix()); var toLabel = new RDFSLabel(toUri.getSuffix()); - var domainUri = new URI(URI_PREFIX + domainLabel); - var domainRDFSLabel = new RDFSLabel(domainLabel); + var domainUri = new URI(URI_PREFIX + "class1"); + var domainRDFSLabel = new RDFSLabel("class1"); var rangeUri = new URI(URI_PREFIX + rangeLabel); var rangeRDFSLabel = new RDFSLabel(rangeLabel); @@ -176,7 +143,7 @@ protected void addAssociation( .inverseRoleName(new CIMSInverseRoleName(toUri)) .associationUsed( new CIMSAssociationUsed( - fromAssociationUsed == AssociationUsed.YES ? "Yes" : "No")) + AssociationUsed.YES == AssociationUsed.YES ? "Yes" : "No")) .multiplicity(new CIMSMultiplicity(URI_PREFIX + "M:1")) .build(); @@ -198,9 +165,9 @@ protected void addAssociation( cimCollection.getAssociations().add(to); } - protected void addEnum(String packageLabel, String enumLabel) { - var uri = new URI(URI_PREFIX + enumLabel); - var label = new RDFSLabel(enumLabel); + protected void addEnum(String packageLabel) { + var uri = new URI(URI_PREFIX + "enum1"); + var label = new RDFSLabel("enum1"); var cimEnum = CIMClass.builder() @@ -221,15 +188,13 @@ protected void addEnum(String packageLabel, String enumLabel) { cimCollection.getEnums().add(cimEnum.build()); } - protected void addEnumEntry(String enumLabel, String enumEntryLabel) { + protected void addEnumEntry(String enumEntryLabel) { var enumEntry = CIMEnumEntry.builder() .uuid(UUID.randomUUID()) .uri(new URI(URI_PREFIX + enumEntryLabel)) .label(new RDFSLabel(enumEntryLabel)) - .type( - new RDFType( - new URI(URI_PREFIX + enumLabel), new RDFSLabel(enumLabel))) + .type(new RDFType(new URI(URI_PREFIX + "enum1"), new RDFSLabel("enum1"))) .build(); cimCollection.getEnumEntries().add(enumEntry); diff --git a/backend/src/test/java/org/rdfarchitect/services/schemaComparison/SchemaComparisonServiceTest.java b/backend/src/test/java/org/rdfarchitect/services/schemaComparison/SchemaComparisonServiceTest.java index c05541b5..87d4215b 100644 --- a/backend/src/test/java/org/rdfarchitect/services/schemaComparison/SchemaComparisonServiceTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/schemaComparison/SchemaComparisonServiceTest.java @@ -57,7 +57,7 @@ class SchemaComparisonServiceTest { @BeforeEach void setUp() { - databasePort = new InMemoryDatabaseAdapter(new InMemoryDatabaseImpl(), new SchemaConfig()); + databasePort = new InMemoryDatabaseAdapter(new InMemoryDatabaseImpl(new SchemaConfig())); service = new SchemaComparisonService(databasePort); var file = readMultipartFileFromFile(PATH, "inMemoryGraph.ttl"); diff --git a/backend/src/test/java/org/rdfarchitect/services/update/CopyClassServiceTest.java b/backend/src/test/java/org/rdfarchitect/services/update/CopyClassServiceTest.java new file mode 100644 index 00000000..8ad92a90 --- /dev/null +++ b/backend/src/test/java/org/rdfarchitect/services/update/CopyClassServiceTest.java @@ -0,0 +1,276 @@ +/* + * 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.update; + +import static org.assertj.core.api.Assertions.assertThat; + +import static utils.TestUtils.readMultipartFileFromFile; + +import org.apache.jena.graph.Node; +import org.apache.jena.graph.NodeFactory; +import org.apache.jena.query.ReadWrite; +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.rdfarchitect.api.dto.CopyClassRequestDTO; +import org.rdfarchitect.api.dto.packages.PackageDTO; +import org.rdfarchitect.api.dto.packages.PackageMapper; +import org.rdfarchitect.config.SchemaConfig; +import org.rdfarchitect.context.SessionContext; +import org.rdfarchitect.database.DatabasePort; +import org.rdfarchitect.database.GraphIdentifier; +import org.rdfarchitect.database.inmemory.InMemoryDatabaseAdapter; +import org.rdfarchitect.database.inmemory.InMemoryDatabaseImpl; +import org.rdfarchitect.models.cim.data.dto.relations.RDFSLabel; +import org.rdfarchitect.rdf.graph.source.builder.implementations.GraphFileSourceBuilderImpl; +import org.rdfarchitect.services.update.classes.CopyClassService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.UUID; + +@SpringBootTest +class CopyClassServiceTest { + + private CopyClassService copyClassService; + private DatabasePort databasePort; + private final GraphIdentifier graphIdentifier = new GraphIdentifier("default", "default"); + + @Autowired private PackageMapper packageMapper; + + private static final String PATH = "src/test/java/org/rdfarchitect/services/update/"; + private static final String PREFIX = "http://example.org#"; + private static final String CLASS_UUID = "43836908-c7f7-4749-bb8b-3ac9250de655"; + + @BeforeEach + void setUp() { + SessionContext.setSessionId(UUID.randomUUID().toString()); + databasePort = new InMemoryDatabaseAdapter(new InMemoryDatabaseImpl(new SchemaConfig())); + copyClassService = new CopyClassService(databasePort, packageMapper, false); + var file = readMultipartFileFromFile(PATH, "class.ttl"); + var graphSource = + new GraphFileSourceBuilderImpl() + .setFile(file) + .setGraphName(graphIdentifier.graphUri()) + .build(); + databasePort.createGraph(graphIdentifier, graphSource.graph()); + } + + @Test + void copyClass_copyExistingClass() { + var targetPackageDTO = + PackageDTO.builder() + .uuid(UUID.fromString("75844dc0-d937-4184-bf6b-d35d8ca6d92a")) + .prefix(PREFIX) + .label("newPackage") + .build(); + + var request = new CopyClassRequestDTO(); + request.setTargetPackage(targetPackageDTO); + request.setCopyAsAbstract(false); + request.setCopyAttributes(true); + request.setCopyAssociations(false); + + copyClassService.copyClass( + graphIdentifier, UUID.fromString(CLASS_UUID), graphIdentifier, request); + + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + assertThat( + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PREFIX + "oldLabel-Copy"), + RDF.type.asNode(), + RDFS.Class.asNode())) + .isTrue(); + + assertThat( + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PREFIX + "oldLabel-Copy"), + RDFS.label.asNode(), + new RDFSLabel("oldLabel-Copy", "en") + .asLangLiteral() + .asNode())) + .isTrue(); + } + } + + @Test + void copyClass_copyExistingClass_abstract() { + var targetPackageDTO = + PackageDTO.builder() + .uuid(UUID.fromString("75844dc0-d937-4184-bf6b-d35d8ca6d92a")) + .prefix(PREFIX) + .label("newPackage") + .build(); + + var request = new CopyClassRequestDTO(); + request.setTargetPackage(targetPackageDTO); + request.setCopyAsAbstract(true); + request.setCopyAttributes(false); + request.setCopyAssociations(false); + + copyClassService.copyClass( + graphIdentifier, UUID.fromString(CLASS_UUID), graphIdentifier, request); + + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + assertThat( + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PREFIX + "oldLabel-Copy"), + RDF.type.asNode(), + RDFS.Class.asNode())) + .isTrue(); + + assertThat( + ctx.getRdfGraph() + .contains( + Node.ANY, + RDFS.domain.asNode(), + NodeFactory.createURI(PREFIX + "oldLabel-Copy"))) + .isFalse(); + } + } + + @Test + void copyClass_copyAsAbstract_doesNotCopySuperClass() { + var targetPackageDTO = + PackageDTO.builder() + .uuid(UUID.fromString("75844dc0-d937-4184-bf6b-d35d8ca6d92a")) + .prefix(PREFIX) + .label("newPackage") + .build(); + + var request = new CopyClassRequestDTO(); + request.setTargetPackage(targetPackageDTO); + request.setCopyAsAbstract(true); + request.setCopyAttributes(false); + request.setCopyAssociations(false); + + copyClassService.copyClass( + graphIdentifier, UUID.fromString(CLASS_UUID), graphIdentifier, request); + + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + var copyUri = NodeFactory.createURI(PREFIX + "oldLabel-Copy"); + assertThat(ctx.getRdfGraph().contains(copyUri, RDFS.subClassOf.asNode(), Node.ANY)) + .isFalse(); + } + } + + @Test + void copyClass_returnsNewClassUUID() { + var targetPackageDTO = + PackageDTO.builder() + .uuid(UUID.fromString("75844dc0-d937-4184-bf6b-d35d8ca6d92a")) + .prefix(PREFIX) + .label("newPackage") + .build(); + + var request = new CopyClassRequestDTO(); + request.setTargetPackage(targetPackageDTO); + request.setCopyAsAbstract(false); + request.setCopyAttributes(false); + request.setCopyAssociations(false); + + var response = + copyClassService.copyClass( + graphIdentifier, UUID.fromString(CLASS_UUID), graphIdentifier, request); + + assertThat(response).isNotNull(); + assertThat(response.getUuid()).isNotEqualTo(CLASS_UUID); + } + + @Test + void copyClass_copyTwice_secondCopyGetsCounter() { + var targetPackageDTO = + PackageDTO.builder() + .uuid(UUID.fromString("75844dc0-d937-4184-bf6b-d35d8ca6d92a")) + .prefix(PREFIX) + .label("newPackage") + .build(); + + var request = new CopyClassRequestDTO(); + request.setTargetPackage(targetPackageDTO); + request.setCopyAsAbstract(false); + request.setCopyAttributes(false); + request.setCopyAssociations(false); + + copyClassService.copyClass( + graphIdentifier, UUID.fromString(CLASS_UUID), graphIdentifier, request); + copyClassService.copyClass( + graphIdentifier, UUID.fromString(CLASS_UUID), graphIdentifier, request); + + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + assertThat( + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PREFIX + "oldLabel-Copy"), + RDFS.label.asNode(), + new RDFSLabel("oldLabel-Copy", "en") + .asLangLiteral() + .asNode())) + .isTrue(); + + assertThat( + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PREFIX + "oldLabel-Copy(1)"), + RDFS.label.asNode(), + new RDFSLabel("oldLabel-Copy(1)", "en") + .asLangLiteral() + .asNode())) + .isTrue(); + } + } + + @Test + void copyClass_copyThreeTimes_thirdCopyGetsIncrementedCounter() { + var targetPackageDTO = + PackageDTO.builder() + .uuid(UUID.fromString("75844dc0-d937-4184-bf6b-d35d8ca6d92a")) + .prefix(PREFIX) + .label("newPackage") + .build(); + + var request = new CopyClassRequestDTO(); + request.setTargetPackage(targetPackageDTO); + request.setCopyAsAbstract(false); + request.setCopyAttributes(false); + request.setCopyAssociations(false); + + copyClassService.copyClass( + graphIdentifier, UUID.fromString(CLASS_UUID), graphIdentifier, request); + copyClassService.copyClass( + graphIdentifier, UUID.fromString(CLASS_UUID), graphIdentifier, request); + copyClassService.copyClass( + graphIdentifier, UUID.fromString(CLASS_UUID), graphIdentifier, request); + + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + assertThat( + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PREFIX + "oldLabel-Copy(2)"), + RDFS.label.asNode(), + new RDFSLabel("oldLabel-Copy(2)", "en") + .asLangLiteral() + .asNode())) + .isTrue(); + } + } +} diff --git a/backend/src/test/java/org/rdfarchitect/services/update/UpdateClassServiceTest.java b/backend/src/test/java/org/rdfarchitect/services/update/UpdateClassServiceTest.java index 10816ab6..736c536e 100644 --- a/backend/src/test/java/org/rdfarchitect/services/update/UpdateClassServiceTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/update/UpdateClassServiceTest.java @@ -25,7 +25,7 @@ import org.apache.jena.graph.Node; import org.apache.jena.graph.NodeFactory; -import org.apache.jena.query.TxnType; +import org.apache.jena.query.ReadWrite; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.RDFNode; import org.apache.jena.vocabulary.RDF; @@ -34,7 +34,6 @@ import org.junit.jupiter.api.Test; import org.rdfarchitect.api.dto.ClassUMLAdaptedDTO; import org.rdfarchitect.api.dto.ClassUMLAdaptedMapper; -import org.rdfarchitect.api.dto.CopyClassRequestDTO; import org.rdfarchitect.api.dto.packages.PackageDTO; import org.rdfarchitect.api.dto.packages.PackageMapper; import org.rdfarchitect.config.SchemaConfig; @@ -48,7 +47,6 @@ import org.rdfarchitect.models.cim.rdf.resources.CIMS; import org.rdfarchitect.models.cim.rdf.resources.RDFA; import org.rdfarchitect.rdf.graph.source.builder.implementations.GraphFileSourceBuilderImpl; -import org.rdfarchitect.services.ChangeLogService; import org.rdfarchitect.services.diagrams.CustomDiagramService; import org.rdfarchitect.services.dl.update.classlayout.UpdateClassLayoutService; import org.rdfarchitect.services.update.classes.UpdateClassService; @@ -74,8 +72,7 @@ class UpdateClassServiceTest { @BeforeEach void setUp() { SessionContext.setSessionId(UUID.randomUUID().toString()); - databasePort = new InMemoryDatabaseAdapter(new InMemoryDatabaseImpl(), new SchemaConfig()); - var mockChangeLogService = mock(ChangeLogService.class); + databasePort = new InMemoryDatabaseAdapter(new InMemoryDatabaseImpl(new SchemaConfig())); var mockUpdateClassLayoutService = mock(UpdateClassLayoutService.class); var mockCustomDiagramService = mock(CustomDiagramService.class); updateClassService = @@ -83,7 +80,6 @@ void setUp() { databasePort, classMapper, packageMapper, - mockChangeLogService, mockUpdateClassLayoutService, mockUpdateClassLayoutService, mockUpdateClassLayoutService, @@ -109,39 +105,37 @@ void addClass_createsNewClass() { updateClassService.addClass(graphIdentifier, packageDTO, PREFIX, "newClass", null); - var graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - try { - graph.begin(TxnType.READ); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { assertThat( - graph.contains( - NodeFactory.createURI(PREFIX + "newClass"), - RDF.type.asNode(), - RDFS.Class.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PREFIX + "newClass"), + RDF.type.asNode(), + RDFS.Class.asNode())) .isTrue(); assertThat( - graph.contains( - NodeFactory.createURI(PREFIX + "newClass"), - RDFS.label.asNode(), - new RDFSLabel("newClass", "en").asLangLiteral().asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PREFIX + "newClass"), + RDFS.label.asNode(), + new RDFSLabel("newClass", "en") + .asLangLiteral() + .asNode())) .isTrue(); - } finally { - graph.end(); } } @Test void addClass_packageWithSameIriExists_throwsConflict() { var packageUri = PREFIX + "packageCollision"; - var graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - try { - graph.begin(TxnType.WRITE); - graph.add( - NodeFactory.createURI(packageUri), - RDF.type.asNode(), - CIMS.classCategory.asNode()); - graph.commit(); - } finally { - graph.end(); + var graphCtx = databasePort.getGraphWithContext(graphIdentifier); + try (var ctx = graphCtx.begin(ReadWrite.WRITE)) { + ctx.getRdfGraph() + .add( + NodeFactory.createURI(packageUri), + RDF.type.asNode(), + CIMS.classCategory.asNode()); + ctx.commit(); } var packageDTO = @@ -162,16 +156,14 @@ void addClass_packageWithSameIriExists_throwsConflict() { .isInstanceOf(ResourceConflictException.class) .hasMessageContaining("package with the same IRI"); - try { - graph.begin(TxnType.READ); + try (var ctx = graphCtx.begin(ReadWrite.READ)) { assertThat( - graph.contains( - NodeFactory.createURI(packageUri), - RDF.type.asNode(), - RDFS.Class.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(packageUri), + RDF.type.asNode(), + RDFS.Class.asNode())) .isFalse(); - } finally { - graph.end(); } } @@ -187,47 +179,52 @@ void replaceClass_replacesExistingClass() { updateClassService.replaceClass(graphIdentifier, newClass); - var graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - try { - graph.begin(TxnType.READ); - - assertThat(graph.contains(NodeFactory.createURI(PREFIX + "class"), Node.ANY, Node.ANY)) + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + assertThat( + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PREFIX + "class"), + Node.ANY, + Node.ANY)) .isFalse(); - assertThat(graph.contains(Node.ANY, Node.ANY, NodeFactory.createURI(PREFIX + "class"))) + assertThat( + ctx.getRdfGraph() + .contains( + Node.ANY, + Node.ANY, + NodeFactory.createURI(PREFIX + "class"))) .isFalse(); assertThat( - graph.contains( - NodeFactory.createURI(PREFIX + "newClass"), - RDF.type.asNode(), - RDFS.Class.asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PREFIX + "newClass"), + RDF.type.asNode(), + RDFS.Class.asNode())) .isTrue(); assertThat( - graph.contains( - NodeFactory.createURI(PREFIX + "newClass"), - RDFS.label.asNode(), - label.asLangLiteral().asNode())) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PREFIX + "newClass"), + RDFS.label.asNode(), + label.asLangLiteral().asNode())) .isTrue(); assertThat( - graph.contains( - NodeFactory.createURI(PREFIX + "subClass"), - RDFS.subClassOf.asNode(), - NodeFactory.createURI(PREFIX + "newClass"))) + ctx.getRdfGraph() + .contains( + NodeFactory.createURI(PREFIX + "subClass"), + RDFS.subClassOf.asNode(), + NodeFactory.createURI(PREFIX + "newClass"))) .isTrue(); - } finally { - graph.end(); } } @Test void deleteClass_removesClassResourceFromGraph() { - updateClassService.deleteClass(graphIdentifier, CLASS_UUID); - var graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - var model = ModelFactory.createModelForGraph(graph); - var classResource = model.createResource(PREFIX + "class"); - - try { - graph.begin(TxnType.READ); + updateClassService.deleteClass(graphIdentifier, UUID.fromString(CLASS_UUID)); + try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.READ)) { + var model = ModelFactory.createModelForGraph(ctx.getRdfGraph()); + var classResource = model.createResource(PREFIX + "class"); var statements = model.listStatements(classResource, null, (RDFNode) null).toList(); assertThat(statements).hasSize(1); assertThat(statements.getFirst().getSubject()).hasToString(PREFIX + "class"); @@ -241,141 +238,6 @@ void deleteClass_removesClassResourceFromGraph() { (RDFNode) null) .hasNext()) .isFalse(); - } finally { - graph.end(); } } - - @Test - void copyClass_copyExistingClass() { - var targetPackageDTO = - PackageDTO.builder() - .uuid(UUID.fromString("75844dc0-d937-4184-bf6b-d35d8ca6d92a")) - .prefix(PREFIX) - .label("newPackage") - .build(); - - var request = new CopyClassRequestDTO(); - request.setTargetPackage(targetPackageDTO); - request.setCopyAsAbstract(false); - request.setCopyAttributes(true); - request.setCopyAssociations(false); - - updateClassService.copyClass( - graphIdentifier, UUID.fromString(CLASS_UUID), graphIdentifier, request); - - var graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - try { - graph.begin(TxnType.READ); - - assertThat( - graph.contains( - NodeFactory.createURI(PREFIX + "oldLabel-Copy"), - RDF.type.asNode(), - RDFS.Class.asNode())) - .isTrue(); - - assertThat( - graph.contains( - NodeFactory.createURI(PREFIX + "oldLabel-Copy"), - RDFS.label.asNode(), - new RDFSLabel("oldLabel-Copy", "en").asLangLiteral().asNode())) - .isTrue(); - } finally { - graph.end(); - } - } - - @Test - void copyClass_copyExistingClass_abstract() { - var targetPackageDTO = - PackageDTO.builder() - .uuid(UUID.fromString("75844dc0-d937-4184-bf6b-d35d8ca6d92a")) - .prefix(PREFIX) - .label("newPackage") - .build(); - - var request = new CopyClassRequestDTO(); - request.setTargetPackage(targetPackageDTO); - request.setCopyAsAbstract(true); - request.setCopyAttributes(false); - request.setCopyAssociations(false); - - updateClassService.copyClass( - graphIdentifier, UUID.fromString(CLASS_UUID), graphIdentifier, request); - - var graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - try { - graph.begin(TxnType.READ); - - assertThat( - graph.contains( - NodeFactory.createURI(PREFIX + "oldLabel-Copy"), - RDF.type.asNode(), - RDFS.Class.asNode())) - .isTrue(); - - assertThat( - graph.contains( - Node.ANY, - RDFS.domain.asNode(), - NodeFactory.createURI(PREFIX + "oldLabel-Copy"))) - .isFalse(); - } finally { - graph.end(); - } - } - - @Test - void copyClass_copyAsAbstract_doesNotCopySuperClass() { - var targetPackageDTO = - PackageDTO.builder() - .uuid(UUID.fromString("75844dc0-d937-4184-bf6b-d35d8ca6d92a")) - .prefix(PREFIX) - .label("newPackage") - .build(); - - var request = new CopyClassRequestDTO(); - request.setTargetPackage(targetPackageDTO); - request.setCopyAsAbstract(true); - request.setCopyAttributes(false); - request.setCopyAssociations(false); - - updateClassService.copyClass( - graphIdentifier, UUID.fromString(CLASS_UUID), graphIdentifier, request); - - var graph = databasePort.getGraphWithContext(graphIdentifier).getRdfGraph(); - try { - graph.begin(TxnType.READ); - - var copyUri = NodeFactory.createURI(PREFIX + "oldLabel-Copy"); - - assertThat(graph.contains(copyUri, RDFS.subClassOf.asNode(), Node.ANY)).isFalse(); - } finally { - graph.end(); - } - } - - @Test - void copyClass_returnsNewClassUUID() { - var targetPackageDTO = - PackageDTO.builder() - .uuid(UUID.fromString("75844dc0-d937-4184-bf6b-d35d8ca6d92a")) - .prefix(PREFIX) - .label("newPackage") - .build(); - - var request = new CopyClassRequestDTO(); - request.setTargetPackage(targetPackageDTO); - request.setCopyAsAbstract(false); - request.setCopyAttributes(false); - request.setCopyAssociations(false); - - var newClassUUID = - updateClassService.copyClass( - graphIdentifier, UUID.fromString(CLASS_UUID), graphIdentifier, request); - - assertThat(newClassUUID).isNotNull(); - assertThat(newClassUUID).isNotEqualTo(UUID.fromString(CLASS_UUID)); - } } diff --git a/backend/src/test/java/org/rdfarchitect/services/update/UpdatePackageServiceTest.java b/backend/src/test/java/org/rdfarchitect/services/update/UpdatePackageServiceTest.java index 093cc0c2..73432ebf 100644 --- a/backend/src/test/java/org/rdfarchitect/services/update/UpdatePackageServiceTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/update/UpdatePackageServiceTest.java @@ -21,6 +21,7 @@ import static org.mockito.Mockito.*; import org.apache.jena.graph.NodeFactory; +import org.apache.jena.query.ReadWrite; import org.apache.jena.shared.PrefixMapping; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; @@ -32,18 +33,16 @@ import org.rdfarchitect.api.dto.packages.PackageDTO; import org.rdfarchitect.api.dto.packages.PackageMapper; import org.rdfarchitect.database.DatabasePort; +import org.rdfarchitect.database.GraphContext; import org.rdfarchitect.database.GraphIdentifier; -import org.rdfarchitect.database.inmemory.GraphWithContext; import org.rdfarchitect.exception.database.ResourceConflictException; -import org.rdfarchitect.models.changelog.ChangeLogEntry; import org.rdfarchitect.models.cim.data.dto.CIMPackage; import org.rdfarchitect.models.cim.data.dto.relations.RDFSComment; import org.rdfarchitect.models.cim.data.dto.relations.RDFSLabel; import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; import org.rdfarchitect.models.cim.queries.update.CIMUpdates; import org.rdfarchitect.rdf.graph.DeltaCompressible; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; -import org.rdfarchitect.services.ChangeLogUseCase; +import org.rdfarchitect.rdf.graph.wrapper.GraphRewindable; import org.rdfarchitect.services.dl.update.packagelayout.UpdatePackageLayoutService; import org.rdfarchitect.services.update.packages.UpdatePackageService; @@ -52,25 +51,24 @@ class UpdatePackageServiceTest { private UpdatePackageService service; - private GraphRewindableWithUUIDs mockGraph; + private GraphRewindable mockGraph; + private GraphContext mockGraphWithContext; private final PackageMapper mapper = Mappers.getMapper(PackageMapper.class); - private ChangeLogUseCase changeLogUseCase; @BeforeEach void setUp() { DatabasePort databasePort = mock(DatabasePort.class); - changeLogUseCase = mock(ChangeLogUseCase.class); var mockUpdatePackageLayoutService = mock(UpdatePackageLayoutService.class); service = new UpdatePackageService( databasePort, mapper, - changeLogUseCase, mockUpdatePackageLayoutService, mockUpdatePackageLayoutService, mockUpdatePackageLayoutService); - mockGraph = mock(GraphRewindableWithUUIDs.class); - var mockGraphWithContext = mock(GraphWithContext.class); + mockGraph = mock(GraphRewindable.class); + mockGraphWithContext = mock(GraphContext.class); + when(mockGraphWithContext.begin(any(ReadWrite.class))).thenReturn(mockGraphWithContext); when(databasePort.getGraphWithContext(any())).thenReturn(mockGraphWithContext); when(mockGraphWithContext.getRdfGraph()).thenReturn(mockGraph); when(databasePort.getPrefixMapping(anyString())).thenReturn(mock(PrefixMapping.class)); @@ -86,12 +84,12 @@ void addPackage_packageWithoutComment_addsPackage() { ArgumentCaptor captor = ArgumentCaptor.forClass(CIMPackage.class); mockedStatic .when(() -> CIMUpdates.insertPackage(eq(mockGraph), any(), captor.capture())) - .thenAnswer(invocation -> null); + .thenAnswer(_ -> null); service.addPackage(new GraphIdentifier("default", "test"), dto); - verify(mockGraph).commit(); - verify(mockGraph).end(); + verify(mockGraphWithContext).commit(anyString()); + verify(mockGraphWithContext).close(); CIMPackage captured = captor.getValue(); assertThat(captured.getUri()).isEqualTo(new URI("http://example.com#TestPackage")); @@ -114,12 +112,12 @@ void addPackage_packageWithComment_addsPackage() { ArgumentCaptor captor = ArgumentCaptor.forClass(CIMPackage.class); mockedStatic .when(() -> CIMUpdates.insertPackage(eq(mockGraph), any(), captor.capture())) - .thenAnswer(invocation -> null); + .thenAnswer(_ -> null); service.addPackage(new GraphIdentifier("default", "test"), dto); - verify(mockGraph).commit(); - verify(mockGraph).end(); + verify(mockGraphWithContext).commit(anyString()); + verify(mockGraphWithContext).close(); CIMPackage captured = captor.getValue(); assertThat(captured.getUri()).isEqualTo(new URI("http://example.com#TestPackage")); @@ -149,7 +147,7 @@ void addPackage_exceptionDuringTransaction_closesGraph() { // expected } - verify(mockGraph).end(); + verify(mockGraphWithContext).close(); } } @@ -166,9 +164,8 @@ void addPackage_classWithSameIriExists_throwsConflict() { .isInstanceOf(ResourceConflictException.class) .hasMessageContaining("class with the same IRI"); - verify(mockGraph, never()).commit(); - verify(mockGraph).end(); - verifyNoInteractions(changeLogUseCase); + verify(mockGraphWithContext, never()).commit(anyString()); + verify(mockGraphWithContext).close(); } @Test @@ -184,12 +181,12 @@ void replacePackage_validPackage_replacesPackage() { ArgumentCaptor captor = ArgumentCaptor.forClass(CIMPackage.class); mockedStatic .when(() -> CIMUpdates.replacePackage(eq(mockGraph), any(), captor.capture())) - .thenAnswer(invocation -> null); + .thenAnswer(_ -> null); service.replacePackage(new GraphIdentifier("default", "test"), dto); - verify(mockGraph).commit(); - verify(mockGraph).end(); + verify(mockGraphWithContext).commit(anyString()); + verify(mockGraphWithContext).close(); CIMPackage captured = captor.getValue(); assertThat(captured.getUri()).isEqualTo(new URI("http://other.org#otherPackage")); @@ -207,17 +204,11 @@ void deletePackage_validUuid_deletesPackageAndRecordsChange() { service.deletePackage(graphIdentifier, packageUuid); mockedStatic.verify( - () -> - CIMUpdates.deletePackage( - eq(mockGraph), any(), eq(packageUuid.toString()))); + () -> CIMUpdates.deletePackage(eq(mockGraph), any(), eq(packageUuid))); } - verify(mockGraph).commit(); - verify(mockGraph).end(); - - ArgumentCaptor captor = ArgumentCaptor.forClass(ChangeLogEntry.class); - verify(changeLogUseCase).recordChange(eq(graphIdentifier), captor.capture()); - assertThat(captor.getValue().getMessage()).isEqualTo("Deleted package " + packageUuid); + verify(mockGraphWithContext).commit("Deleted package " + packageUuid); + verify(mockGraphWithContext).close(); } @Test @@ -227,13 +218,13 @@ void deletePackage_failureDuringDelete_alwaysEndsGraphTransaction() { try (MockedStatic mockedStatic = mockStatic(CIMUpdates.class)) { mockedStatic - .when(() -> CIMUpdates.deletePackage(any(), any(), anyString())) + .when(() -> CIMUpdates.deletePackage(any(), any(), any(UUID.class))) .thenThrow(new RuntimeException("boom")); assertThatThrownBy(() -> service.deletePackage(graphIdentifier, packageUuid)) .isInstanceOf(RuntimeException.class); - verify(mockGraph).end(); + verify(mockGraphWithContext).close(); } } } diff --git a/backend/src/test/java/org/rdfarchitect/services/update/graph/ImportGraphsServiceTest.java b/backend/src/test/java/org/rdfarchitect/services/update/graph/ImportGraphsServiceTest.java index f696a514..4f62d1d5 100644 --- a/backend/src/test/java/org/rdfarchitect/services/update/graph/ImportGraphsServiceTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/update/graph/ImportGraphsServiceTest.java @@ -19,7 +19,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -31,10 +30,7 @@ import org.mockito.ArgumentCaptor; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; -import org.rdfarchitect.database.inmemory.GraphWithContext; import org.rdfarchitect.models.cim.rdf.resources.RDFA; -import org.rdfarchitect.rdf.graph.wrapper.GraphRewindableWithUUIDs; -import org.rdfarchitect.services.ChangeLogUseCase; import org.rdfarchitect.services.dl.update.packagelayout.CreateDiagramLayoutUseCase; import org.springframework.mock.web.MockMultipartFile; @@ -44,17 +40,15 @@ class ImportGraphsServiceTest { private ImportGraphsUseCase importGraphsUseCase; - private ChangeLogUseCase changeLogUseCaseMock; + private CreateDiagramLayoutUseCase createDiagramLayoutUseCaseMock; private DatabasePort databasePortMock; @BeforeEach void setUp() { - changeLogUseCaseMock = mock(ChangeLogUseCase.class); - var createDiagramLayoutUseCaseMock = mock(CreateDiagramLayoutUseCase.class); + createDiagramLayoutUseCaseMock = mock(CreateDiagramLayoutUseCase.class); databasePortMock = mock(DatabasePort.class); importGraphsUseCase = - new ImportGraphsService( - changeLogUseCaseMock, createDiagramLayoutUseCaseMock, databasePortMock); + new ImportGraphsService(createDiagramLayoutUseCaseMock, databasePortMock); } @Test @@ -79,12 +73,6 @@ void importGraphs_sameFileNameTwice_autoGeneratesUniqueGraphUris() { when(databasePortMock.listGraphUris(datasetName)) .thenThrow(new RuntimeException("dataset does not exist")); - var graphWithContextMock = mock(GraphWithContext.class); - when(databasePortMock.getGraphWithContext(any(GraphIdentifier.class))) - .thenReturn(graphWithContextMock); - var graphMock = mock(GraphRewindableWithUUIDs.class, RETURNS_DEEP_STUBS); - when(graphWithContextMock.getRdfGraph()).thenReturn(graphMock); - var result = importGraphsUseCase.importGraphs(datasetName, List.of(file1, file2), null); assertThat(result.failedFileNames()).isEmpty(); @@ -99,6 +87,6 @@ void importGraphs_sameFileNameTwice_autoGeneratesUniqueGraphUris() { .extracting(GraphIdentifier::graphUri) .containsExactly(RDFA.GRAPH_URI + "graph", RDFA.GRAPH_URI + "graph_1"); - verify(changeLogUseCaseMock, times(2)).recordChange(any(GraphIdentifier.class), any()); + verify(createDiagramLayoutUseCaseMock, times(2)).createDiagramLayout(any()); } } diff --git a/frontend/src/lib/rendering/svelteflow/components/SvelteFlowClassContextMenu.svelte b/frontend/src/lib/rendering/svelteflow/components/SvelteFlowClassContextMenu.svelte index 2e90ecfd..8799ee34 100644 --- a/frontend/src/lib/rendering/svelteflow/components/SvelteFlowClassContextMenu.svelte +++ b/frontend/src/lib/rendering/svelteflow/components/SvelteFlowClassContextMenu.svelte @@ -31,7 +31,11 @@ import { ContextMenu } from "$lib/components/bitsui/contextmenu"; import ContextMenuSeparator from "$lib/components/bitsui/contextmenu/ContextMenuSeparator.svelte"; - import { copyState, editorState } from "$lib/sharedState.svelte.js"; + import { + copyState, + DiagramType, + editorState, + } from "$lib/sharedState.svelte.js"; import { getContextMenuTriggerStyle, @@ -253,13 +257,15 @@ - - Remove from Diagram - + {#if editorState.selectedDiagram.getProperty("type") !== DiagramType.PACKAGE} + + Remove from Diagram + + {/if} 0) || - (change.deletions && change.deletions.length > 0) + return change.contextDeltas?.some( + context => + (context.additions && context.additions.length > 0) || + (context.deletions && context.deletions.length > 0), ); } + function isDataLost(change) { + return change.contextDeltas?.some( + context => context.additions === null || context.deletions === null, + ); + } function toggleRowExpanded() { setExpanded(rowKey, !getExpanded(rowKey)); } function formatTimestamp(rawTimestamp) { const date = new Date(rawTimestamp); + const pad = n => n.toString().padStart(2, "0"); + return ( `${pad(date.getDate())}-${pad(date.getMonth() + 1)}-${date.getFullYear()} ` + `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` ); } + + function getAdditionsKey(contextName) { + return `${change.changeId}::${contextName}::additions`; + } + + function getDeletionsKey(contextName) { + return `${change.changeId}::${contextName}::deletions`; + } @@ -95,6 +111,7 @@ {formatTimestamp(change.timestamp)} + {#if hasTriples(change)}