diff --git a/src/main/java/fr/inria/corese/core/api/DataBroker.java b/src/main/java/fr/inria/corese/core/api/DataBroker.java index dc9f53f30..8a7e3fbe9 100644 --- a/src/main/java/fr/inria/corese/core/api/DataBroker.java +++ b/src/main/java/fr/inria/corese/core/api/DataBroker.java @@ -7,7 +7,7 @@ import fr.inria.corese.core.kgram.core.Query; import fr.inria.corese.core.logic.RDF; import fr.inria.corese.core.sparql.datatype.DatatypeMap; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; import java.util.List; diff --git a/src/main/java/fr/inria/corese/core/load/AddTripleHelperDataManager.java b/src/main/java/fr/inria/corese/core/load/AddTripleHelperDataManager.java index b8a446624..04f535d2a 100644 --- a/src/main/java/fr/inria/corese/core/load/AddTripleHelperDataManager.java +++ b/src/main/java/fr/inria/corese/core/load/AddTripleHelperDataManager.java @@ -2,7 +2,7 @@ import fr.inria.corese.core.Graph; import fr.inria.corese.core.logic.Entailment; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; import fr.inria.corese.core.kgram.api.core.Edge; import fr.inria.corese.core.kgram.api.core.Node; import fr.inria.corese.core.sparql.datatype.DatatypeMap; diff --git a/src/main/java/fr/inria/corese/core/load/Load.java b/src/main/java/fr/inria/corese/core/load/Load.java index 86f4ffcd0..2ce517738 100644 --- a/src/main/java/fr/inria/corese/core/load/Load.java +++ b/src/main/java/fr/inria/corese/core/load/Load.java @@ -52,7 +52,7 @@ import fr.inria.corese.core.sparql.triple.parser.Constant; import fr.inria.corese.core.sparql.triple.parser.LoadTurtle; import fr.inria.corese.core.sparql.triple.parser.NSManager; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; import fr.inria.corese.core.util.HTTPHeaders; import fr.inria.corese.core.workflow.SemanticWorkflow; import fr.inria.corese.core.workflow.WorkflowParser; diff --git a/src/main/java/fr/inria/corese/core/load/TripleCreatorBase.java b/src/main/java/fr/inria/corese/core/load/TripleCreatorBase.java index 2896151bc..fc8464214 100644 --- a/src/main/java/fr/inria/corese/core/load/TripleCreatorBase.java +++ b/src/main/java/fr/inria/corese/core/load/TripleCreatorBase.java @@ -17,7 +17,7 @@ import fr.inria.corese.core.sparql.triple.parser.AccessRight; import fr.inria.corese.core.sparql.triple.parser.Atom; import fr.inria.corese.core.sparql.triple.parser.Constant; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; /** * Base class for creating and inserting RDF triples into a Graph. diff --git a/src/main/java/fr/inria/corese/core/load/jsonld/CoreseJsonTripleCallback.java b/src/main/java/fr/inria/corese/core/load/jsonld/CoreseJsonTripleCallback.java index 409eee088..05ef9fd70 100644 --- a/src/main/java/fr/inria/corese/core/load/jsonld/CoreseJsonTripleCallback.java +++ b/src/main/java/fr/inria/corese/core/load/jsonld/CoreseJsonTripleCallback.java @@ -11,7 +11,7 @@ import fr.inria.corese.core.load.AddTripleHelperDataManager; import fr.inria.corese.core.load.ILoadSerialization; import fr.inria.corese.core.load.Load; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; /** * Implementation of interface from Jsonld-java (json-ld parser) for adding diff --git a/src/main/java/fr/inria/corese/core/load/rdfa/CoreseRDFaTripleSink.java b/src/main/java/fr/inria/corese/core/load/rdfa/CoreseRDFaTripleSink.java index c36a290f3..9777c5df4 100644 --- a/src/main/java/fr/inria/corese/core/load/rdfa/CoreseRDFaTripleSink.java +++ b/src/main/java/fr/inria/corese/core/load/rdfa/CoreseRDFaTripleSink.java @@ -9,7 +9,7 @@ import fr.inria.corese.core.load.AddTripleHelperDataManager; import fr.inria.corese.core.load.ILoadSerialization; import fr.inria.corese.core.load.Load; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; /** * Implements the interface TripleSink (from semargl) in order to add the diff --git a/src/main/java/fr/inria/corese/core/logic/BrokerDistanceDataManager.java b/src/main/java/fr/inria/corese/core/logic/BrokerDistanceDataManager.java index d63bb258d..aa01bdab6 100644 --- a/src/main/java/fr/inria/corese/core/logic/BrokerDistanceDataManager.java +++ b/src/main/java/fr/inria/corese/core/logic/BrokerDistanceDataManager.java @@ -5,7 +5,7 @@ import fr.inria.corese.core.Graph; import fr.inria.corese.core.query.QueryProcess; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; import fr.inria.corese.core.kgram.api.core.Edge; import fr.inria.corese.core.kgram.api.core.Node; import fr.inria.corese.core.kgram.core.Mappings; diff --git a/src/main/java/fr/inria/corese/core/logic/ClosureDataManager.java b/src/main/java/fr/inria/corese/core/logic/ClosureDataManager.java index 340cb1b7a..524f270b4 100644 --- a/src/main/java/fr/inria/corese/core/logic/ClosureDataManager.java +++ b/src/main/java/fr/inria/corese/core/logic/ClosureDataManager.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.logic; import fr.inria.corese.core.Graph; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; import fr.inria.corese.core.kgram.api.core.Edge; import fr.inria.corese.core.kgram.api.core.ExprType; import fr.inria.corese.core.kgram.api.core.Node; diff --git a/src/main/java/fr/inria/corese/core/logic/Distance.java b/src/main/java/fr/inria/corese/core/logic/Distance.java index 41152c064..d640bff2a 100644 --- a/src/main/java/fr/inria/corese/core/logic/Distance.java +++ b/src/main/java/fr/inria/corese/core/logic/Distance.java @@ -8,7 +8,7 @@ import org.slf4j.LoggerFactory; import fr.inria.corese.core.Graph; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; import fr.inria.corese.core.kgram.api.core.Node; /** diff --git a/src/main/java/fr/inria/corese/core/producer/DataBrokerConstructExtern.java b/src/main/java/fr/inria/corese/core/producer/DataBrokerConstructExtern.java index f453e67d7..a16a41e9b 100644 --- a/src/main/java/fr/inria/corese/core/producer/DataBrokerConstructExtern.java +++ b/src/main/java/fr/inria/corese/core/producer/DataBrokerConstructExtern.java @@ -7,7 +7,7 @@ import fr.inria.corese.core.api.DataBrokerConstruct; import fr.inria.corese.core.load.Load; import fr.inria.corese.core.load.LoadException; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; import fr.inria.corese.core.kgram.api.core.Edge; import fr.inria.corese.core.kgram.api.core.Node; import fr.inria.corese.core.kgram.core.Query; diff --git a/src/main/java/fr/inria/corese/core/producer/DataBrokerExtern.java b/src/main/java/fr/inria/corese/core/producer/DataBrokerExtern.java index 100023eef..a60ee0aea 100644 --- a/src/main/java/fr/inria/corese/core/producer/DataBrokerExtern.java +++ b/src/main/java/fr/inria/corese/core/producer/DataBrokerExtern.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.producer; import fr.inria.corese.core.api.DataBroker; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; /** * Broker between ProducerImpl and external DataManager diff --git a/src/main/java/fr/inria/corese/core/producer/DataBrokerLocal.java b/src/main/java/fr/inria/corese/core/producer/DataBrokerLocal.java index 2544aee89..ed15ee7bd 100644 --- a/src/main/java/fr/inria/corese/core/producer/DataBrokerLocal.java +++ b/src/main/java/fr/inria/corese/core/producer/DataBrokerLocal.java @@ -4,7 +4,7 @@ import fr.inria.corese.core.Graph; import fr.inria.corese.core.api.DataBroker; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; import fr.inria.corese.core.kgram.api.core.Edge; import fr.inria.corese.core.kgram.api.core.Node; import fr.inria.corese.core.kgram.core.Query; diff --git a/src/main/java/fr/inria/corese/core/producer/MetadataManager.java b/src/main/java/fr/inria/corese/core/producer/MetadataManager.java index d7444650d..c1f8d96c0 100644 --- a/src/main/java/fr/inria/corese/core/producer/MetadataManager.java +++ b/src/main/java/fr/inria/corese/core/producer/MetadataManager.java @@ -3,7 +3,7 @@ import fr.inria.corese.core.kgram.api.core.Edge; import fr.inria.corese.core.kgram.api.core.Node; import fr.inria.corese.core.logic.Distance; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/fr/inria/corese/core/query/DatasetManager.java b/src/main/java/fr/inria/corese/core/query/DatasetManager.java index 03095479c..c6d904512 100644 --- a/src/main/java/fr/inria/corese/core/query/DatasetManager.java +++ b/src/main/java/fr/inria/corese/core/query/DatasetManager.java @@ -5,7 +5,7 @@ import fr.inria.corese.core.rule.RuleEngine; import fr.inria.corese.core.storage.CoreseGraphDataManagerBuilder; import fr.inria.corese.core.storage.DataManagerJava; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; import fr.inria.corese.core.util.Property; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/fr/inria/corese/core/query/PluginImpl.java b/src/main/java/fr/inria/corese/core/query/PluginImpl.java index 555d95503..07a032a3f 100644 --- a/src/main/java/fr/inria/corese/core/query/PluginImpl.java +++ b/src/main/java/fr/inria/corese/core/query/PluginImpl.java @@ -42,9 +42,8 @@ import fr.inria.corese.core.sparql.triple.function.term.Binding; import fr.inria.corese.core.sparql.triple.parser.*; import fr.inria.corese.core.sparql.triple.parser.Access.Level; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; import fr.inria.corese.core.transform.TemplateVisitor; -import fr.inria.corese.core.transform.Transformer; import fr.inria.corese.core.transform.TransformerUtils; import fr.inria.corese.core.util.GraphListen; import fr.inria.corese.core.util.MappingsGraph; diff --git a/src/main/java/fr/inria/corese/core/query/ProducerImpl.java b/src/main/java/fr/inria/corese/core/query/ProducerImpl.java index 0d5fc4db3..a9ea28c3e 100644 --- a/src/main/java/fr/inria/corese/core/query/ProducerImpl.java +++ b/src/main/java/fr/inria/corese/core/query/ProducerImpl.java @@ -19,7 +19,7 @@ import fr.inria.corese.core.sparql.datatype.DatatypeMap; import fr.inria.corese.core.sparql.triple.parser.AccessRight; import fr.inria.corese.core.sparql.triple.parser.Metadata; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/fr/inria/corese/core/query/ProviderService.java b/src/main/java/fr/inria/corese/core/query/ProviderService.java index f9e7b2cce..87dbca379 100644 --- a/src/main/java/fr/inria/corese/core/query/ProviderService.java +++ b/src/main/java/fr/inria/corese/core/query/ProviderService.java @@ -25,7 +25,7 @@ import fr.inria.corese.core.sparql.triple.parser.context.ContextLog; import fr.inria.corese.core.storage.CoreseGraphDataManagerBuilder; import fr.inria.corese.core.storage.DataManagerJava; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; import fr.inria.corese.core.util.Property; import jakarta.ws.rs.ProcessingException; import jakarta.ws.rs.client.ResponseProcessingException; diff --git a/src/main/java/fr/inria/corese/core/query/QueryProcess.java b/src/main/java/fr/inria/corese/core/query/QueryProcess.java index 54bc89fb9..7c61e6926 100755 --- a/src/main/java/fr/inria/corese/core/query/QueryProcess.java +++ b/src/main/java/fr/inria/corese/core/query/QueryProcess.java @@ -35,7 +35,7 @@ import fr.inria.corese.core.sparql.triple.parser.*; import fr.inria.corese.core.sparql.triple.parser.Access.Feature; import fr.inria.corese.core.sparql.triple.parser.Access.Level; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; import fr.inria.corese.core.util.Extension; import fr.inria.corese.core.util.Property; import jakarta.ws.rs.client.ResponseProcessingException; diff --git a/src/main/java/fr/inria/corese/core/query/StorageFactory.java b/src/main/java/fr/inria/corese/core/query/StorageFactory.java index 127d755e7..08c6c6103 100644 --- a/src/main/java/fr/inria/corese/core/query/StorageFactory.java +++ b/src/main/java/fr/inria/corese/core/query/StorageFactory.java @@ -2,7 +2,7 @@ import fr.inria.corese.core.query.DatasetManager.TypeDataBase; import fr.inria.corese.core.sparql.triple.parser.URLServer; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; import java.security.InvalidParameterException; import java.util.Collection; diff --git a/src/main/java/fr/inria/corese/core/rule/Cleaner.java b/src/main/java/fr/inria/corese/core/rule/Cleaner.java index 0ad3ad154..ec72711b6 100644 --- a/src/main/java/fr/inria/corese/core/rule/Cleaner.java +++ b/src/main/java/fr/inria/corese/core/rule/Cleaner.java @@ -10,7 +10,7 @@ import fr.inria.corese.core.query.QueryProcess; import fr.inria.corese.core.sparql.exceptions.EngineException; import fr.inria.corese.core.sparql.triple.function.term.Binding; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; import fr.inria.corese.core.util.Property; import java.io.IOException; diff --git a/src/main/java/fr/inria/corese/core/rule/RuleEngine.java b/src/main/java/fr/inria/corese/core/rule/RuleEngine.java index e087ae759..5371611ea 100644 --- a/src/main/java/fr/inria/corese/core/rule/RuleEngine.java +++ b/src/main/java/fr/inria/corese/core/rule/RuleEngine.java @@ -30,7 +30,7 @@ import fr.inria.corese.core.sparql.triple.parser.Access.Feature; import fr.inria.corese.core.sparql.triple.parser.Access.Level; import fr.inria.corese.core.sparql.triple.printer.SPIN; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; import fr.inria.corese.core.util.Property; import fr.inria.corese.core.visitor.solver.QuerySolverVisitorRule; import org.slf4j.Logger; diff --git a/src/main/java/fr/inria/corese/core/shacl/Shacl.java b/src/main/java/fr/inria/corese/core/shacl/Shacl.java index 7942e865c..571a9fe6a 100644 --- a/src/main/java/fr/inria/corese/core/shacl/Shacl.java +++ b/src/main/java/fr/inria/corese/core/shacl/Shacl.java @@ -7,7 +7,7 @@ import fr.inria.corese.core.logic.RDF; import fr.inria.corese.core.producer.DataProducer; import fr.inria.corese.core.query.QueryProcess; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; import fr.inria.corese.core.kgram.api.core.Edge; import fr.inria.corese.core.sparql.api.IDatatype; import fr.inria.corese.core.sparql.datatype.DatatypeMap; diff --git a/src/main/java/fr/inria/corese/core/storage/CoreseGraphDataManager.java b/src/main/java/fr/inria/corese/core/storage/CoreseGraphDataManager.java index 88aac3850..551251957 100644 --- a/src/main/java/fr/inria/corese/core/storage/CoreseGraphDataManager.java +++ b/src/main/java/fr/inria/corese/core/storage/CoreseGraphDataManager.java @@ -1,258 +1,151 @@ package fr.inria.corese.core.storage; -import java.util.ArrayList; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import fr.inria.corese.core.Graph; import fr.inria.corese.core.producer.MetadataManager; -import fr.inria.corese.core.storage.api.dataManager.DataManager; -import fr.inria.corese.core.kgram.api.core.Edge; -import fr.inria.corese.core.kgram.api.core.Node; -import fr.inria.corese.core.sparql.triple.parser.HashMapList; +import fr.inria.corese.core.storage.api.datamanager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.lifecycle.DataManagerLifecycle; +import fr.inria.corese.core.storage.api.datamanager.operations.BulkOperations; +import fr.inria.corese.core.storage.api.datamanager.operations.MetadataOperations; +import fr.inria.corese.core.storage.api.datamanager.operations.MutationOperations; +import fr.inria.corese.core.storage.api.datamanager.operations.QueryOperations; +import fr.inria.corese.core.storage.api.datamanager.transaction.IsolationLevel; +import fr.inria.corese.core.storage.api.datamanager.transaction.TransactionManager; +import fr.inria.corese.core.storage.impl.lifecycle.LifecycleManagerImpl; +import fr.inria.corese.core.storage.impl.operations.BulkOperationsImpl; +import fr.inria.corese.core.storage.impl.operations.MetadataOperationsImpl; +import fr.inria.corese.core.storage.impl.operations.MutationOperationsImpl; +import fr.inria.corese.core.storage.impl.operations.QueryOperationsImpl; +import fr.inria.corese.core.storage.impl.transaction.TransactionManagerImpl; /** * DataManager for corese Graph for testing purpose */ public class CoreseGraphDataManager implements DataManager { - private static final Logger logger = LoggerFactory.getLogger(CoreseGraphDataManager.class); - static final String STORAGE_PATH = "http://ns.inria.fr/corese/dataset"; - private static final String DEBUG = "debug"; - List emptyNodeList; - List emptyEdgeList; - private Graph graph; + private final Graph graph; + private final LifecycleManagerImpl lifecycleManager; + private final QueryOperationsImpl queryOperations; + private final MetadataOperationsImpl metadataOperations; + private final MutationOperationsImpl mutationOperations; + private final BulkOperationsImpl bulkOperations; + private final TransactionManagerImpl transactionManager; private MetadataManager metadataManager; - private String path = STORAGE_PATH; - private boolean debug = false; /** - * Please use the CoreseGraphDataManagerBuilder to create a - * CoreseGraphDataManager. - * + * Protected constructor. + * Please use CoreseGraphDataManagerBuilder to create an instance. */ protected CoreseGraphDataManager() { - setGraph(new Graph()); - initlocal(); + this(new Graph()); } /** - * Please use the CoreseGraphDataManagerBuilder to create a - * CoreseGraphDataManager. - * - * @param g + * Protected constructor with existing graph. + * Please use CoreseGraphDataManagerBuilder to create an instance. + * + * @param graph Existing graph instance */ - protected CoreseGraphDataManager(Graph g) { - setGraph(g); - initlocal(); - } - - void initlocal() { - emptyNodeList = new ArrayList<>(0); - emptyEdgeList = new ArrayList<>(0); - } - - @Override - public void startReadTransaction() { - getGraph().init(); - } - - @Override - public void endReadTransaction() { - - } - - @Override - public void startWriteTransaction() { - getGraph().init(); - } - - @Override - public void endWriteTransaction() { - getGraph().init(); - } - - public String getStoragePath() { - return getPath(); - } - - @Override - public void start(HashMapList map) { - if (map != null) { - init(map); - } - } - - @Override - public void init(HashMapList map) { - logger.info("Init data manager: " + map); - if (map.containsKey(DEBUG)) { - setDebug(map.booleanValue(DEBUG)); - } - } - - // from.size == 1 -> named graph semantics - // else -> default graph semantics - @Override - public Iterable getEdges( - Node subject, Node predicate, Node object, List from) { - return getGraph().iterate(subject, predicate, object, from); - } - - @Override - public int graphSize() { - return getGraph().size(); - } - - @Override - public int countEdges(Node predicate) { - return getGraph().size(predicate); + protected CoreseGraphDataManager(Graph graph) { + this.graph = graph; + this.lifecycleManager = new LifecycleManagerImpl(this.graph); + this.queryOperations = new QueryOperationsImpl(this.graph); + this.metadataOperations = new MetadataOperationsImpl(this.graph); + this.mutationOperations = new MutationOperationsImpl(this.graph); + this.bulkOperations = new BulkOperationsImpl(this.graph); + this.transactionManager = new TransactionManagerImpl( + this.graph, + false, + IsolationLevel.READ_COMMITTED + ); } - /** - * Retrieve occurrence of query edge in target storage. - * Use case: edge is rdf star triple, purpose is to get its reference node - * if any - * - * @param edge The query edge to find in target storage - * @return The target edge if any + * Returns the transaction manager. + * + * @return Transaction manager */ - @Override - public Edge find(Edge edge) { - return getGraph().find(edge); - } - - // in practice, context is not used ... - @Override - public Iterable predicates(Node context) { - return getGraph().getSortedProperties(); - } - - @Override - public Iterable contexts() { - return getGraph().getGraphNodes(emptyNodeList); - } - - @Override - public Iterable getNodes(Node context) { - if (context == null) { - return getGraph().getNodeGraphIterator(); - } - return getGraph().getNodeGraphIterator(getGraph().getNode(context)); - } - - @Override - public String blankNode() { - return getGraph().newBlankID(); - } - - @Override - public Iterable insert(Node s, Node p, Node o, List contexts) { - return getGraph().insert(s, p, o, contexts); + public TransactionManager getTransactionManager() { + return transactionManager; } - - // @todo: rdf star - @Override - public Edge insert(Edge edge) { - return getGraph().insertEdgeWithTargetNode(edge); - } - - @Override - public Iterable delete(Node s, Node p, Node o, List contexts) { - return getGraph().delete(s, p, o, contexts); - } - - @Override - public Iterable delete(Edge edge) { - return getGraph().deleteEdgeWithTargetNode(edge); - } - - @Override - public boolean clear(List contexts, boolean silent) { - for (Node g : contexts) { - getGraph().clear(g.getLabel(), silent); - } - return true; - } - /** - * Removes all edges in graph. + * Returns the lifecycle manager. + * + * @return Lifecycle manager */ - @Override - public void clear() { - getGraph().clear(); - getGraph().dropGraphNames(); + public DataManagerLifecycle getLifecycle() { + return lifecycleManager; } - @Override - public boolean addGraph(Node source_context, Node target_context, boolean silent) { - return getGraph().add(source_context.getLabel(), target_context.getLabel(), silent); - } - - @Override - public boolean copyGraph(Node source_context, Node target_context, boolean silent) { - return getGraph().copy(source_context.getLabel(), target_context.getLabel(), silent); - } - @Override - public boolean moveGraph(Node source_context, Node target_context, boolean silent) { - return getGraph().move(source_context.getLabel(), target_context.getLabel(), silent); + /** + * Returns the query operations. + * + * @return Query operations + */ + public QueryOperations getQueryOperations() { + return queryOperations; } /** - * Declare a new context in graph. - * - * @param context New context to declare. + * Returns the metadata operations. + * + * @return Metadata operations */ - @Override - public void declareContext(Node context) { - getGraph().addGraphNode(context); + public MetadataOperations getMetadataOperations() { + return metadataOperations; } - @Override - public void unDeclareContext(Node context) { - this.clear(List.of(context), false); + /** + * Returns the mutation operations. + * + * @return Mutation operations + */ + public MutationOperations getMutationOperations() { + return mutationOperations; } - @Override - public void unDeclareAllContexts() { - this.clear(); + /** + * Returns the bulk operations. + * + * @return Bulk operations + */ + public BulkOperations getBulkOperations() { + return bulkOperations; } + /** + * Returns the underlying Graph. + * Direct graph access for advanced use cases. + * + * @return The graph + */ public Graph getGraph() { return graph; } - public void setGraph(Graph graph) { - this.graph = graph; + /** + * Checks if this DataManager has a MetadataManager. + * + * @return true if MetadataManager is set + */ + public boolean hasMetadataManager() { + return metadataManager != null; } - @Override + /** + * Gets the MetadataManager. + * + * @return MetadataManager or null + */ public MetadataManager getMetadataManager() { return metadataManager; } - @Override + /** + * Sets the MetadataManager. + * + * @param metadataManager MetadataManager to set + */ public void setMetadataManager(MetadataManager metadataManager) { this.metadataManager = metadataManager; } - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - - public boolean isDebug() { - return debug; - } - - public void setDebug(boolean debug) { - this.debug = debug; - } - -} +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/CoreseGraphDataManagerBuilder.java b/src/main/java/fr/inria/corese/core/storage/CoreseGraphDataManagerBuilder.java index 90518f68f..556ae5dfa 100644 --- a/src/main/java/fr/inria/corese/core/storage/CoreseGraphDataManagerBuilder.java +++ b/src/main/java/fr/inria/corese/core/storage/CoreseGraphDataManagerBuilder.java @@ -1,24 +1,26 @@ package fr.inria.corese.core.storage; import fr.inria.corese.core.Graph; -import fr.inria.corese.core.storage.api.dataManager.DataManagerBuilder; +import fr.inria.corese.core.storage.api.datamanager.DataManagerBuilder; public class CoreseGraphDataManagerBuilder implements DataManagerBuilder { - ////////////////////////// - // Mandatory parameters // - ////////////////////////// - - ///////////////////////// - // Optional parameters // - ///////////////////////// private Graph graph; private boolean defGraph = false; + private boolean enableTransactions = false; - ////////////////// - // Constructors // - ////////////////// + + /** + * Enables transaction support. + * + * @param enable true to enable transactions + * @return this builder + */ + public CoreseGraphDataManagerBuilder withTransactions(boolean enable) { + this.enableTransactions = enable; + return this; + } /** * Create a CoreseGraphDataManagerBuilder. @@ -26,10 +28,6 @@ public class CoreseGraphDataManagerBuilder implements DataManagerBuilder { public CoreseGraphDataManagerBuilder() { } - //////////// - // Setter // - //////////// - /** * Build the dataManager from an existing Corese Graphn * @@ -42,9 +40,6 @@ public CoreseGraphDataManagerBuilder graph(Graph graph) { return this; } - /////////// - // Build // - /////////// @Override public CoreseGraphDataManager build() { diff --git a/src/main/java/fr/inria/corese/core/storage/DataManagerJava.java b/src/main/java/fr/inria/corese/core/storage/DataManagerJava.java index f678ad643..b4e058b9c 100644 --- a/src/main/java/fr/inria/corese/core/storage/DataManagerJava.java +++ b/src/main/java/fr/inria/corese/core/storage/DataManagerJava.java @@ -1,41 +1,30 @@ package fr.inria.corese.core.storage; -import java.util.ArrayList; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import fr.inria.corese.core.Graph; +import fr.inria.corese.core.kgram.api.core.Edge; +import fr.inria.corese.core.kgram.api.core.Node; +import fr.inria.corese.core.kgram.core.Mappings; import fr.inria.corese.core.load.Load; import fr.inria.corese.core.load.LoadException; import fr.inria.corese.core.load.QueryLoad; import fr.inria.corese.core.query.QueryProcess; -import fr.inria.corese.core.kgram.api.core.Edge; -import fr.inria.corese.core.kgram.api.core.Node; -import fr.inria.corese.core.kgram.core.Mappings; import fr.inria.corese.core.sparql.api.IDatatype; import fr.inria.corese.core.sparql.datatype.DatatypeMap; import fr.inria.corese.core.sparql.exceptions.EngineException; import fr.inria.corese.core.sparql.triple.function.proxy.GraphSpecificFunction; import fr.inria.corese.core.sparql.triple.parser.Access; -import fr.inria.corese.core.sparql.triple.parser.Access.Feature; -import fr.inria.corese.core.sparql.triple.parser.Access.Level; import fr.inria.corese.core.sparql.triple.parser.HashMapList; import fr.inria.corese.core.sparql.triple.parser.NSManager; import fr.inria.corese.core.sparql.triple.parser.URLServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; /** - * DataManager on top of json (or xml) document + * DataManager on top of JSON (or XML) document. * - * a) implemented in java as graph data manager - * path = path of insert where query that creates a graph (from json, xml, etc.) - * update query creates graph data manager - * - * b) implemented in ldscript when path contains ldscript (draft for testing) - * path = path of ldscript function definition - * 1- read (json) file with us:read() ldscript determine json file - * 2- iterate edges with ldscript function us:iterate() */ public class DataManagerJava extends CoreseGraphDataManager { private static final Logger logger = LoggerFactory.getLogger(DataManagerJava.class); @@ -58,20 +47,30 @@ public class DataManagerJava extends CoreseGraphDataManager { private HashMapList map; private List load; - // path of insert where query that creates a graph (from json or xml) - // or - // path of ldscript us:read and us:iterate functions + // Local path field (since parent doesn't have setPath/getPath) + private final String path; + + /** + * Constructs a DataManagerJava with a path to query or LDScript functions. + * + * @param path Path to INSERT WHERE query or LDScript functions + */ public DataManagerJava(String path) { + super(); // Call parent constructor URLServer url = new URLServer(path); - // remove parameter if any - setPath(url.getServer()); + // Remove parameter if any + this.path = url.getServer(); setQueryPath(url.getServer()); } - // creation time in StorageFactory - @Override + /** + * Starts the data manager with configuration. + * Called at creation time in StorageFactory. + * + * @param map Configuration parameters + */ public void start(HashMapList map) { - logger.info("Start data manager: " + getStoragePath()); + logger.info("Start data manager: {}", path); if (map == null) { init(); } else { @@ -82,9 +81,12 @@ public void start(HashMapList map) { } } - // path parameter map - // called by service store clause in ProviderService - @Override + /** + * Initializes with path parameter map. + * Called by service store clause in ProviderService. + * + * @param map Configuration parameters + */ public void init(HashMapList map) { localInit(map); } @@ -94,7 +96,12 @@ boolean localInit(HashMapList map) { return parameter(map); } - // return true when init() is performed + /** + * Processes parameters and returns true when init() is performed. + * + * @param map Configuration parameters + * @return true if initialization was performed + */ boolean parameter(HashMapList map) { setMap(map); String queryPath = map.getFirst(PATH); @@ -107,7 +114,7 @@ boolean parameter(HashMapList map) { if (queryPath != null) { setQueryPath(queryPath); setQuery(null); - logger.info("Service query path= " + queryPath); + logger.info("Service query path= {}", queryPath); init(); return true; } else { @@ -116,7 +123,7 @@ boolean parameter(HashMapList map) { query = clean(query); setQuery(query); setQueryPath(null); - logger.info("Service query = " + query); + logger.info("Service query = {}", query); init(); return true; } @@ -128,7 +135,9 @@ String clean(String str) { return str.replace("%20", " "); } - //@Override + /** + * Initializes based on mode (Graph or LDScript). + */ void init() { if (isLdscript()) { initldscript(); @@ -137,33 +146,37 @@ void init() { } } - // create graph from json using update query located at path - // set this data manager graph + /** + * Initializes in Graph mode: creates graph from JSON using UPDATE query. + */ void initgraph() { logger.info("Mode graph"); - // graph to be created by update query - setGraph(Graph.create()); - //setQueryProcess(QueryProcess.create(getGraph())); + // Graph to be created by update query + // Create new graph and reinitialize parent components + Graph newGraph = Graph.create(); + setQueryProcess(QueryProcess.create(this)); QueryLoad ql = QueryLoad.create(); - Load ld = Load.create(getGraph()); + Load ld = Load.create(newGraph); ld.setDataManager(this); - // temporary authorize xt:read file to read e.g. json document - //Level read = Access.setValue(Feature.READ, Level.DEFAULT); - // Level readFile = Access.setValue(Feature.READ_FILE, Level.DEFAULT); + + // Temporarily authorize xt:read file to read e.g. json document // authorize xt:read() because accept list is empty during this initialization Access.setDefaultResultWhenEmptyAccept(true); + try { - if (getLoad()!=null) { + // Load data files if specified + if (getLoad() != null) { for (String name : getLoad()) { - logger.info("Load " + name); + logger.info("Load {}", name); ld.parse(name); } } - // query who creates rdf graph (from json) + + // Get query (from path or direct string) String q; if (getQueryPath() != null) { - logger.info("Load " + getQueryPath()); + logger.info("Load {}", getQueryPath()); q = ql.readWE(getQueryPath()); } else if (getQuery() != null) { q = getQuery(); @@ -171,65 +184,105 @@ void initgraph() { return; } + // Format query with parameters if provided if (getMap() != null && getMap().containsKey(PARAM)) { q = String.format(q, getMap().getFirst(PARAM)); } - logger.info("Process query:\n" + q); - // update query creates rdf graph (from json) - // this is graph of current DataManager - Mappings map = getQueryProcess().query(q); - if (map.getGraph() != null) { - // construct where query - setGraph((Graph) map.getGraph()); + + logger.info("Process query:\n{}", q); + + // Execute UPDATE query to create RDF graph (from JSON) + // This is the graph of current DataManager + Mappings mappings = getQueryProcess().query(q); + if (mappings.getGraph() != null) { + // CONSTRUCT WHERE query result + newGraph = (Graph) mappings.getGraph(); } - getGraph().init(); + newGraph.init(); + + logger.info("Graph initialized with {} edges", newGraph.size()); + } catch (LoadException | EngineException ex) { - logger.error(ex.getMessage()); - } - finally { - //Access.set(Feature.READ, read); - //Access.set(Feature.READ_FILE, readFile); + logger.error("Failed to initialize graph mode: {}", ex.getMessage()); + } finally { Access.setDefaultResultWhenEmptyAccept(false); } } - // load json using ldscript us:read() function - // defined at path - // iterate edges from json using ldscript us:iterate() function + /** + * Initializes in LDScript mode: prepares for JSON iteration. + */ void initldscript() { - logger.info("Mode json"); + logger.info("Mode ldscript"); try { - setGraph(Graph.create()); setQueryProcess(QueryProcess.create(Graph.create())); + + // Import LDScript functions getQueryProcess().imports(getQueryPath()); + + // Read JSON document using LDScript us:read() function setJsonDocument(getQueryProcess().funcall(readFunction)); + + // Joker for null pattern matching joker = DatatypeMap.newInstance(GraphSpecificFunction.JOKER); + + logger.info("LDScript initialized, JSON document loaded"); + } catch (EngineException ex) { - logger.error(ex.getMessage()); + logger.error("Failed to initialize ldscript mode: {}", ex.getMessage()); } } - @Override + /** + * Gets edges with LDScript support. + * + * @param s Subject (null for any) + * @param p Predicate (null for any) + * @param o Object (null for any) + * @param graphList Contexts + * @return Iterable of edges + */ public Iterable getEdges(Node s, Node p, Node o, List graphList) { if (isLdscript()) { - return iterateJson(s, p, o, graphList); + return iterateJson(s, p, o); } - return super.getEdges(s, p, o, graphList); + // In graph mode, use QueryOperations from parent + return getGraph().iterate(s, p, o, graphList); } - Iterable iterateJson(Node s, Node p, Node o, List graphList) { + /** + * Iterates over JSON document using LDScript us:iterate() function. + * + * @param s Subject pattern (null for any) + * @param p Predicate pattern (null for any) + * @param o Object pattern (null for any) + * @return Iterable of matching edges + */ + Iterable iterateJson(Node s, Node p, Node o) { try { - IDatatype dt = getQueryProcess().funcall(iterateFunction, getJsonDocument(), value(s), value(p), value(o)); + IDatatype dt = getQueryProcess().funcall( + iterateFunction, + getJsonDocument(), + value(s), + value(p), + value(o) + ); if (dt == null) { return new ArrayList<>(0); } return cast(dt); } catch (EngineException ex) { - logger.error(ex.getMessage()); + logger.error("Failed to iterate JSON: {}", ex.getMessage()); } return new ArrayList<>(0); } + /** + * Converts Node to IDatatype value, or joker if null. + * + * @param n Node to convert + * @return IDatatype value or joker + */ IDatatype value(Node n) { if (n == null) { return joker; @@ -237,7 +290,12 @@ IDatatype value(Node n) { return n.getDatatypeValue(); } - // list of triple reference, result of triple(s, p, o) + /** + * Converts IDatatype list to Edge list. + * + * @param list IDatatype list of triple references + * @return List of edges + */ Iterable cast(IDatatype list) { ArrayList edgeList = new ArrayList<>(); for (IDatatype dt : list) { @@ -246,18 +304,6 @@ Iterable cast(IDatatype list) { return edgeList; } - boolean filter(IDatatype dt, Node s, Node p, Node o) { - Edge e = dt.getEdge(); - if (isJoker(p)) { - return true; - } - return e.getPropertyNode().equals(p); - } - - boolean isJoker(Node n) { - return n.getDatatypeValue().equals(joker); - } - public QueryProcess getQueryProcess() { return queryProcess; } @@ -314,4 +360,7 @@ public void setLoad(List load) { this.load = load; } -} + public String getPath() { + return path; + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/api/dataManager/DataManager.java b/src/main/java/fr/inria/corese/core/storage/api/datamanager/DataManager.java similarity index 60% rename from src/main/java/fr/inria/corese/core/storage/api/dataManager/DataManager.java rename to src/main/java/fr/inria/corese/core/storage/api/datamanager/DataManager.java index 6cc0a1c45..f0ead8374 100644 --- a/src/main/java/fr/inria/corese/core/storage/api/dataManager/DataManager.java +++ b/src/main/java/fr/inria/corese/core/storage/api/datamanager/DataManager.java @@ -1,11 +1,10 @@ -package fr.inria.corese.core.storage.api.dataManager; +package fr.inria.corese.core.storage.api.datamanager; import fr.inria.corese.core.producer.MetadataManager; import fr.inria.corese.core.sparql.triple.parser.HashMapList; /** * Interface to adapt an external storage system to Corese. - * * {@code DataManagerRead} for {@code select where} SPARQL queries. * {@code DataManagerUpdate} for {@code update} and {@code construct} queries. * @@ -14,9 +13,6 @@ */ public interface DataManager extends DataManagerRead, DataManagerUpdate { - /******************* - * MetaDataManager * - *******************/ /** * Indicates whether or not this DataManage has a MetaDataManager. @@ -67,35 +63,14 @@ default void init(HashMapList map) { } // manage edge index i as named graph kg:rule_i - default void setRuleDataManager(boolean b) { - } - - default boolean isRuleDataManager() { - return false; - } - - // manage integer context in edge iterator as a filter on edge index - // where edge.index >= context.intValue - default boolean isEdgeIndexContext() { - return false; + @SuppressWarnings("unused") + default void setRuleDataManager(boolean b) { } - /**************** - * Transactions * - ****************/ - /** - * Indicates whether or not this DataManage supports transactions. - * - * @return true if this DataManage supports transactions, otherwise false. - */ - default boolean transactionSupport() { - return false; - } /** * Start a read transaction. - * * A read transaction can be re-entrant, i.e. several read transactions can be * nested within each other. * Only if the transactions are supported. @@ -105,7 +80,6 @@ default void startReadTransaction() { /** * End a read transaction. - * * If several read transactions are nested within each other, this function * terminates only the most recent read transaction. * Only if the transactions are supported. @@ -115,7 +89,6 @@ default void endReadTransaction() { /** * Start a write transaction. - * * A write transaction can't be re-entrant. * Only if the transactions are supported. */ @@ -129,42 +102,6 @@ default void startWriteTransaction() { default void endWriteTransaction() { } - /** - * Abort a transaction and undo the changes. - * Only if the transactions are supported. - */ - default void abortTransaction() { - } - - /** - * Indicates whether this DataManage is in transaction or not. - * Only if the transactions are supported. - * - * @return true if this DataManage is in transaction, otherwise false. - */ - default boolean isInTransaction() { - return this.isInReadTransaction() || this.isInWriteTransaction(); - } - - /** - * Indicates whether this DataManage is in read transaction or not. - * Only if the transactions are supported. - * - * @return true if this DataManage is in read transaction, otherwise false. - */ - default boolean isInReadTransaction() { - return false; - } - - /** - * Indicates whether this DataManage is in write transaction or not. - * Only if the transactions are supported. - * - * @return true if this DataManage is in write transaction, otherwise false. - */ - default boolean isInWriteTransaction() { - return false; - } default void startRuleEngine() { } diff --git a/src/main/java/fr/inria/corese/core/storage/api/dataManager/DataManagerBuilder.java b/src/main/java/fr/inria/corese/core/storage/api/datamanager/DataManagerBuilder.java similarity index 83% rename from src/main/java/fr/inria/corese/core/storage/api/dataManager/DataManagerBuilder.java rename to src/main/java/fr/inria/corese/core/storage/api/datamanager/DataManagerBuilder.java index 99b6e7fb6..58f38a898 100644 --- a/src/main/java/fr/inria/corese/core/storage/api/dataManager/DataManagerBuilder.java +++ b/src/main/java/fr/inria/corese/core/storage/api/datamanager/DataManagerBuilder.java @@ -1,4 +1,4 @@ -package fr.inria.corese.core.storage.api.dataManager; +package fr.inria.corese.core.storage.api.datamanager; /** * Builder for DataManager diff --git a/src/main/java/fr/inria/corese/core/storage/api/dataManager/DataManagerRead.java b/src/main/java/fr/inria/corese/core/storage/api/datamanager/DataManagerRead.java similarity index 93% rename from src/main/java/fr/inria/corese/core/storage/api/dataManager/DataManagerRead.java rename to src/main/java/fr/inria/corese/core/storage/api/datamanager/DataManagerRead.java index a549ebb6c..593603bf2 100644 --- a/src/main/java/fr/inria/corese/core/storage/api/dataManager/DataManagerRead.java +++ b/src/main/java/fr/inria/corese/core/storage/api/datamanager/DataManagerRead.java @@ -1,16 +1,14 @@ -package fr.inria.corese.core.storage.api.dataManager; - -import java.util.ArrayList; -import java.util.List; +package fr.inria.corese.core.storage.api.datamanager; import fr.inria.corese.core.kgram.api.core.Edge; import fr.inria.corese.core.kgram.api.core.Node; +import java.util.ArrayList; +import java.util.List; + /** * Interface to adapt an external storage system to Corese. - * * {@code DataManagerRead} for {@code select where} SPARQL queries. - * * This interface is not used by the internal Corese graph, specific DataBroker * directly process it. * @@ -35,6 +33,7 @@ default int graphSize() { * @param predicate The predicate to count. * @return Number of edges with a specific predicate. */ + @SuppressWarnings("unused") default int countEdges(Node predicate) { return 0; } @@ -54,15 +53,12 @@ default Edge find(Edge edge) { /** * Returns an {@link Iterable} over all {@link Edge}s in the graph that match * the supplied criteria. - * * If several edges have the same subject, predicate and object then only one is * returned. - * * If the size of the contexts in parameter is not equal to 1, then the value of * Graph of Edges returned does not matter. It can be for example * {@code http://ns.inria.fr/corese.core.kgram/default}. Be careful, the value of * Graph of Edges cannot be {@code null}. - * * All {@code null} in context list are ignored. E.g. If the list contains only * null, it is handled as an empty list. * @@ -85,18 +81,17 @@ default Iterable getEdges(Node subject, Node predicate, Node object, List< // with condition edge.index >= index // use case: rule engine transitive closure ClosureDataManager + @SuppressWarnings("unused") default Iterable getEdges(Node subject, Node predicate, Node object, List contexts, int oper, int index) { return getEdges(subject, predicate, object, contexts); } - - // triple exist in any graph + + // triple exists in any graph default boolean exist(Node subject, Node predicate, Node object) { - for (Edge edge : getEdges(subject, predicate, object, null)) { - return true; - } - return false; + return getEdges(subject, predicate, object, null).iterator().hasNext(); } + /** * Returns an {@link Iterable} over all predicates of edges that match the * context without duplicates. @@ -105,6 +100,7 @@ default boolean exist(Node subject, Node predicate, Node object) { * @return An {@link Iterable} over all predicates of edges that match the * context. */ + @SuppressWarnings("unused") default Iterable predicates(Node context) { return new ArrayList<>(0); } @@ -116,6 +112,7 @@ default Iterable predicates(Node context) { * @param context Context to match, {@code null} to match with any contexts. * @return An {@link Iterable} over all node in graph that match the context. */ + @SuppressWarnings("unused") default Iterable getNodes(Node context) { return new ArrayList<>(0); } diff --git a/src/main/java/fr/inria/corese/core/storage/api/dataManager/DataManagerUpdate.java b/src/main/java/fr/inria/corese/core/storage/api/datamanager/DataManagerUpdate.java similarity index 95% rename from src/main/java/fr/inria/corese/core/storage/api/dataManager/DataManagerUpdate.java rename to src/main/java/fr/inria/corese/core/storage/api/datamanager/DataManagerUpdate.java index 3c00f8b70..90cdab0ab 100644 --- a/src/main/java/fr/inria/corese/core/storage/api/dataManager/DataManagerUpdate.java +++ b/src/main/java/fr/inria/corese/core/storage/api/datamanager/DataManagerUpdate.java @@ -1,4 +1,4 @@ -package fr.inria.corese.core.storage.api.dataManager; +package fr.inria.corese.core.storage.api.datamanager; import java.util.HashSet; import java.util.List; @@ -10,9 +10,7 @@ /** * Interface to adapt an external storage system to Corese. - * * {@code DataManagerUpdate} for {@code update} and {@code construct} queries. - * * This interface is also used by the internal Corese graph. * * @author Olivier Corby @@ -62,7 +60,6 @@ default Iterable insert(Node subject, Node predicate, Node object, List insert(Node subject, Node predicate, Node object, List delete(Node subject, Node predicate, Node object, List contexts) { return null; } @@ -130,7 +129,7 @@ default Iterable delete(Edge edge) { */ default boolean clear(List contexts, boolean silent) { Iterable deleted = this.delete(null, null, null, contexts); - Boolean succes = deleted.iterator().hasNext(); + boolean succes = deleted.iterator().hasNext(); if (silent) { return true; @@ -155,6 +154,7 @@ default void clear() { * @return True if the graph has been modified or {@code silent} parameter is * true, else false. */ + @SuppressWarnings("unused") default boolean addGraph(Node source_context, Node target_context, boolean silent) { return false; } @@ -171,8 +171,7 @@ default boolean addGraph(Node source_context, Node target_context, boolean silen */ default boolean copyGraph(Node source_context, Node target_context, boolean silent) { this.clear(List.of(target_context), silent); - Boolean result = this.addGraph(source_context, target_context, silent); - return result; + return this.addGraph(source_context, target_context, silent); } /** @@ -186,7 +185,7 @@ default boolean copyGraph(Node source_context, Node target_context, boolean sile * true, else false. */ default boolean moveGraph(Node source_context, Node target_context, boolean silent) { - Boolean result = this.copyGraph(source_context, target_context, silent); + boolean result = this.copyGraph(source_context, target_context, silent); this.clear(List.of(source_context), silent); return result; } @@ -196,13 +195,13 @@ default boolean moveGraph(Node source_context, Node target_context, boolean sile * * @param context New context to declare. */ + @SuppressWarnings("unused") default void declareContext(Node context) { } /** * Clear and undeclare a context in graph. * - * @param context */ default void unDeclareContext(Node context) { this.clear(List.of(context), false); diff --git a/src/main/java/fr/inria/corese/core/storage/api/datamanager/lifecycle/DataManagerLifecycle.java b/src/main/java/fr/inria/corese/core/storage/api/datamanager/lifecycle/DataManagerLifecycle.java new file mode 100644 index 000000000..b1e038ba9 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/api/datamanager/lifecycle/DataManagerLifecycle.java @@ -0,0 +1,94 @@ +package fr.inria.corese.core.storage.api.datamanager.lifecycle; + +import fr.inria.corese.core.storage.api.datamanager.support.config.DataManagerConfig; +import fr.inria.corese.core.storage.api.datamanager.support.exception.DataManagerException; +import fr.inria.corese.core.storage.api.datamanager.support.exception.ErrorCode; + +/** + * Lifecycle management for the DataManager. + */ +public interface DataManagerLifecycle { + + /** + * Initializes the DataManager with the provided configuration. + * + * @param config DataManager configuration (must not be null) + * @throws DataManagerException if initialization fails + * @throws IllegalStateException if already initialized + * @throws IllegalArgumentException if config is null + */ + void initialize(DataManagerConfig config) throws DataManagerException; + + /** + * Checks if the DataManager is initialized and ready to use. + * + * @return true if initialized (RUNNING state), false otherwise + */ + default boolean isInitialized() { + return getState().isRunnig(); + } + + /** + * Cleanly shuts down the DataManager and releases all resources. + * + * @throws DataManagerException if shutdown fails + * @throws IllegalStateException if not initialized + */ + void shutdown() throws DataManagerException; + + /** + * Restarts the DataManager with a new configuration. + * This method attempts to restore the previous configuration if the restart fails. + * However, restoration is not guaranteed if both shutdown and re-initialization fail. + * + * @param config New configuration (must not be null) + * @throws DataManagerException if restart fails + * @throws IllegalArgumentException if config is null + * @throws IllegalStateException if shutdown fails (when already initialized) + */ + default void restart(DataManagerConfig config) throws DataManagerException { + DataManagerConfig oldConfig = getConfig(); + boolean wasInitialized = isInitialized(); + + if (wasInitialized) { + shutdown(); + } + + try { + initialize(config); + } catch (DataManagerException e) { + if (wasInitialized && oldConfig != null) { + try { + initialize(oldConfig); + throw new DataManagerException( + ErrorCode.RESTART_FAILED_ROLLBACK_SUCCESS, + "Failed to restart with new config. Restored previous configuration.", + e); + } catch (DataManagerException rollbackEx) { + DataManagerException criticalFailure = new DataManagerException( + ErrorCode.RESTART_FAILED_ROLLBACK_FAILED, + "Failed to restart and unable to restore previous configuration.", + e); + criticalFailure.addSuppressed(rollbackEx); + throw criticalFailure; + } + } + throw e; + } + } + /** + * Returns the current lifecycle state. + * + * @return Current state + */ + LifecycleState getState(); + + /** + * Returns the currently used configuration. + * + * @return Current configuration, or null if not initialized + */ + DataManagerConfig getConfig(); + + +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/api/datamanager/lifecycle/LifecycleState.java b/src/main/java/fr/inria/corese/core/storage/api/datamanager/lifecycle/LifecycleState.java new file mode 100644 index 000000000..235ab351a --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/api/datamanager/lifecycle/LifecycleState.java @@ -0,0 +1,50 @@ +package fr.inria.corese.core.storage.api.datamanager.lifecycle; + +/** + * Possible lifecycle states of the DataManager. + */ +public enum LifecycleState { + /** + * The DataManager is not yet initialized + */ + NOT_INITIALIZED("Not initialized"), + + /** + * The DataManager is initializing + */ + INITIALIZING("Initializing"), + + /** + * The DataManager is operational and ready + */ + RUNNING("Running"), + + /** + * The DataManager is shutting down + */ + SHUTTING_DOWN("Shutting down"), + + /** + * The DataManager has been shut down + */ + SHUTDOWN("Shutdown"); + + private final String description; + + LifecycleState(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + + /** + * Checks if the DataManager can be used in this state. + * + * @return true if usable (RUNNING) + */ + public boolean isRunnig() { + return this == RUNNING; + } +} diff --git a/src/main/java/fr/inria/corese/core/storage/api/datamanager/operations/BulkOperations.java b/src/main/java/fr/inria/corese/core/storage/api/datamanager/operations/BulkOperations.java new file mode 100644 index 000000000..c03d5dbb5 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/api/datamanager/operations/BulkOperations.java @@ -0,0 +1,151 @@ +package fr.inria.corese.core.storage.api.datamanager.operations; + +import fr.inria.corese.core.kgram.api.core.Edge; +import fr.inria.corese.core.kgram.api.core.Node; +import fr.inria.corese.core.storage.api.datamanager.support.exception.DataManagerException; +import fr.inria.corese.core.storage.api.datamanager.support.model.EdgePattern; +import fr.inria.corese.core.storage.api.datamanager.support.model.MutationResult; + +import java.util.List; + +/** + * Bulk mutation operations for batch processing. + */ +public interface BulkOperations { + + /** + * Inserts multiple edges in a single batch operation. + * + * @param edges List of edges to insert + * @return Bulk mutation result with statistics + * @throws DataManagerException if operation fails + * @throws IllegalArgumentException if edges is null or empty + */ + MutationResult insertBatch(List edges) throws DataManagerException; + + /** + * Deletes multiple edges in a single batch operation. + * More efficient than calling deleteEdge() multiple times. + * + * @param edges List of edges to delete + * @return Bulk mutation result with statistics + * @throws DataManagerException if operation fails + * @throws IllegalArgumentException if edges is null or empty + */ + MutationResult deleteBatch(List edges) throws DataManagerException; + + /** + * Deletes all edges matching the given pattern. + * + * @param pattern Pattern to match edges for deletion + * @return Bulk mutation result with deleted edges + * @throws DataManagerException if operation fails + * @throws IllegalArgumentException if pattern is null + */ + MutationResult deleteByPattern(EdgePattern pattern) throws DataManagerException; + + /** + * Clears (deletes all edges from) specific contexts. + * + * @param contexts List of contexts to clear (empty or null for all) + * @param silent If true, don't fail if context doesn't exist + * @return Bulk mutation result + * @throws DataManagerException if operation fails + */ + MutationResult clearContexts(List contexts, boolean silent) throws DataManagerException; + + /** + * Clears all contexts (deletes entire graph). + * + * @return Bulk mutation result with all deleted edges + * @throws DataManagerException if operation fails + */ + MutationResult clearAll() throws DataManagerException; + + /** + * Adds edges from source context to target context. + * Edges remain in source context. + * + * @param sourceContext Source context + * @param targetContext Target context + * @param silent If true, don't fail if source doesn't exist + * @return Bulk mutation result + * @throws DataManagerException if operation fails + * @throws IllegalArgumentException if contexts are null + */ + MutationResult addGraph(Node sourceContext, Node targetContext, boolean silent) + throws DataManagerException; + + /** + * Copies edges from source context to target context. + * Replaces all edges in target context. + * + * @param sourceContext Source context + * @param targetContext Target context + * @param silent If true, don't fail if source doesn't exist + * @return Bulk mutation result + * @throws DataManagerException if operation fails + * @throws IllegalArgumentException if contexts are null + */ + MutationResult copyGraph(Node sourceContext, Node targetContext, boolean silent) + throws DataManagerException; + + /** + * Moves edges from source context to target context. + * Removes edges from source context. + * + * @param sourceContext Source context + * @param targetContext Target context + * @param silent If true, don't fail if source doesn't exist + * @return Bulk mutation result + * @throws DataManagerException if operation fails + * @throws IllegalArgumentException if contexts are null + */ + MutationResult moveGraph(Node sourceContext, Node targetContext, boolean silent) + throws DataManagerException; + + /** + * Declares (creates) a context without adding edges. + * Useful for pre-creating named graphs. + * + * @param context Context to declare + * @return Mutation result + * @throws DataManagerException if operation fails + * @throws IllegalArgumentException if context is null + */ + MutationResult declareContext(Node context) throws DataManagerException; + + /** + * Declares (creates) multiple contexts in a single batch operation. + * More efficient than calling declareContext() multiple times. + * Useful for pre-creating multiple named graphs. + * + * @param contexts List of contexts to declare + * @return Bulk mutation result with statistics + * @throws DataManagerException if operation fails + * @throws IllegalArgumentException if contexts is null or empty + */ + MutationResult declareContexts(List contexts) throws DataManagerException; + + /** + * Undeclares (deletes) a context and all its edges. + * + * @param context Context to undeclare + * @return Bulk mutation result with deleted edges + * @throws DataManagerException if operation fails + * @throws IllegalArgumentException if context is null + */ + MutationResult undeclareContext(Node context) throws DataManagerException; + + /** + * Undeclares (deletes) multiple contexts and all their edges in a single batch operation. + * More efficient than calling undeclareContext() multiple times. + * + * @param contexts List of contexts to undeclare + * @return Bulk mutation result with all deleted edges + * @throws DataManagerException if operation fails + * @throws IllegalArgumentException if contexts is null or empty + */ + MutationResult undeclareContexts(List contexts) throws DataManagerException; + +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/api/datamanager/operations/MetadataOperations.java b/src/main/java/fr/inria/corese/core/storage/api/datamanager/operations/MetadataOperations.java new file mode 100644 index 000000000..f6a76ce66 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/api/datamanager/operations/MetadataOperations.java @@ -0,0 +1,70 @@ +package fr.inria.corese.core.storage.api.datamanager.operations; + +import fr.inria.corese.core.kgram.api.core.Node; +import fr.inria.corese.core.storage.api.datamanager.support.exception.DataManagerException; +import fr.inria.corese.core.storage.api.datamanager.support.model.GraphStatistics; +import java.util.Set; + +/** + * Metadata operations for the graph. + * Provides access to graph structure information (predicates, nodes, contexts, statistics). + * + */ +public interface MetadataOperations { + + /** + * Returns all predicates in the specified context. + * + * @param context Context to query (null for all contexts) + * @return Set of predicates (unmodifiable) + * @throws DataManagerException if query fails + */ + Set getPredicates(Node context) throws DataManagerException; + + /** + * Returns all predicates in all contexts. + * Convenience method equivalent to getPredicates(null). + * + * @return Set of all predicates (unmodifiable) + * @throws DataManagerException if query fails + */ + default Set getAllPredicates() throws DataManagerException { + return getPredicates(null); + } + + /** + * Returns all nodes (subjects and objects) in the specified context. + * + * @param context Context to query (null for all contexts) + * @return Set of nodes (unmodifiable) + * @throws DataManagerException if query fails + */ + Set getNodes(Node context) throws DataManagerException; + + /** + * Returns all nodes in all contexts. + * Convenience method equivalent to getNodes(null). + * + * @return Set of all nodes (unmodifiable) + * @throws DataManagerException if query fails + */ + default Set getAllNodes() throws DataManagerException { + return getNodes(null); + } + + /** + * Returns all contexts (named graphs). + * + * @return Set of context nodes (unmodifiable) + * @throws DataManagerException if query fails + */ + Set getContexts() throws DataManagerException; + + /** + * Returns statistics about the graph content and structure. + * + * @return Graph statistics + * @throws DataManagerException if statistics cannot be computed + */ + GraphStatistics getStatistics() throws DataManagerException; +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/api/datamanager/operations/MutationOperations.java b/src/main/java/fr/inria/corese/core/storage/api/datamanager/operations/MutationOperations.java new file mode 100644 index 000000000..b7e3c8ee5 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/api/datamanager/operations/MutationOperations.java @@ -0,0 +1,99 @@ +package fr.inria.corese.core.storage.api.datamanager.operations; + +import fr.inria.corese.core.kgram.api.core.Edge; +import fr.inria.corese.core.kgram.api.core.Node; +import fr.inria.corese.core.storage.api.datamanager.support.exception.DataManagerException; +import fr.inria.corese.core.storage.api.datamanager.support.model.MutationResult; + +import java.util.List; + +/** + * mutation operations. + */ +public interface MutationOperations { + + /** + * Inserts a single edge. + * + * @param edge Edge to insert + * @return Mutation result + * @throws DataManagerException if operation fails + * @throws IllegalArgumentException if edge is null + */ + MutationResult insertEdge(Edge edge) throws DataManagerException; + + /** + * Inserts an edge with explicit subject, predicate, object, and contexts. + * + * @param subject Subject node + * @param predicate Predicate node + * @param object Object node + * @param contexts List of contexts (null or empty for default graph) + * @throws DataManagerException if operation fails + * @throws IllegalArgumentException if subject, predicate, or object is null + */ + MutationResult insertEdge(Node subject, Node predicate, Node object, List contexts) + throws DataManagerException; + + /** + * Deletes a single edge. + * + * @param edge Edge to delete + * @return Mutation result + * @throws DataManagerException if operation fails + * @throws IllegalArgumentException if edge is null + */ + MutationResult deleteEdge(Edge edge) throws DataManagerException; + + /** + * Deletes edges matching the given pattern. + * + * @param subject Subject node (null for any) + * @param predicate Predicate node (null for any) + * @param object Object node (null for any) + * @param contexts List of contexts (null or empty for all) + * @return Mutation result (bulk result with all deleted edges) + * @throws DataManagerException if operation fails + */ + MutationResult deleteEdges(Node subject, Node predicate, Node object, List contexts) + throws DataManagerException; + + /** + * Updates an edge (delete + insert in one operation). + * + * @param oldEdge Edge to delete + * @param newEdge Edge to insert + * @return Mutation result + * @throws DataManagerException if operation fails + * @throws IllegalArgumentException if oldEdge or newEdge is null + */ + default MutationResult updateEdge(Edge oldEdge, Edge newEdge) throws DataManagerException { + if (oldEdge == null || newEdge == null) { + throw new IllegalArgumentException("Both oldEdge and newEdge must be non-null"); + } + + // Simple implementation: delete then insert + // In a transactional system, this would be atomic + MutationResult deleteResult = deleteEdge(oldEdge); + if (deleteResult.isFailure()) { + return deleteResult; + } + + MutationResult insertResult = insertEdge(newEdge); + if (insertResult.isFailure()) { + return MutationResult.failure( + "Insert failed after delete: " + insertResult.getMessage() + ); + } + + return insertResult; + } + + /** + * Generates a new blank node identifier. + * + * @return New blank node ID + * @throws DataManagerException if generation fails + */ + String generateBlankNode() throws DataManagerException; +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/api/datamanager/operations/QueryOperations.java b/src/main/java/fr/inria/corese/core/storage/api/datamanager/operations/QueryOperations.java new file mode 100644 index 000000000..925743257 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/api/datamanager/operations/QueryOperations.java @@ -0,0 +1,62 @@ +package fr.inria.corese.core.storage.api.datamanager.operations; + +import fr.inria.corese.core.kgram.api.core.Edge; +import fr.inria.corese.core.storage.api.datamanager.support.exception.DataManagerException; +import fr.inria.corese.core.storage.api.datamanager.support.model.EdgePattern; + +import java.util.stream.Stream; + +/** + * Query operations for reading edges. + * Provides modern Stream-based API for querying the graph. + */ +public interface QueryOperations { + + /** + * Queries edges matching the given pattern. + * + * @param pattern Edge pattern to match + * @return Stream of matching edges (must be closed) + * @throws DataManagerException if query fails + * @throws IllegalArgumentException if pattern is null + */ + Stream query(EdgePattern pattern) throws DataManagerException; + + /** + * Counts edges matching the given pattern. + * More efficient than query().count() as it doesn't need to load edges. + * + * @param pattern Edge pattern to match + * @return Number of matching edges + * @throws DataManagerException if count fails + * @throws IllegalArgumentException if pattern is null + */ + long count(EdgePattern pattern) throws DataManagerException; + + /** + * Checks if at least one edge matches the given pattern. + * More efficient than query().findAny() as it can stop after first match. + * + * @param pattern Edge pattern to match + * @return true if at least one edge matches + * @throws DataManagerException if check fails + * @throws IllegalArgumentException if pattern is null + */ + boolean exists(EdgePattern pattern) throws DataManagerException; + + /** + * Finds a specific edge (for RDF-star support). + * Use case: edge is an RDF-star triple, purpose is to get its reference node if any. + * + * @param edge The query edge to find in target storage + * @return The target edge if found, otherwise the input edge + * @throws DataManagerException find fails + * @throws IllegalArgumentException if edge is null + */ + default Edge find(Edge edge) throws DataManagerException { + if (edge == null) { + throw new IllegalArgumentException("Edge cannot be null"); + } + return edge; + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/api/datamanager/support/config/DataManagerConfig.java b/src/main/java/fr/inria/corese/core/storage/api/datamanager/support/config/DataManagerConfig.java new file mode 100644 index 000000000..3c340e3a8 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/api/datamanager/support/config/DataManagerConfig.java @@ -0,0 +1,161 @@ +package fr.inria.corese.core.storage.api.datamanager.support.config; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * Typed configuration for the DataManager. + */ +public final class DataManagerConfig { + + private final Map properties; + private final boolean transactionSupport; + private final boolean debug; + + /** + * Private constructor - use the Builder. + */ + private DataManagerConfig(Builder builder) { + this.properties = Map.copyOf(builder.properties); + this.transactionSupport = builder.transactionSupport; + this.debug = builder.debug; + } + + /** + * Returns a property. + * + * @param key Property key + * @return Property value, or Optional.empty() if absent + */ + public Optional getProperty(String key) { + return Optional.ofNullable(properties.get(key)); + } + + /** + * Returns a property with a specific type. + * + * @param Property type + * @param key Property key + * @param type Expected type class + * @return Typed value, or Optional.empty() if absent or wrong type + */ + @SuppressWarnings("unchecked") + public Optional getProperty(String key, Class type) { + Object value = properties.get(key); + if (type.isInstance(value)) { + return Optional.of((T) value); + } + return Optional.empty(); + } + + /** + * Returns all properties. + * + * @return Immutable map of properties + */ + public Map getProperties() { + return properties; + } + + + /** + * Indicates whether debug mode is enabled. + * + * @return true if debug enabled + */ + public boolean isDebug() { + return debug; + } + + /** + * Creates a new builder. + * + * @return New builder instance + */ + public static Builder builder() { + return new Builder(); + } + + @Override + public String toString() { + return "DataManagerConfig{" + + ", transactionSupport=" + transactionSupport + + ", debug=" + debug + + ", properties=" + properties + + '}'; + } + + /** + * Builder for DataManagerConfig. + */ + public static final class Builder { + private final Map properties = new HashMap<>(); + private boolean transactionSupport = false; + private boolean debug = false; + + private Builder() { + } + + /** + * Adds a custom property to the configuration. + * + * @param key Property key (must not be null, empty, or blank) + * @param value Property value (must not be null) + * @return this builder instance + * @throws IllegalArgumentException if key is null/empty/blank or value is null + */ + public Builder property(String key, Object value) { + if (key == null || key.isBlank()) { + throw new IllegalArgumentException("Property key cannot be null, empty, or blank"); + } + if (value == null) { + throw new IllegalArgumentException("Property value cannot be null"); + } + this.properties.put(key, value); + return this; + } + + /** + * Enables or disables transaction support. + * + * @param enable true to enable + * @return This builder (for chaining) + */ + public Builder enableTransactions(boolean enable) { + this.transactionSupport = enable; + return this; + } + + /** + * Enables or disables transaction support (alias for enableTransactions). + * + * @param enable true to enable + * @return This builder (for chaining) + */ + public Builder transactionSupport(boolean enable) { + return enableTransactions(enable); + } + + /** + * Enables or disables debug mode. + * + * @param debug true to enable + * @return This builder (for chaining) + */ + public Builder debug(boolean debug) { + this.debug = debug; + return this; + } + + /** + * Builds the DataManagerConfig instance. + * + * @return New configured instance + * @throws IllegalStateException if storagePath has not been set + */ + public DataManagerConfig build() { + return new DataManagerConfig(this); + } + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/api/datamanager/support/exception/DataManagerException.java b/src/main/java/fr/inria/corese/core/storage/api/datamanager/support/exception/DataManagerException.java new file mode 100644 index 000000000..f0490b5e4 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/api/datamanager/support/exception/DataManagerException.java @@ -0,0 +1,50 @@ +package fr.inria.corese.core.storage.api.datamanager.support.exception; + +/** + * Specific exception for DataManager operations. + */ +public class DataManagerException extends Exception { + + private final ErrorCode code; + + /** + * Constructs an exception with a code and message. + * + * @param code Error code + * @param message Descriptive message + */ + public DataManagerException(ErrorCode code, String message) { + super(message); + this.code = code; + } + + /** + * Constructs an exception with a code, message and cause. + * + * @param code Error code + * @param message Descriptive message + * @param cause Original exception + */ + public DataManagerException(ErrorCode code, String message, Throwable cause) { + super(message, cause); + this.code = code; + } + + /** + * Returns the error code. + * + * @return Error code + */ + public ErrorCode getCode() { + return code; + } + + @Override + public String toString() { + return "DataManagerException{" + + "code=" + code + + ", message='" + getMessage() + '\'' + + '}'; + } + +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/api/datamanager/support/exception/ErrorCode.java b/src/main/java/fr/inria/corese/core/storage/api/datamanager/support/exception/ErrorCode.java new file mode 100644 index 000000000..59b4fa453 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/api/datamanager/support/exception/ErrorCode.java @@ -0,0 +1,56 @@ +package fr.inria.corese.core.storage.api.datamanager.support.exception; + +/** + * Error codes for DataManager operations. + */ +public enum ErrorCode { + /** Initialization failure */ + INITIALIZATION_FAILED("INIT_FAIL", "Initialization failed"), + + /** Shutdown failure */ + SHUTDOWN_FAILED("SHUTDOWN_FAIL", "Shutdown failed"), + + /** Transaction error */ + TRANSACTION_ERROR("TX_ERROR", "Transaction error"), + + /** Query failure */ + QUERY_FAILED("QUERY_FAIL", "Query failed"), + + /** Mutation failure */ + MUTATION_FAILED("MUTATION_FAIL", "Mutation failed"), + + /** Invalid pattern */ + INVALID_PATTERN("INVALID_PATTERN", "Invalid pattern"), + + /** Context not found */ + CONTEXT_NOT_FOUND("CTX_NOT_FOUND", "Context not found"), + + /** Unsupported operation */ + UNSUPPORTED_OPERATION("UNSUPPORTED", "Operation not supported"), + + /** Invalid state */ + INVALID_STATE("INVALID_STATE", "Invalid state"), + + RESTART_FAILED_ROLLBACK_SUCCESS("RESTART_FAIL_ROLLBACK_OK", + "Restart failed but previous configuration restored"), + + /** Restart failed and rollback also failed (critical) */ + RESTART_FAILED_ROLLBACK_FAILED("RESTART_FAIL_ROLLBACK_FAIL", + "Restart failed and unable to restore previous configuration"),; + + private final String code; + private final String description; + + ErrorCode(String code, String description) { + this.code = code; + this.description = description; + } + + public String getCode() { + return code; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/fr/inria/corese/core/storage/api/datamanager/support/model/EdgePattern.java b/src/main/java/fr/inria/corese/core/storage/api/datamanager/support/model/EdgePattern.java new file mode 100644 index 000000000..9f715d469 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/api/datamanager/support/model/EdgePattern.java @@ -0,0 +1,223 @@ +package fr.inria.corese.core.storage.api.datamanager.support.model; + +import fr.inria.corese.core.kgram.api.core.Node; + +import java.util.List; +import java.util.Objects; + +/** + * Pattern for matching edges in queries. + */ +public final class EdgePattern { + + private final Node subject; + private final Node predicate; + private final Node object; + private final List contexts; + + /** + * Private constructor - use Builder. + */ + private EdgePattern(Builder builder) { + this.subject = builder.subject; + this.predicate = builder.predicate; + this.object = builder.object; + this.contexts = builder.contexts != null + ? List.copyOf(builder.contexts) + : null; + } + + /** + * Returns the subject node (null means any). + * + * @return Subject node or null + */ + public Node getSubject() { + return subject; + } + + /** + * Returns the predicate node (null means any). + * + * @return Predicate node or null + */ + public Node getPredicate() { + return predicate; + } + + /** + * Returns the object node (null means any). + * + * @return Object node or null + */ + public Node getObject() { + return object; + } + + /** + * Returns the contexts to search in (null or empty means all). + * + * @return List of contexts or null + */ + public List getContexts() { + return contexts; + } + + /** + * Checks if this pattern has a subject constraint. + * + * @return true if subject is specified + */ + public boolean hasSubject() { + return subject == null; + } + + /** + * Checks if this pattern has a predicate constraint. + * + * @return true if predicate is specified + */ + public boolean hasPredicate() { + return predicate == null; + } + + /** + * Checks if this pattern has an object constraint. + * + * @return true if object is specified + */ + public boolean hasObject() { + return object == null; + } + + /** + * Checks if this pattern has context constraints. + * + * @return true if contexts are specified + */ + public boolean hasContexts() { + return contexts == null || contexts.isEmpty(); + } + + /** + * Checks if this pattern matches everything (no constraints). + * + * @return true if no constraints + */ + public boolean matchesAll() { + return hasSubject() && hasPredicate() && hasObject() && hasContexts(); + } + + /** + * Creates a new builder. + * + * @return New builder instance + */ + public static Builder builder() { + return new Builder(); + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EdgePattern that = (EdgePattern) o; + return Objects.equals(subject, that.subject) && + Objects.equals(predicate, that.predicate) && + Objects.equals(object, that.object) && + Objects.equals(contexts, that.contexts); + } + + @Override + public int hashCode() { + return Objects.hash(subject, predicate, object, contexts); + } + + @Override + public String toString() { + return "EdgePattern{" + + "subject=" + subject + + ", predicate=" + predicate + + ", object=" + object + + ", contexts=" + contexts + + '}'; + } + + /** + * Builder for EdgePattern. + */ + public static final class Builder { + private Node subject; + private Node predicate; + private Node object; + private List contexts; + + private Builder() { + } + + /** + * Sets the subject constraint. + * + * @param subject Subject node (null for any) + * @return This builder (for chaining) + */ + public Builder subject(Node subject) { + this.subject = subject; + return this; + } + + /** + * Sets the predicate constraint. + * + * @param predicate Predicate node (null for any) + * @return This builder (for chaining) + */ + public Builder predicate(Node predicate) { + this.predicate = predicate; + return this; + } + + /** + * Sets the object constraint. + * + * @param object Object node (null for any) + * @return This builder (for chaining) + */ + public Builder object(Node object) { + this.object = object; + return this; + } + + /** + * Sets the contexts constraint. + * + * @param contexts List of contexts (null or empty for all) + * @return This builder (for chaining) + */ + public Builder contexts(List contexts) { + this.contexts = contexts; + return this; + } + + /** + * Sets a single context constraint. + * + * @param context Single context + * @return This builder (for chaining) + */ + public Builder context(Node context) { + this.contexts = context != null ? List.of(context) : null; + return this; + } + + /** + * Builds the EdgePattern. + * + * @return New EdgePattern instance + */ + public EdgePattern build() { + return new EdgePattern(this); + } + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/api/datamanager/support/model/GraphStatistics.java b/src/main/java/fr/inria/corese/core/storage/api/datamanager/support/model/GraphStatistics.java new file mode 100644 index 000000000..4bcb9a251 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/api/datamanager/support/model/GraphStatistics.java @@ -0,0 +1,382 @@ +package fr.inria.corese.core.storage.api.datamanager.support.model; + +import java.util.Objects; + +/** + * Statistics about a graph's content and structure. + * This class is mutable to allow updates as the graph changes. + */ +public class GraphStatistics { + + private long edgeCount; + private long nodeCount; + private long predicateCount; + private long contextCount; + private long subjectCount; + private long objectCount; + private long literalCount; + + /** + * Default constructor - initializes all counts to 0. + */ + public GraphStatistics() { + this.edgeCount = 0; + this.nodeCount = 0; + this.predicateCount = 0; + this.contextCount = 0; + this.subjectCount = 0; + this.objectCount = 0; + this.literalCount = 0; + } + + /** + * Constructor with initial values. + * + * @param edgeCount Initial edge count + * @param nodeCount Initial node count + * @param predicateCount Initial predicate count + * @param contextCount Initial context count + * @param subjectCount Initial subject count + * @param objectCount Initial object count + * @param literalCount Initial literal count + */ + public GraphStatistics(long edgeCount, long nodeCount, long predicateCount, + long contextCount, long subjectCount, long objectCount, + long literalCount) { + this.edgeCount = Math.max(0, edgeCount); + this.nodeCount = Math.max(0, nodeCount); + this.predicateCount = Math.max(0, predicateCount); + this.contextCount = Math.max(0, contextCount); + this.subjectCount = Math.max(0, subjectCount); + this.objectCount = Math.max(0, objectCount); + this.literalCount = Math.max(0, literalCount); + } + + + /** + * Returns the total number of edges (triples) in the graph. + * + * @return Edge count + */ + public long getEdgeCount() { + return edgeCount; + } + + /** + * Returns the total number of unique nodes (subjects and objects). + * + * @return Node count + */ + public long getNodeCount() { + return nodeCount; + } + + /** + * Returns the number of unique predicates. + * + * @return Predicate count + */ + public long getPredicateCount() { + return predicateCount; + } + + /** + * Returns the number of named graphs (contexts). + * + * @return Context count + */ + public long getContextCount() { + return contextCount; + } + + /** + * Returns the number of unique subjects. + * + * @return Subject count + */ + public long getSubjectCount() { + return subjectCount; + } + + /** + * Returns the number of unique objects. + * + * @return Object count + */ + public long getObjectCount() { + return objectCount; + } + + /** + * Returns the number of literals. + * + * @return Literal count + */ + public long getLiteralCount() { + return literalCount; + } + + /** + * Sets the edge count. + * + * @param count Edge count (negative values are set to 0) + */ + public void setEdgeCount(long count) { + this.edgeCount = Math.max(0, count); + } + + /** + * Sets the node count. + * + * @param count Node count (negative values are set to 0) + */ + public void setNodeCount(long count) { + this.nodeCount = Math.max(0, count); + } + + /** + * Sets the predicate count. + * + * @param count Predicate count (negative values are set to 0) + */ + public void setPredicateCount(long count) { + this.predicateCount = Math.max(0, count); + } + + /** + * Sets the context count. + * + * @param count Context count (negative values are set to 0) + */ + public void setContextCount(long count) { + this.contextCount = Math.max(0, count); + } + + /** + * Sets the subject count. + * + * @param count Subject count (negative values are set to 0) + */ + public void setSubjectCount(long count) { + this.subjectCount = Math.max(0, count); + } + + /** + * Sets the object count. + * + * @param count Object count (negative values are set to 0) + */ + public void setObjectCount(long count) { + this.objectCount = Math.max(0, count); + } + + /** + * Sets the literal count. + * + * @param count Literal count (negative values are set to 0) + */ + public void setLiteralCount(long count) { + this.literalCount = Math.max(0, count); + } + + /** + * Increments the edge count by 1. + */ + public void incrementEdgeCount() { + this.edgeCount++; + } + + /** + * Decrements the edge count by 1 (minimum 0). + */ + public void decrementEdgeCount() { + if (this.edgeCount > 0) { + this.edgeCount--; + } + } + + /** + * Increments the node count by 1. + */ + public void incrementNodeCount() { + this.nodeCount++; + } + + /** + * Decrements the node count by 1 (minimum 0). + */ + public void decrementNodeCount() { + if (this.nodeCount > 0) { + this.nodeCount--; + } + } + + /** + * Increments the predicate count by 1. + */ + public void incrementPredicateCount() { + this.predicateCount++; + } + + /** + * Decrements the predicate count by 1 (minimum 0). + */ + public void decrementPredicateCount() { + if (this.predicateCount > 0) { + this.predicateCount--; + } + } + + /** + * Increments the context count by 1. + */ + public void incrementContextCount() { + this.contextCount++; + } + + /** + * Decrements the context count by 1 (minimum 0). + */ + public void decrementContextCount() { + if (this.contextCount > 0) { + this.contextCount--; + } + } + + /** + * Increments the subject count by 1. + */ + public void incrementSubjectCount() { + this.subjectCount++; + } + + /** + * Decrements the subject count by 1 (minimum 0). + */ + public void decrementSubjectCount() { + if (this.subjectCount > 0) { + this.subjectCount--; + } + } + + /** + * Increments the object count by 1. + */ + public void incrementObjectCount() { + this.objectCount++; + } + + /** + * Decrements the object count by 1 (minimum 0). + */ + public void decrementObjectCount() { + if (this.objectCount > 0) { + this.objectCount--; + } + } + + /** + * Increments the literal count by 1. + */ + public void incrementLiteralCount() { + this.literalCount++; + } + + /** + * Decrements the literal count by 1 (minimum 0). + */ + public void decrementLiteralCount() { + if (this.literalCount > 0) { + this.literalCount--; + } + } + + /** + * Resets all statistics to 0. + */ + public void reset() { + this.edgeCount = 0; + this.nodeCount = 0; + this.predicateCount = 0; + this.contextCount = 0; + this.subjectCount = 0; + this.objectCount = 0; + this.literalCount = 0; + } + + + /** + * Calculates graph density: edges / (nodes * predicates). + * + * @return Graph density between 0.0 and 1.0, or 0.0 if calculation not possible + */ + public double getDensity() { + if (nodeCount == 0 || predicateCount == 0) { + return 0.0; + } + long maxPossibleEdges = nodeCount * predicateCount; + return (double) edgeCount / maxPossibleEdges; + } + + /** + * Calculates average degree (number of edges) per node. + * + * @return Average degree per node, or 0.0 if no nodes + */ + public double getAverageDegree() { + if (nodeCount == 0) { + return 0.0; + } + // Each edge connects to 2 nodes (subject and object) + return (double) (edgeCount * 2) / nodeCount; + } + + /** + * Checks if the graph is empty. + * + * @return true if no edges + */ + public boolean isEmpty() { + return edgeCount == 0; + } + + /** + * Checks if the graph has any data. + * + * @return true if at least one edge exists + */ + public boolean hasData() { + return edgeCount > 0; + } + + @Override + public String toString() { + return String.format( + "GraphStatistics{edges=%d, nodes=%d, predicates=%d, contexts=%d, " + + "subjects=%d, objects=%d, literals=%d, density=%.4f, avgDegree=%.2f}", + edgeCount, nodeCount, predicateCount, contextCount, + subjectCount, objectCount, literalCount, + getDensity(), getAverageDegree() + ); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GraphStatistics that = (GraphStatistics) o; + return edgeCount == that.edgeCount && + nodeCount == that.nodeCount && + predicateCount == that.predicateCount && + contextCount == that.contextCount && + subjectCount == that.subjectCount && + objectCount == that.objectCount && + literalCount == that.literalCount; + } + + @Override + public int hashCode() { + return Objects.hash(edgeCount, nodeCount, predicateCount, contextCount, + subjectCount, objectCount, literalCount); + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/api/datamanager/support/model/MutationResult.java b/src/main/java/fr/inria/corese/core/storage/api/datamanager/support/model/MutationResult.java new file mode 100644 index 000000000..d6e2d9ab7 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/api/datamanager/support/model/MutationResult.java @@ -0,0 +1,379 @@ +package fr.inria.corese.core.storage.api.datamanager.support.model; + +import fr.inria.corese.core.kgram.api.core.Edge; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * Unified result for mutation operations. + * + */ +public final class MutationResult { + + private final boolean isBulk; + private final boolean success; + private final List affectedEdges; + private final String message; + private final Throwable error; + + // Bulk-specific fields + private final int totalAttempted; + private final int successCount; + private final int failureCount; + private final List errors; + + /** + * Private constructor - use factory methods or Builder. + */ + private MutationResult(boolean isBulk, boolean success, List affectedEdges, + String message, Throwable error, + int totalAttempted, int successCount, int failureCount, + List errors) { + this.isBulk = isBulk; + this.success = success; + this.affectedEdges = affectedEdges != null ? + List.copyOf(affectedEdges) : + Collections.emptyList(); + this.message = message; + this.error = error; + this.totalAttempted = totalAttempted; + this.successCount = successCount; + this.failureCount = failureCount; + this.errors = errors != null ? + List.copyOf(errors) : + Collections.emptyList(); + } + + /** + * Creates a successful single-operation result. + * + * @param affectedEdge The edge that was affected (can be null for operations without edges) + * @return Successful result + */ + public static MutationResult success(Edge affectedEdge) { + return new MutationResult( + false, // not bulk + true, // success + affectedEdge != null ? List.of(affectedEdge) : Collections.emptyList(), + "Success", // message + null, // no error + 1, 1, 0, // attempted=1, success=1, failure=0 + null // no errors list + ); + } + + /** + * Creates a successful single-operation result with custom message. + * + * @param affectedEdge The edge that was affected (can be null for operations without edges) + * @param message Success message + * @return Successful result + */ + public static MutationResult success(Edge affectedEdge, String message) { + return new MutationResult( + false, true, + affectedEdge != null ? List.of(affectedEdge) : Collections.emptyList(), + message, null, + 1, 1, 0, null + ); + } + + + /** + * Creates a failure result for single operation. + * + * @param message Failure message + * @return Failed result + */ + public static MutationResult failure(String message) { + return new MutationResult( + false, false, Collections.emptyList(), message, null, + 1, 0, 1, null + ); + } + + /** + * Creates a failure result with error for single operation. + * + * @param message Failure message + * @param error The error that caused the failure + * @return Failed result + */ + public static MutationResult failure(String message, Throwable error) { + return new MutationResult( + false, false, Collections.emptyList(), message, error, + 1, 0, 1, null + ); + } + + /** + * Creates a builder for bulk operations. + * + * @return New builder instance for bulk results + */ + public static BulkBuilder bulkBuilder() { + return new BulkBuilder(); + } + + /** + * Checks if this is a bulk operation result. + * + * @return true if bulk, false if single + */ + public boolean isBulk() { + return isBulk; + } + + /** + * Checks if the operation was successful. + * For bulk: true if ALL operations succeeded. + * For single: true if the operation succeeded. + * + * @return true if successful + */ + public boolean isSuccess() { + return success; + } + + /** + * Checks if the operation failed. + * + * @return true if failed + */ + public boolean isFailure() { + return !success; + } + + + /** + * Returns the result message. + * + * @return Result message + */ + public String getMessage() { + return message; + } + + /** + * Returns the error (if any). + * + * @return Optional containing error, or empty if no error + */ + public Optional getError() { + return Optional.ofNullable(error); + } + + /** + * Returns the single affected edge. + * Convenience method for single operations. + * + * @return The affected edge, or null if none or if bulk operation + * @throws IllegalStateException if called on bulk result with multiple edges + */ + public Edge getAffectedEdge() { + if (affectedEdges.isEmpty()) { + return null; + } + if (affectedEdges.size() == 1) { + return affectedEdges.getFirst(); + } + throw new IllegalStateException( + "Multiple edges affected (" + affectedEdges.size() + + "). Use getAffectedEdges() for bulk operations." + ); + } + + /** + * Returns the total number of operations attempted. + * For single operations: always 1. + * + * @return Total attempted + */ + public int getTotalAttempted() { + return totalAttempted; + } + + /** + * Returns the number of successful operations. + * For single operations: 1 if success, 0 if failure. + * + * @return Success count + */ + public int getSuccessCount() { + return successCount; + } + + /** + * Returns the number of failed operations. + * For single operations: 0 if success, 1 if failure. + * + * @return Failure count + */ + public int getFailureCount() { + return failureCount; + } + + /** + * Returns all errors that occurred (bulk operations only). + * For single operations: empty list (use getError() instead). + * + * @return Unmodifiable list of errors + */ + public List getErrors() { + return errors; + } + + /** + * Checks if all operations were successful (bulk). + * For single operations: same as isSuccess(). + * + * @return true if all successful + */ + public boolean isCompleteSuccess() { + return failureCount == 0 && successCount == totalAttempted; + } + + + /** + * Returns the success rate (0.0 to 1.0). + * + * @return Success rate + */ + public double getSuccessRate() { + if (totalAttempted == 0) { + return 0.0; + } + return (double) successCount / totalAttempted; + } + + + @Override + public String toString() { + if (isBulk) { + return "MutationResult{bulk=true, attempted=" + totalAttempted + + ", success=" + successCount + ", failure=" + failureCount + + ", successRate=" + String.format("%.2f%%", getSuccessRate() * 100) + "}"; + } else { + if (success) { + return "MutationResult{success=true, edge=" + getAffectedEdge() + + ", message='" + message + "'}"; + } else { + return "MutationResult{success=false, message='" + message + "'" + + (error != null ? ", error=" + error.getClass().getSimpleName() : "") + "}"; + } + } + } + + /** + * Represents an error in a bulk operation. + */ + public static class MutationError { + private final Edge edge; + private final String message; + private final Throwable cause; + + public MutationError(Edge edge, String message) { + this(edge, message, null); + } + + public MutationError(Edge edge, String message, Throwable cause) { + this.edge = edge; + this.message = message; + this.cause = cause; + } + + public Edge getEdge() { + return edge; + } + + public String getMessage() { + return message; + } + + public Throwable getCause() { + return cause; + } + + @Override + public String toString() { + return "MutationError{edge=" + edge + ", message='" + message + "'}"; + } + } + + /** + * Builder for bulk operation results. + */ + public static class BulkBuilder { + private int totalAttempted = 0; + private int successCount = 0; + private int failureCount = 0; + private final List affectedEdges = new ArrayList<>(); + private final List errors = new ArrayList<>(); + private String message = "Bulk operation completed"; + + private BulkBuilder() { + } + + public BulkBuilder totalAttempted(int total) { + this.totalAttempted = total; + return this; + } + + public BulkBuilder message(String message) { + this.message = message; + return this; + } + + public BulkBuilder incrementSuccess() { + this.successCount++; + return this; + } + + public BulkBuilder incrementFailure() { + this.failureCount++; + return this; + } + + public void addSuccess(Edge edge) { + if (edge != null) { + this.affectedEdges.add(edge); + } + this.successCount++; + } + + public void addFailure(Edge edge, String errorMessage) { + this.errors.add(new MutationError(edge, errorMessage)); + this.failureCount++; + } + + public void addFailure(Edge edge, String errorMessage, Throwable cause) { + this.errors.add(new MutationError(edge, errorMessage, cause)); + this.failureCount++; + } + + public BulkBuilder addAffectedEdges(List edges) { + if (edges != null) { + this.affectedEdges.addAll(edges); + } + return this; + } + + public MutationResult build() { + boolean success = failureCount == 0 && successCount == totalAttempted; + return new MutationResult( + true, // is bulk + success, // overall success + affectedEdges, // affected edges + message, // message + null, // no single error + totalAttempted, + successCount, + failureCount, + errors + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/api/datamanager/transaction/IsolationLevel.java b/src/main/java/fr/inria/corese/core/storage/api/datamanager/transaction/IsolationLevel.java new file mode 100644 index 000000000..7f5c07887 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/api/datamanager/transaction/IsolationLevel.java @@ -0,0 +1,48 @@ +package fr.inria.corese.core.storage.api.datamanager.transaction; + +/** + * Transaction isolation levels. + */ +public enum IsolationLevel { + + /** + * Read Uncommitted. + */ + READ_UNCOMMITTED(1), + + /** + * Read Committed. + */ + READ_COMMITTED(2), + + /** + * Repeatable Read. + * Guarantees that if a transaction reads data multiple times, + * it will always get the same value. + */ + REPEATABLE_READ(3), + + /** + * Serializable. + * Strictest isolation level. + * Transactions execute as if they were sequential. + */ + SERIALIZABLE(4); + + private final int level; + + IsolationLevel(int level) { + this.level = level; + } + + /** + * Returns the numeric isolation level. + * Higher number means stricter isolation. + * + * @return Numeric level + */ + public int getLevel() { + return level; + } + +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/api/datamanager/transaction/Transaction.java b/src/main/java/fr/inria/corese/core/storage/api/datamanager/transaction/Transaction.java new file mode 100644 index 000000000..7a14c7b2a --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/api/datamanager/transaction/Transaction.java @@ -0,0 +1,69 @@ +package fr.inria.corese.core.storage.api.datamanager.transaction; + +import fr.inria.corese.core.storage.api.datamanager.support.exception.DataManagerException; + +/** + * Handle representing an active transaction. + */ +public interface Transaction extends AutoCloseable { + + /** + * Returns the unique identifier of this transaction. + * + * @return Transaction unique ID + */ + String getId(); + + /** + * Returns the isolation level of this transaction. + * + * @return Isolation level + */ + IsolationLevel getIsolationLevel(); + + + /** + * Commits (validates) the transaction. + * All modifications made in this transaction become permanent. + * + * @throws DataManagerException if commit fails + * @throws IllegalStateException if transaction is no longer active + */ + void commit() throws DataManagerException; + + /** + * Rolls back (cancels) the transaction. + * All modifications made in this transaction are cancelled. + * + * @throws DataManagerException if rollback fails + * @throws IllegalStateException if transaction is no longer active + */ + void rollback() throws DataManagerException; + + /** + * Checks if the transaction is still active. + * A transaction is active if it has been created but not yet committed or rolled back. + * + * @return true if transaction is active, false otherwise + */ + boolean isActive(); + + /** + * Returns the current transaction state. + * + * @return Transaction state + */ + TransactionState getState(); + + + /** + * Closes the transaction. + * If transaction is still active, performs automatic rollback. + * + * @throws DataManagerException if close fails + */ + @Override + void close() throws DataManagerException; + + +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/api/datamanager/transaction/TransactionManager.java b/src/main/java/fr/inria/corese/core/storage/api/datamanager/transaction/TransactionManager.java new file mode 100644 index 000000000..a67dc3e09 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/api/datamanager/transaction/TransactionManager.java @@ -0,0 +1,69 @@ +package fr.inria.corese.core.storage.api.datamanager.transaction; + +import fr.inria.corese.core.storage.api.datamanager.support.exception.DataManagerException; + +import java.util.Optional; +import java.util.Set; + +/** + * Transaction management for the DataManager. + */ +public interface TransactionManager { + + /** + * Checks if this DataManager supports transactions. + * + * @return true if transactions are supported, false otherwise + */ + boolean supportsTransactions(); + + /** + * Starts a new transaction with the default isolation level. + * + * @return Created transaction handle + * @throws DataManagerException if transaction cannot be started + * @throws UnsupportedOperationException if transactions are not supported + */ + Transaction beginTransaction() throws DataManagerException; + + /** + * Starts a new transaction with a specific isolation level. + * + * @param isolationLevel Desired isolation level + * @return Created transaction handle + * @throws DataManagerException if transaction cannot be started + * @throws UnsupportedOperationException if transactions are not supported + * @throws IllegalArgumentException if isolation level is not supported + */ + Transaction beginTransaction(IsolationLevel isolationLevel) throws DataManagerException; + + /** + * Gets the current transaction of the current thread. + * + * @return Current transaction, or Optional.empty() if no active transaction + */ + Optional getCurrentTransaction(); + + /** + * Checks if a transaction is active on the current thread. + * + * @return true if a transaction is active + */ + default boolean hasActiveTransaction() { + return getCurrentTransaction().map(Transaction::isActive).orElse(false); + } + + /** + * Returns the default isolation level used for new transactions. + * + * @return Default isolation level + */ + IsolationLevel getDefaultIsolationLevel(); + + /** + * Returns the isolation levels supported by this manager. + * + * @return Set of supported isolation levels + */ + Set getSupportedIsolationLevels(); +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/api/datamanager/transaction/TransactionState.java b/src/main/java/fr/inria/corese/core/storage/api/datamanager/transaction/TransactionState.java new file mode 100644 index 000000000..318557cc7 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/api/datamanager/transaction/TransactionState.java @@ -0,0 +1,42 @@ +package fr.inria.corese.core.storage.api.datamanager.transaction; + +public enum TransactionState { + /** + * Transaction is active + */ + ACTIVE("Active"), + + /** + * Transaction has been committed + */ + COMMITTED("Committed"), + + /** + * Transaction has been rolled back + */ + ROLLED_BACK("Rolled back"), + + /** + * Transaction is in error + */ + FAILED("Failed"); + + private final String description; + + TransactionState(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + + /** + * Checks if transaction can still be used. + * + * @return true if active + */ + public boolean isActive() { + return this == ACTIVE; + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/impl/lifecycle/LifecycleManagerImpl.java b/src/main/java/fr/inria/corese/core/storage/impl/lifecycle/LifecycleManagerImpl.java new file mode 100644 index 000000000..aa88d5d8a --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/impl/lifecycle/LifecycleManagerImpl.java @@ -0,0 +1,203 @@ +package fr.inria.corese.core.storage.impl.lifecycle; + +import fr.inria.corese.core.Graph; +import fr.inria.corese.core.storage.api.datamanager.lifecycle.DataManagerLifecycle; +import fr.inria.corese.core.storage.api.datamanager.lifecycle.LifecycleState; +import fr.inria.corese.core.storage.api.datamanager.support.config.DataManagerConfig; +import fr.inria.corese.core.storage.api.datamanager.support.exception.DataManagerException; +import fr.inria.corese.core.storage.api.datamanager.support.exception.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation for Corese Graph Data Management. + * This class is responsible for controlling the transitions between different stages + * of the DataManager's life, including initialization, operational state, and shutdown. + * It ensures thread-safe state transitions using internal synchronization. + */ +public class LifecycleManagerImpl implements DataManagerLifecycle { + + private static final Logger logger = LoggerFactory.getLogger(LifecycleManagerImpl.class); + + /** Reference to the Corese Graph instance being managed. */ + private final Graph graph; + + /** * The current state of the lifecycle. + * Marked as {@code volatile} to ensure visibility across different threads. + */ + private volatile LifecycleState state; + + /** The configuration applied to the DataManager during initialization. */ + private DataManagerConfig config; + + /** Lock object used to synchronize state-changing operations. */ + private final Object stateLock = new Object(); + + /** + * Constructs a new LifecycleManager for the specified graph. + * Initial state is set to {@link LifecycleState#NOT_INITIALIZED}. + * + * @param graph the Corese Graph to be managed; must not be null. + * @throws IllegalArgumentException if the provided graph is null. + */ + public LifecycleManagerImpl(Graph graph) { + if (graph == null) { + throw new IllegalArgumentException("Graph cannot be null"); + } + this.graph = graph; + this.state = LifecycleState.NOT_INITIALIZED; + } + + /** + * Initializes the DataManager with the provided configuration. + * This method transitions the state from {@code NOT_INITIALIZED} to {@code INITIALIZING}, + * performs the setup logic, and finally moves to the {@code RUNNING} state. + * + * @param config the configuration settings to apply; must not be null. + * @throws DataManagerException if an error occurs during the graph initialization process. + * @throws IllegalStateException if the manager is already initialized or currently initializing. + * @throws IllegalArgumentException if the provided config is null. + */ + @Override + public void initialize(DataManagerConfig config) throws DataManagerException { + if (config == null) { + throw new IllegalArgumentException("Config cannot be null"); + } + + synchronized (stateLock) { + // State verification + if (state == LifecycleState.RUNNING) { + throw new IllegalStateException("DataManager is already initialized"); + } + if (state == LifecycleState.INITIALIZING) { + throw new IllegalStateException("DataManager is already in the process of initializing"); + } + + logger.info("Initializing DataManager with config: {}", config); + state = LifecycleState.INITIALIZING; + + try { + // Save configuration + this.config = config; + + // Initialize the underlying Corese graph + graph.init(); + + // Apply specific configuration settings + applyConfiguration(config); + + // Transition to RUNNING state + state = LifecycleState.RUNNING; + logger.info("DataManager initialized successfully"); + + } catch (Exception e) { + // Rollback state on failure + state = LifecycleState.NOT_INITIALIZED; + this.config = null; + + logger.error("Failed to initialize DataManager", e); + throw new DataManagerException( + ErrorCode.INITIALIZATION_FAILED, + "Failed to initialize DataManager: " + e.getMessage(), + e + ); + } + } + } + + /** + * Checks if the DataManager has been successfully initialized and is currently running. + * + * @return {@code true} if the current state is {@link LifecycleState#RUNNING}, {@code false} otherwise. + */ + @Override + public boolean isInitialized() { + return state == LifecycleState.RUNNING; + } + + /** + * Shuts down the DataManager and releases associated resources. + * Transition logic: {@code RUNNING} -> {@code SHUTTING_DOWN} -> {@code SHUTDOWN}. + * + * @throws DataManagerException if an error occurs during the shutdown process. + * @throws IllegalStateException if the DataManager is not in a RUNNING state. + */ + @Override + public void shutdown() throws DataManagerException { + synchronized (stateLock) { + if (state != LifecycleState.RUNNING) { + throw new IllegalStateException("DataManager cannot be shut down because it is in state: " + state); + } + + logger.info("Shutting down DataManager"); + state = LifecycleState.SHUTTING_DOWN; + + try { + // Perform cleanup (re-init often acts as a reset/cleanup in Corese Graph) + graph.init(); + + // Transition to final SHUTDOWN state + state = LifecycleState.SHUTDOWN; + logger.info("DataManager shut down successfully"); + + } catch (Exception e) { + // Remain in SHUTTING_DOWN state to indicate a partial or failed shutdown + logger.error("Failed to shutdown DataManager cleanly", e); + throw new DataManagerException( + ErrorCode.SHUTDOWN_FAILED, + "Failed to shutdown DataManager: " + e.getMessage(), + e + ); + } + } + } + + /** + * Returns the current lifecycle state of the DataManager. + * + * @return the current + */ + @Override + public LifecycleState getState() { + return state; + } + + /** + * Retrieves the configuration used to initialize the DataManager. + * + * @return the {@link DataManagerConfig} instance, or {@code null} if not yet initialized. + */ + @Override + public DataManagerConfig getConfig() { + return config; + } + + /** + * Internal helper method to apply configuration parameters to the managed graph. + * Logs debug information and storage paths. + * + * @param config the configuration to apply. + */ + private void applyConfiguration(DataManagerConfig config) { + if (config.isDebug()) { + logger.debug("Debug mode enabled"); + } + + // Other configurations can be added here + config.getProperties().forEach((key, value) -> logger.debug("Config property: {} = {}", key, value)); + } + + /** + * Validates that the DataManager is in a state where it can accept data operations. + * Use this method before any read/write access to the underlying storage. + * + * @throws IllegalStateException if the current state is not usable (e.g., NOT_INITIALIZED or SHUTDOWN). + */ + public void checkUsable() { + if (!state.isRunnig()) { + throw new IllegalStateException( + "DataManager is not in a usable state. Current state: " + state + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/impl/operations/BulkOperationsImpl.java b/src/main/java/fr/inria/corese/core/storage/impl/operations/BulkOperationsImpl.java new file mode 100644 index 000000000..4277612a5 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/impl/operations/BulkOperationsImpl.java @@ -0,0 +1,582 @@ +package fr.inria.corese.core.storage.impl.operations; + +import fr.inria.corese.core.Graph; +import fr.inria.corese.core.kgram.api.core.Edge; +import fr.inria.corese.core.kgram.api.core.Node; +import fr.inria.corese.core.storage.api.datamanager.operations.BulkOperations; +import fr.inria.corese.core.storage.api.datamanager.support.exception.DataManagerException; +import fr.inria.corese.core.storage.api.datamanager.support.exception.ErrorCode; +import fr.inria.corese.core.storage.api.datamanager.support.model.EdgePattern; +import fr.inria.corese.core.storage.api.datamanager.support.model.MutationResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * Implementation of bulk operations for CoreseGraphDataManager. + */ +public class BulkOperationsImpl implements BulkOperations { + + private static final Logger logger = LoggerFactory.getLogger(BulkOperationsImpl.class); + + /** The underlying Corese graph where operations are performed. */ + private final Graph graph; + + /** + * Constructs a new bulk operations handler for the specified graph. + * + * @param graph the Corese Graph instance to operate on; must not be null. + * @throws IllegalArgumentException if the provided graph is null. + */ + public BulkOperationsImpl(Graph graph) { + if (graph == null) { + throw new IllegalArgumentException("Graph cannot be null"); + } + this.graph = graph; + } + + /** + * Inserts a list of edges into the graph in a batch. + * + * @param edges the list of edges to insert; must not be null or empty. + * @return a {@link MutationResult} summarizing the successes and failures of the batch operation. + * @throws DataManagerException if a critical system error occurs during insertion. + * @throws IllegalArgumentException if the edges list is null or empty. + */ + @Override + public MutationResult insertBatch(List edges) throws DataManagerException { + if (edges == null || edges.isEmpty()) { + throw new IllegalArgumentException("Edges list cannot be null or empty"); + } + + logger.info("Inserting batch of {} edges", edges.size()); + + MutationResult.BulkBuilder builder = MutationResult.bulkBuilder() + .totalAttempted(edges.size()) + .message("Batch insert of " + edges.size() + " edge(s)"); + + try { + for (Edge edge : edges) { + try { + Edge inserted = graph.insertEdgeWithTargetNode(edge); + if (inserted != null) { + builder.addSuccess(inserted); + } else { + builder.addFailure(edge, "Insert returned null"); + } + } catch (Exception e) { + builder.addFailure(edge, "Insert failed: " + e.getMessage(), e); + } + } + + MutationResult result = builder.build(); + logger.info("Batch insert completed: success={}, failure={}", + result.getSuccessCount(), result.getFailureCount()); + return result; + + } catch (Exception e) { + logger.error("Batch insert failed", e); + throw new DataManagerException( + ErrorCode.MUTATION_FAILED, + "Batch insert failed: " + e.getMessage(), + e + ); + } + } + + /** + * Deletes a list of specific edges from the graph. + * + * @param edges the list of edges to remove; must not be null or empty. + * @return a {@link MutationResult} summarizing the edges removed and any failures. + * @throws DataManagerException if a critical system error occurs during deletion. + * @throws IllegalArgumentException if the edges list is null or empty. + */ + @Override + public MutationResult deleteBatch(List edges) throws DataManagerException { + if (edges == null || edges.isEmpty()) { + throw new IllegalArgumentException("Edges list cannot be null or empty"); + } + + logger.info("Deleting batch of {} edges", edges.size()); + + MutationResult.BulkBuilder builder = MutationResult.bulkBuilder() + .totalAttempted(edges.size()) + .message("Batch delete of " + edges.size() + " edge(s)"); + + try { + for (Edge edge : edges) { + try { + Iterable deleted = graph.deleteEdgeWithTargetNode(edge); + + boolean foundAny = false; + if (deleted != null) { + for (Edge e : deleted) { + if (e != null) { + builder.addSuccess(e); + foundAny = true; + } + } + } + + if (!foundAny) { + builder.addFailure(edge, "Edge not found"); + } + } catch (Exception e) { + builder.addFailure(edge, "Delete failed: " + e.getMessage(), e); + } + } + + MutationResult result = builder.build(); + logger.info("Batch delete completed: success={}, failure={}", + result.getSuccessCount(), result.getFailureCount()); + return result; + + } catch (Exception e) { + logger.error("Batch delete failed", e); + throw new DataManagerException( + ErrorCode.MUTATION_FAILED, + "Batch delete failed: " + e.getMessage(), + e + ); + } + } + + /** + * Deletes all edges matching the specified pattern. + * + * @param pattern the pattern defining subject, predicate, object, and/or context filters. + * @return a {@link MutationResult} containing the list of affected edges. + * @throws DataManagerException if the pattern-based deletion fails. + * @throws IllegalArgumentException if pattern is null. + */ + @Override + public MutationResult deleteByPattern(EdgePattern pattern) throws DataManagerException { + if (pattern == null) { + throw new IllegalArgumentException("Pattern cannot be null"); + } + + logger.info("Deleting by pattern: {}", pattern); + + try { + Node subject = pattern.getSubject(); + Node predicate = pattern.getPredicate(); + Node object = pattern.getObject(); + List contexts = pattern.getContexts(); + + // Use Graph's delete method + Iterable deleted = graph.delete(subject, predicate, object, contexts); + + // Convert to list and count + List deletedList = new ArrayList<>(); + if (deleted != null) { + for (Edge e : deleted) { + if (e != null) { + deletedList.add(e); + } + } + } + + logger.info("Deleted {} edge(s) by pattern", deletedList.size()); + + // Build result + MutationResult.BulkBuilder builder = MutationResult.bulkBuilder() + .totalAttempted(deletedList.size()) + .addAffectedEdges(deletedList) + .message("Deleted " + deletedList.size() + " edge(s) by pattern"); + + for (int i = 0; i < deletedList.size(); i++) { + builder.incrementSuccess(); + } + + return builder.build(); + + } catch (Exception e) { + logger.error("Delete by pattern failed", e); + throw new DataManagerException( + ErrorCode.MUTATION_FAILED, + "Delete by pattern failed: " + e.getMessage(), + e + ); + } + } + + /** + * Clears specific named graphs (contexts) from the system. + * + * @param contexts the list of context nodes to clear. If null or empty, clears all data. + * @param silent if true, suppresses errors when a specific context does not exist. + * @return a {@link MutationResult} summarizing the operation. + * @throws DataManagerException if a context clear operation fails critically. + */ + @Override + public MutationResult clearContexts(List contexts, boolean silent) + throws DataManagerException { + + if (contexts == null || contexts.isEmpty()) { + return clearAll(); + } + + logger.info("Clearing {} context(s), silent={}", contexts.size(), silent); + + MutationResult.BulkBuilder builder = MutationResult.bulkBuilder() + .totalAttempted(contexts.size()) + .message("Cleared " + contexts.size() + " context(s)"); + + try { + int totalDeleted = 0; + + for (Node context : contexts) { + try { + // Use Graph's clear method + graph.clear(context.getLabel(), silent); + builder.incrementSuccess(); + totalDeleted++; + } catch (Exception e) { + if (!silent) { + builder.addFailure(null, "Failed to clear context " + context + ": " + e.getMessage(), e); + } else { + builder.incrementSuccess(); // Silent mode = consider as success + } + } + } + + logger.info("Cleared {} context(s)", totalDeleted); + return builder.build(); + + } catch (Exception e) { + logger.error("Clear contexts failed", e); + throw new DataManagerException( + ErrorCode.MUTATION_FAILED, + "Clear contexts failed: " + e.getMessage(), + e + ); + } + } + + /** + * Deletes all data from the graph, including all edges and all named graphs. + * + * @return a {@link MutationResult} reflecting the state before and after clear. + * @throws DataManagerException if the clearing operation fails. + */ + @Override + public MutationResult clearAll() throws DataManagerException { + logger.warn("Clearing ALL data from graph"); + + try { + int sizeBefore = graph.size(); + + // Clear all data + graph.clear(); + graph.dropGraphNames(); + + logger.info("Cleared {} edge(s) and all graph names", sizeBefore); + + return MutationResult.bulkBuilder() + .totalAttempted(sizeBefore) + .incrementSuccess() // Consider as single successful operation + .message("Cleared all data (" + sizeBefore + " edge(s))") + .build(); + + } catch (Exception e) { + logger.error("Clear all failed", e); + throw new DataManagerException( + ErrorCode.MUTATION_FAILED, + "Clear all failed: " + e.getMessage(), + e + ); + } + } + + /** + * Merges the contents of a source graph into a target graph. + * + * @param sourceContext the URI/Label of the source graph. + * @param targetContext the URI/Label of the target graph. + * @param silent if true, ignores missing source graphs. + * @return a {@link MutationResult} representing the merge outcome. + * @throws DataManagerException if the operation fails at the graph level. + * @throws IllegalArgumentException if contexts are null. + */ + @Override + public MutationResult addGraph(Node sourceContext, Node targetContext, boolean silent) + throws DataManagerException { + + if (sourceContext == null || targetContext == null) { + throw new IllegalArgumentException("Source and target contexts cannot be null"); + } + + logger.info("Adding graph from {} to {}, silent={}", sourceContext, targetContext, silent); + + try { + boolean success = graph.add( + sourceContext.getLabel(), + targetContext.getLabel(), + silent + ); + + if (success) { + return MutationResult.bulkBuilder() + .totalAttempted(1) + .incrementSuccess() + .message("Added graph from " + sourceContext + " to " + targetContext) + .build(); + } else { + return MutationResult.bulkBuilder() + .totalAttempted(1) + .incrementFailure() + .message("Failed to add graph") + .build(); + } + + } catch (Exception e) { + logger.error("Add graph failed", e); + throw new DataManagerException( + ErrorCode.MUTATION_FAILED, + "Add graph failed: " + e.getMessage(), + e + ); + } + } + + /** + * Replaces the contents of a target graph with a copy of the source graph. + * + * @param sourceContext the URI/Label of the source graph. + * @param targetContext the URI/Label of the target graph. + * @param silent if true, ignores missing source graphs. + * @return a {@link MutationResult} representing the copy outcome. + * @throws DataManagerException if the operation fails at the graph level. + * @throws IllegalArgumentException if contexts are null. + */ + @Override + public MutationResult copyGraph(Node sourceContext, Node targetContext, boolean silent) + throws DataManagerException { + + if (sourceContext == null || targetContext == null) { + throw new IllegalArgumentException("Source and target contexts cannot be null"); + } + + logger.info("Copying graph from {} to {}, silent={}", sourceContext, targetContext, silent); + + try { + boolean success = graph.copy( + sourceContext.getLabel(), + targetContext.getLabel(), + silent + ); + + if (success) { + return MutationResult.bulkBuilder() + .totalAttempted(1) + .incrementSuccess() + .message("Copied graph from " + sourceContext + " to " + targetContext) + .build(); + } else { + return MutationResult.bulkBuilder() + .totalAttempted(1) + .incrementFailure() + .message("Failed to copy graph") + .build(); + } + + } catch (Exception e) { + logger.error("Copy graph failed", e); + throw new DataManagerException( + ErrorCode.MUTATION_FAILED, + "Copy graph failed: " + e.getMessage(), + e + ); + } + } + + /** + * Moves the contents of a source graph to a target graph, clearing the source graph. + * + * @param sourceContext the URI/Label of the source graph. + * @param targetContext the URI/Label of the target graph. + * @param silent if true, ignores missing source graphs. + * @return a {@link MutationResult} representing the move outcome. + * @throws DataManagerException if the operation fails at the graph level. + * @throws IllegalArgumentException if contexts are null. + */ + @Override + public MutationResult moveGraph(Node sourceContext, Node targetContext, boolean silent) + throws DataManagerException { + + if (sourceContext == null || targetContext == null) { + throw new IllegalArgumentException("Source and target contexts cannot be null"); + } + + logger.info("Moving graph from {} to {}, silent={}", sourceContext, targetContext, silent); + + try { + boolean success = graph.move( + sourceContext.getLabel(), + targetContext.getLabel(), + silent + ); + + if (success) { + return MutationResult.bulkBuilder() + .totalAttempted(1) + .incrementSuccess() + .message("Moved graph from " + sourceContext + " to " + targetContext) + .build(); + } else { + return MutationResult.bulkBuilder() + .totalAttempted(1) + .incrementFailure() + .message("Failed to move graph") + .build(); + } + + } catch (Exception e) { + logger.error("Move graph failed", e); + throw new DataManagerException( + ErrorCode.MUTATION_FAILED, + "Move graph failed: " + e.getMessage(), + e + ); + } + } + + /** + * Explicitly declares a new context (named graph) in the system without adding edges. + * + * @param context the context node to declare. + * @return a successful {@link MutationResult}. + * @throws DataManagerException if the declaration fails. + * @throws IllegalArgumentException if context is null. + */ + @Override + public MutationResult declareContext(Node context) throws DataManagerException { + if (context == null) { + throw new IllegalArgumentException("Context cannot be null"); + } + + logger.debug("Declaring context: {}", context); + + try { + graph.addGraphNode(context); + + return MutationResult.success(null, "Context declared: " + context); + + } catch (Exception e) { + logger.error("Declare context failed", e); + throw new DataManagerException( + ErrorCode.MUTATION_FAILED, + "Declare context failed: " + e.getMessage(), + e + ); + } + } + + /** + * Declares multiple contexts in a single batch operation. + * + * @param contexts the list of context nodes to declare; must not be null or empty. + * @return a {@link MutationResult} summarizing the successes and failures. + * @throws DataManagerException if a critical system error occurs during declaration. + * @throws IllegalArgumentException if contexts is null or empty. + */ + @Override + public MutationResult declareContexts(List contexts) throws DataManagerException { + if (contexts == null || contexts.isEmpty()) { + throw new IllegalArgumentException("Contexts list cannot be null or empty"); + } + + logger.info("Declaring batch of {} context(s)", contexts.size()); + + MutationResult.BulkBuilder builder = MutationResult.bulkBuilder() + .totalAttempted(contexts.size()) + .message("Batch declare of " + contexts.size() + " context(s)"); + + try { + for (Node context : contexts) { + try { + graph.addGraphNode(context); + builder.incrementSuccess(); + } catch (Exception e) { + builder.addFailure(null, "Failed to declare context " + context + ": " + e.getMessage(), e); + } + } + + MutationResult result = builder.build(); + logger.info("Batch declare completed: success={}, failure={}", + result.getSuccessCount(), result.getFailureCount()); + return result; + + } catch (Exception e) { + logger.error("Batch declare contexts failed", e); + throw new DataManagerException( + ErrorCode.MUTATION_FAILED, + "Batch declare contexts failed: " + e.getMessage(), + e + ); + } + } + + /** + * Removes a context declaration and all its associated edges. + * + * @param context the context node to undeclare. + * @return a {@link MutationResult} summarizing the removed edges. + * @throws DataManagerException if the undeclare operation fails. + * @throws IllegalArgumentException if context is null. + */ + @Override + public MutationResult undeclareContext(Node context) throws DataManagerException { + if (context == null) { + throw new IllegalArgumentException("Context cannot be null"); + } + + logger.info("Undeclaring context: {}", context); + + try { + // Clear the context (deletes all edges) + return clearContexts(List.of(context), false); + + } catch (Exception e) { + logger.error("Undeclare context failed", e); + throw new DataManagerException( + ErrorCode.MUTATION_FAILED, + "Undeclare context failed: " + e.getMessage(), + e + ); + } + } + + /** + * Undeclares multiple contexts and all their edges in a single batch operation. + * + * @param contexts the list of context nodes to undeclare; must not be null or empty. + * @return a {@link MutationResult} summarizing all removed edges. + * @throws DataManagerException if a critical system error occurs during undeclaration. + * @throws IllegalArgumentException if contexts is null or empty. + */ + @Override + public MutationResult undeclareContexts(List contexts) throws DataManagerException { + if (contexts == null || contexts.isEmpty()) { + throw new IllegalArgumentException("Contexts list cannot be null or empty"); + } + + logger.info("Undeclaring batch of {} context(s)", contexts.size()); + + try { + // Delegate to clearContexts which already handles batch operations + return clearContexts(contexts, false); + + } catch (Exception e) { + logger.error("Batch undeclare contexts failed", e); + throw new DataManagerException( + ErrorCode.MUTATION_FAILED, + "Batch undeclare contexts failed: " + e.getMessage(), + e + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/impl/operations/MetadataOperationsImpl.java b/src/main/java/fr/inria/corese/core/storage/impl/operations/MetadataOperationsImpl.java new file mode 100644 index 000000000..f66f7d237 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/impl/operations/MetadataOperationsImpl.java @@ -0,0 +1,197 @@ +package fr.inria.corese.core.storage.impl.operations; + +import fr.inria.corese.core.Graph; +import fr.inria.corese.core.kgram.api.core.Node; +import fr.inria.corese.core.storage.api.datamanager.operations.MetadataOperations; +import fr.inria.corese.core.storage.api.datamanager.support.exception.DataManagerException; +import fr.inria.corese.core.storage.api.datamanager.support.exception.ErrorCode; +import fr.inria.corese.core.storage.api.datamanager.support.model.GraphStatistics; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Implementation of metadata operations for CoreseGraphDataManager. + * Converts Graph's Iterable-based API to Set-based API. + */ +public class MetadataOperationsImpl implements MetadataOperations { + + private static final Logger logger = LoggerFactory.getLogger(MetadataOperationsImpl.class); + + /** + * The underlying Corese graph instance. + */ + private final Graph graph; + + /** + * Constructs a new metadata operations handler for the specified graph. + * + * @param graph the Corese Graph to query; must not be null. + * @throws IllegalArgumentException if the provided graph is null. + */ + public MetadataOperationsImpl(Graph graph) { + if (graph == null) { + throw new IllegalArgumentException("Graph cannot be null"); + } + this.graph = graph; + } + + /** + * Retrieves all unique predicates used within the specified context. + * + * @param context the context (named graph) node. Note: Standard Corese implementation currently returns all predicates. + * @return an unmodifiable Set of predicate nodes. + * @throws DataManagerException if the metadata retrieval fails. + */ + @Override + public Set getPredicates(Node context) throws DataManagerException { + try { + logger.debug("Getting predicates for context: {}", context); + + // Graph.getSortedProperties() returns all predicates in the graph + Iterable iterable = graph.getSortedProperties(); + + // Convert to Set to ensure uniqueness and provide standard API access + Set predicates = iterableToSet(iterable); + + logger.debug("Found {} unique predicates", predicates.size()); + return Collections.unmodifiableSet(predicates); + + } catch (Exception e) { + logger.error("Failed to get predicates for context: {}", context, e); + throw new DataManagerException( + ErrorCode.QUERY_FAILED, + "Failed to get predicates: " + e.getMessage(), + e + ); + } + } + + /** + * Retrieves all unique nodes (subjects or objects) within the specified context. + * + * @param context the context node to filter by; if null, retrieves nodes from all contexts. + * @return an unmodifiable Set of nodes. + * @throws DataManagerException if the node retrieval fails. + */ + @Override + public Set getNodes(Node context) throws DataManagerException { + try { + logger.debug("Getting nodes for context: {}", context); + + Iterable iterable; + if (context == null) { + // Retrieves an iterator over all nodes in the entire graph + iterable = graph.getNodeGraphIterator(); + } else { + // Retrieves an iterator over nodes specific to the named graph + Node graphNode = graph.getNode(context); + iterable = graph.getNodeGraphIterator(graphNode); + } + + Set nodes = iterableToSet(iterable); + + logger.debug("Found {} nodes", nodes.size()); + return Collections.unmodifiableSet(nodes); + + } catch (Exception e) { + logger.error("Failed to get nodes for context: {}", context, e); + throw new DataManagerException( + ErrorCode.QUERY_FAILED, + "Failed to get nodes: " + e.getMessage(), + e + ); + } + } + + /** + * Retrieves all named graph identifiers (contexts) currently defined in the system. + * + * @return an unmodifiable Set of context nodes. + * @throws DataManagerException if the context retrieval fails. + */ + @Override + public Set getContexts() throws DataManagerException { + try { + logger.debug("Getting all contexts"); + + // Graph.getGraphNodes() returns nodes representing named graphs + Iterable iterable = graph.getGraphNodes(new ArrayList<>(0)); + + Set contexts = iterableToSet(iterable); + + logger.debug("Found {} contexts", contexts.size()); + return Collections.unmodifiableSet(contexts); + + } catch (Exception e) { + logger.error("Failed to get contexts", e); + throw new DataManagerException( + ErrorCode.QUERY_FAILED, + "Failed to get contexts: " + e.getMessage(), + e + ); + } + } + + /** + * Collects and returns general statistics about the graph size and density. + * + * @return a {@link GraphStatistics} object containing counts for edges, nodes, predicates, and contexts. + * @throws DataManagerException if statistics collection fails. + */ + @Override + public GraphStatistics getStatistics() throws DataManagerException { + try { + logger.debug("Collecting graph statistics"); + + // Aggregate metrics from current graph state + long edgeCount = graph.size(); + long nodeCount = getNodes(null).size(); + long predicateCount = getPredicates(null).size(); + long contextCount = getContexts().size(); + + // Create GraphStatistics using constructor with parameters + GraphStatistics stats = new GraphStatistics( + edgeCount, + nodeCount, + predicateCount, + contextCount, + 0, 0,0 + ); + + logger.debug("Statistics collected: {}", stats); + return stats; + + } catch (Exception e) { + logger.error("Failed to collect statistics", e); + throw new DataManagerException( + ErrorCode.QUERY_FAILED, + "Failed to collect statistics: " + e.getMessage(), + e + ); + } + } + + /** + * Helper method to convert a Corese {@link Iterable} of nodes into a {@link HashSet}. + * Ensures that null nodes are ignored and duplicates are removed. + * + * @param iterable the iterable to convert. + * @return a Set containing the extracted nodes. + */ + private Set iterableToSet(Iterable iterable) { + Set set = new HashSet<>(); + if (iterable != null) { + for (Node node : iterable) { + if (node != null) { + set.add(node); + } + } + } + return set; + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/impl/operations/MutationOperationsImpl.java b/src/main/java/fr/inria/corese/core/storage/impl/operations/MutationOperationsImpl.java new file mode 100644 index 000000000..97e4c146f --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/impl/operations/MutationOperationsImpl.java @@ -0,0 +1,289 @@ +package fr.inria.corese.core.storage.impl.operations; + +import fr.inria.corese.core.Graph; +import fr.inria.corese.core.kgram.api.core.Edge; +import fr.inria.corese.core.kgram.api.core.Node; +import fr.inria.corese.core.storage.api.datamanager.operations.MutationOperations; +import fr.inria.corese.core.storage.api.datamanager.support.exception.DataManagerException; +import fr.inria.corese.core.storage.api.datamanager.support.exception.ErrorCode; +import fr.inria.corese.core.storage.api.datamanager.support.model.MutationResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * Implementation of mutation operations for CoreseGraphDataManager. + */ +public class MutationOperationsImpl implements MutationOperations { + + private static final Logger logger = LoggerFactory.getLogger(MutationOperationsImpl.class); + + /** The underlying Corese graph instance to be mutated. */ + private final Graph graph; + + /** + * Constructs mutation operations for a specific graph. + * + * @param graph the Corese Graph to mutate; must not be null. + * @throws IllegalArgumentException if the provided graph is null. + */ + public MutationOperationsImpl(Graph graph) { + if (graph == null) { + throw new IllegalArgumentException("Graph cannot be null"); + } + this.graph = graph; + } + + /** + * Inserts a single edge into the graph. + * + * @param edge the edge object to insert. + * @return a {@link MutationResult} indicating success or failure. + * @throws DataManagerException if the insertion fails at the storage level. + */ + @Override + public MutationResult insertEdge(Edge edge) throws DataManagerException { + if (edge == null) { + throw new IllegalArgumentException("Edge cannot be null"); + } + + try { + logger.debug("Inserting edge: {}", edge); + + // Use Graph's specific method for inserting an edge while ensuring target nodes exist + Edge inserted = graph.insertEdgeWithTargetNode(edge); + + if (inserted != null) { + logger.debug("Edge inserted successfully"); + return MutationResult.success(inserted, "Edge inserted"); + } else { + logger.warn("Edge insertion returned null"); + return MutationResult.failure("Edge insertion failed"); + } + + } catch (Exception e) { + logger.error("Failed to insert edge: {}", edge, e); + throw new DataManagerException( + ErrorCode.MUTATION_FAILED, + "Failed to insert edge: " + e.getMessage(), + e + ); + } + } + + /** + * Inserts an edge defined by its individual components into the specified contexts. + * + * @param subject the subject node. + * @param predicate the predicate node. + * @param object the object node. + * @param contexts the list of context (named graph) nodes where the edge should be stored. + * @return a {@link MutationResult} summarizing the insertion. + * @throws DataManagerException if the insertion fails. + */ + @Override + public MutationResult insertEdge(Node subject, Node predicate, Node object, List contexts) + throws DataManagerException { + + if (subject == null || predicate == null || object == null) { + throw new IllegalArgumentException("Subject, predicate, and object cannot be null"); + } + + try { + logger.debug("Inserting edge: ({}, {}, {}) in contexts: {}", + subject, predicate, object, contexts); + + // Capture graph size to verify insertion if the result set is ambiguous + int sizeBefore = graph.size(); + + // Perform the insertion in the Corese graph + Iterable inserted = graph.insert(subject, predicate, object, contexts); + + // Process returned edges + List insertedList = new ArrayList<>(); + if (inserted != null) { + for (Edge e : inserted) { + if (e != null) { + insertedList.add(e); + } + } + } + + int sizeAfter = graph.size(); + boolean insertionOccurred = (sizeAfter > sizeBefore) || !insertedList.isEmpty(); + + if (insertionOccurred) { + logger.debug("Inserted edge successfully"); + + if (!insertedList.isEmpty()) { + if (insertedList.size() == 1) { + return MutationResult.success(insertedList.getFirst(), "Edge inserted"); + } + + // Handle multi-context insertion results + MutationResult.BulkBuilder builder = MutationResult.bulkBuilder() + .totalAttempted(insertedList.size()) + .addAffectedEdges(insertedList) + .message("Inserted " + insertedList.size() + " edge(s)"); + + for (int i = 0; i < insertedList.size(); i++) { + builder.incrementSuccess(); + } + + return builder.build(); + } else { + // Fallback edge creation for result reporting if the iterator was empty but size changed + Edge edge = graph.create( + contexts != null && !contexts.isEmpty() ? contexts.getFirst() : graph.getDefaultGraphNode(), + subject, predicate, object + ); + return MutationResult.success(edge, "Edge inserted (verified by size change)"); + } + } else { + logger.warn("Edge insertion did not change graph size (possible duplicate)"); + Edge edge = graph.create( + contexts != null && !contexts.isEmpty() ? contexts.getFirst() : graph.getDefaultGraphNode(), + subject, predicate, object + ); + return MutationResult.success(edge, "Edge may already exist"); + } + + } catch (Exception e) { + logger.error("Failed to insert edge: ({}, {}, {})", subject, predicate, object, e); + throw new DataManagerException( + ErrorCode.MUTATION_FAILED, + "Failed to insert edge: " + e.getMessage(), + e + ); + } + } + + /** + * Deletes a specific edge object from the graph. + * + * @param edge the edge to delete. + * @return a {@link MutationResult} indicating if the edge was found and deleted. + * @throws DataManagerException if the deletion fails. + */ + @Override + public MutationResult deleteEdge(Edge edge) throws DataManagerException { + if (edge == null) { + throw new IllegalArgumentException("Edge cannot be null"); + } + + try { + logger.debug("Deleting edge: {}", edge); + + // Corese graph returns an Iterable of deleted edges + Iterable deleted = graph.deleteEdgeWithTargetNode(edge); + + List deletedList = new ArrayList<>(); + if (deleted != null) { + for (Edge e : deleted) { + if (e != null) { + deletedList.add(e); + } + } + } + + if (!deletedList.isEmpty()) { + return MutationResult.success(deletedList.getFirst(), "Edge deleted"); + } else { + logger.warn("Edge deletion returned empty (edge may not exist)"); + return MutationResult.failure("Edge not found or deletion failed"); + } + + } catch (Exception e) { + logger.error("Failed to delete edge: {}", edge, e); + throw new DataManagerException( + ErrorCode.MUTATION_FAILED, + "Failed to delete edge: " + e.getMessage(), + e + ); + } + } + + /** + * Deletes edges matching the specified subject, predicate, and object in the given contexts. + * Supports wildcards (null values). + * + * @param subject the subject node (or null for wildcard). + * @param predicate the predicate node (or null for wildcard). + * @param object the object node (or null for wildcard). + * @param contexts the list of contexts to search in. + * @return a {@link MutationResult} summarizing all deleted edges. + * @throws DataManagerException if the deletion process fails. + */ + @Override + public MutationResult deleteEdges(Node subject, Node predicate, Node object, List contexts) + throws DataManagerException { + + try { + logger.debug("Deleting edges: ({}, {}, {}) in contexts: {}", + subject, predicate, object, contexts); + + // Iterate over matching edges based on pattern + Iterable matchingEdges = graph.iterate(subject, predicate, object, contexts); + + List deletedList = new ArrayList<>(); + for (Edge edge : matchingEdges) { + if (edge != null) { + // Delete the specific edge instance + List deleted = graph.delete(edge); + if (deleted != null) { + for (Edge e : deleted) { + if (e != null) { + deletedList.add(e); + } + } + } + } + } + + logger.debug("Deleted {} edge(s)", deletedList.size()); + + MutationResult.BulkBuilder builder = MutationResult.bulkBuilder() + .totalAttempted(deletedList.size()) + .addAffectedEdges(deletedList) + .message("Deleted " + deletedList.size() + " edge(s)"); + + for (int i = 0; i < deletedList.size(); i++) { + builder.incrementSuccess(); + } + + return builder.build(); + + } catch (Exception e) { + logger.error("Failed to delete edges: ({}, {}, {})", subject, predicate, object, e); + throw new DataManagerException( + ErrorCode.MUTATION_FAILED, + "Failed to delete edges: " + e.getMessage(), + e + ); + } + } + + /** + * Generates a unique blank node identifier within the graph context. + * + * @return a new blank node ID string. + * @throws DataManagerException if the ID generation fails. + */ + @Override + public String generateBlankNode() throws DataManagerException { + try { + String blankId = graph.newBlankID(); + logger.debug("Generated blank node: {}", blankId); + return blankId; + } catch (Exception e) { + logger.error("Failed to generate blank node", e); + throw new DataManagerException( + ErrorCode.MUTATION_FAILED, + "Failed to generate blank node: " + e.getMessage(), + e + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/impl/operations/QueryOperationsImpl.java b/src/main/java/fr/inria/corese/core/storage/impl/operations/QueryOperationsImpl.java new file mode 100644 index 000000000..740b96365 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/impl/operations/QueryOperationsImpl.java @@ -0,0 +1,202 @@ +package fr.inria.corese.core.storage.impl.operations; + +import fr.inria.corese.core.Graph; +import fr.inria.corese.core.kgram.api.core.Edge; +import fr.inria.corese.core.kgram.api.core.Node; +import fr.inria.corese.core.storage.api.datamanager.support.exception.ErrorCode; +import fr.inria.corese.core.storage.api.datamanager.support.model.EdgePattern; +import fr.inria.corese.core.storage.api.datamanager.operations.QueryOperations; +import fr.inria.corese.core.storage.api.datamanager.support.exception.DataManagerException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * Implementation of query operations for CoreseGraphDataManager. + * Converts Graph's Iterable-based API to modern Stream-based API. + */ +public class QueryOperationsImpl implements QueryOperations { + + private static final Logger logger = LoggerFactory.getLogger(QueryOperationsImpl.class); + + /** The underlying Corese graph instance to be queried. */ + private final Graph graph; + + /** + * Constructs query operations for a specific graph. + * + * @param graph the Corese Graph instance to query; must not be null. + * @throws IllegalArgumentException if the provided graph is null. + */ + public QueryOperationsImpl(Graph graph) { + if (graph == null) { + throw new IllegalArgumentException("Graph cannot be null"); + } + this.graph = graph; + } + + /** + * Executes a query based on the provided edge pattern and returns a stream of results. + * + * @param pattern the pattern defining filters for subject, predicate, object, and contexts. + * @return a {@link Stream} of {@link Edge} objects matching the pattern. + * @throws DataManagerException if the query fails at the graph level. + * @throws IllegalArgumentException if the pattern is null. + */ + @Override + public Stream query(EdgePattern pattern) throws DataManagerException { + if (pattern == null) { + throw new IllegalArgumentException("Pattern cannot be null"); + } + + try { + logger.debug("Querying with pattern: {}", pattern); + + // Extract pattern components for the graph iterator + Node subject = pattern.getSubject(); + Node predicate = pattern.getPredicate(); + Node object = pattern.getObject(); + List contexts = pattern.getContexts(); + + // Obtain the iterator from the Corese Graph + Iterable iterable = graph.iterate(subject, predicate, object, contexts); + + // Convert the Iterable to a Stream for modern API usage + Stream stream = StreamSupport.stream(iterable.spliterator(), false); + + logger.debug("Query executed successfully"); + return stream; + + } catch (Exception e) { + logger.error("Failed to execute query with pattern: {}", pattern, e); + throw new DataManagerException( + ErrorCode.QUERY_FAILED, + "Failed to execute query: " + e.getMessage(), + e + ); + } + } + + /** + * Counts the number of edges matching the provided pattern. + * This method includes optimizations for common cases, such as counting all edges + * or counting edges with a specific predicate, to avoid full iteration when possible. + * + * @param pattern the pattern defining the edges to count. + * @return the number of matching edges. + * @throws DataManagerException if the counting operation fails. + */ + @Override + public long count(EdgePattern pattern) throws DataManagerException { + if (pattern == null) { + throw new IllegalArgumentException("Pattern cannot be null"); + } + + try { + logger.debug("Counting with pattern: {}", pattern); + + // Optimize: if only predicate is specified, use Graph.size(predicate) + if (!pattern.hasPredicate() && pattern.hasSubject() && + pattern.hasObject() && pattern.hasContexts()) { + + int count = graph.size(pattern.getPredicate()); + logger.debug("Count (optimized by predicate): {}", count); + return count; + } + + // Optimization: if the pattern matches everything, use the total graph size + if (pattern.matchesAll()) { + int count = graph.size(); + logger.debug("Count (total graph size): {}", count); + return count; + } + + // General case: perform a query and count the stream elements + try (Stream stream = query(pattern)) { + long count = stream.count(); + logger.debug("Count (general iteration): {}", count); + return count; + } + + } catch (DataManagerException e) { + throw e; + } catch (Exception e) { + logger.error("Failed to count with pattern: {}", pattern, e); + throw new DataManagerException( + ErrorCode.QUERY_FAILED, + "Failed to count edges: " + e.getMessage(), + e + ); + } + } + + /** + * Checks if at least one edge matches the provided pattern. + * + * @param pattern the pattern defining the search criteria. + * @return {@code true} if at least one matching edge exists, {@code false} otherwise. + * @throws DataManagerException if the existence check fails. + */ + @Override + public boolean exists(EdgePattern pattern) throws DataManagerException { + if (pattern == null) { + throw new IllegalArgumentException("Pattern cannot be null"); + } + + try { + logger.debug("Checking existence with pattern: {}", pattern); + + // Use findFirst() to terminate iteration immediately upon finding a match + try (Stream stream = query(pattern)) { + boolean exists = stream.findFirst().isPresent(); + logger.debug("Exists: {}", exists); + return exists; + } + + } catch (DataManagerException e) { + throw e; + } catch (Exception e) { + logger.error("Failed to check existence with pattern: {}", pattern, e); + throw new DataManagerException( + ErrorCode.QUERY_FAILED, + "Failed to check edge existence: " + e.getMessage(), + e + ); + } + } + + /** + * Attempts to find a specific edge instance within the graph. + * + * @param edge the edge template or reference to look for. + * @return the found {@link Edge} instance from the graph, or the original edge if not found but valid. + * @throws DataManagerException if the lookup fails. + */ + @Override + public Edge find(Edge edge) throws DataManagerException { + if (edge == null) { + throw new IllegalArgumentException("Edge cannot be null"); + } + + try { + logger.debug("Finding edge: {}", edge); + + // Use Graph's find method which supports advanced features like RDF-star + Edge found = graph.find(edge); + + logger.debug("Found result: {}", found); + return found != null ? found : edge; + + } catch (Exception e) { + logger.error("Failed to find edge: {}", edge, e); + throw new DataManagerException( + ErrorCode.QUERY_FAILED, + "Failed to find edge: " + e.getMessage(), + e + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/impl/transaction/TransactionImpl.java b/src/main/java/fr/inria/corese/core/storage/impl/transaction/TransactionImpl.java new file mode 100644 index 000000000..1753bdada --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/impl/transaction/TransactionImpl.java @@ -0,0 +1,215 @@ +package fr.inria.corese.core.storage.impl.transaction; + +import fr.inria.corese.core.storage.api.datamanager.support.exception.DataManagerException; +import fr.inria.corese.core.storage.api.datamanager.support.exception.ErrorCode; +import fr.inria.corese.core.storage.api.datamanager.transaction.IsolationLevel; +import fr.inria.corese.core.storage.api.datamanager.transaction.Transaction; +import fr.inria.corese.core.storage.api.datamanager.transaction.TransactionState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Instant; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Implementation of a transaction handle. + */ +public class TransactionImpl implements Transaction { + + private static final Logger logger = LoggerFactory.getLogger(TransactionImpl.class); + + private final String id; + private final IsolationLevel isolationLevel; + private final Instant startTime; + private final TransactionCallback callback; + + /** + * Thread-safe reference to the current state of the transaction. + */ + private final AtomicReference state; + + /** + * Callback interface for transaction operations. + * Allows the TransactionManager or DataManager to be notified of lifecycle changes. + */ + public interface TransactionCallback { + /** + * Invoked when the transaction is requested to commit. + * + * @param transaction the transaction being committed. + * @throws DataManagerException if the commit operation fails. + */ + void onCommit(TransactionImpl transaction) throws DataManagerException; + + /** + * Invoked when the transaction is requested to rollback. + * + * @param transaction the transaction being rolled back. + * @throws DataManagerException if the rollback operation fails. + */ + void onRollback(TransactionImpl transaction) throws DataManagerException; + } + + /** + * Constructs a new transaction handle. + * + * @param isolationLevel the requested isolation level for this transaction. + * @param callback the callback to handle persistence of changes. + */ + public TransactionImpl(IsolationLevel isolationLevel, TransactionCallback callback) { + this.id = UUID.randomUUID().toString(); + this.isolationLevel = isolationLevel; + this.startTime = Instant.now(); + this.callback = callback; + this.state = new AtomicReference<>(TransactionState.ACTIVE); + + logger.debug("Transaction {} started with isolation level {}", id, isolationLevel); + } + + @Override + public String getId() { + return id; + } + + @Override + public IsolationLevel getIsolationLevel() { + return isolationLevel; + } + + /** + * Commits the changes made during this transaction. + * + * @throws IllegalStateException if the transaction is not in an ACTIVE state. + * @throws DataManagerException if the commit fails at the storage level. + */ + @Override + public void commit() throws DataManagerException { + TransactionState currentState = state.get(); + + if (!currentState.isActive()) { + throw new IllegalStateException( + "Cannot commit transaction in state: " + currentState + ); + } + + logger.debug("Committing transaction {}", id); + + try { + // Execute the commit logic via the registered callback + callback.onCommit(this); + + // Atomically update state to COMMITTED + if (!state.compareAndSet(TransactionState.ACTIVE, TransactionState.COMMITTED)) { + throw new IllegalStateException( + "Transaction state changed during commit process" + ); + } + + logger.info("Transaction {} committed successfully", id); + + } catch (DataManagerException e) { + state.set(TransactionState.FAILED); + logger.error("Failed to commit transaction {}", id, e); + throw e; + + } catch (Exception e) { + state.set(TransactionState.FAILED); + logger.error("Unexpected error during commit of transaction {}", id, e); + throw new DataManagerException( + ErrorCode.TRANSACTION_ERROR, + "Failed to commit transaction: " + e.getMessage(), + e + ); + } + } + + /** + * Reverts the changes made during this transaction. + * + * @throws IllegalStateException if the transaction is not in an ACTIVE state. + * @throws DataManagerException if the rollback operation fails. + */ + @Override + public void rollback() throws DataManagerException { + TransactionState currentState = state.get(); + + if (!currentState.isActive()) { + throw new IllegalStateException( + "Cannot rollback transaction in state: " + currentState + ); + } + + logger.debug("Rolling back transaction {}", id); + + try { + // Execute the rollback logic via the registered callback + callback.onRollback(this); + + // Atomically update state to ROLLED_BACK + if (!state.compareAndSet(TransactionState.ACTIVE, TransactionState.ROLLED_BACK)) { + throw new IllegalStateException( + "Transaction state changed during rollback process" + ); + } + + logger.info("Transaction {} rolled back successfully", id); + + } catch (DataManagerException e) { + state.set(TransactionState.FAILED); + logger.error("Failed to rollback transaction {}", id, e); + throw e; + + } catch (Exception e) { + state.set(TransactionState.FAILED); + logger.error("Unexpected error during rollback of transaction {}", id, e); + throw new DataManagerException( + ErrorCode.TRANSACTION_ERROR, + "Failed to rollback transaction: " + e.getMessage(), + e + ); + } + } + + @Override + public boolean isActive() { + return state.get().isActive(); + } + + @Override + public TransactionState getState() { + return state.get(); + } + + /** + * Closes the transaction. If the transaction is still active, an automatic rollback is performed. + * + * @throws DataManagerException if an automatic rollback fails. + */ + @Override + public void close() throws DataManagerException { + TransactionState currentState = state.get(); + + if (currentState.isActive()) { + logger.warn("Transaction {} closed while still active; performing automatic rollback", id); + try { + rollback(); + } catch (DataManagerException e) { + logger.error("Failed to perform auto-rollback for transaction {}", id, e); + throw e; + } + } + + logger.debug("Transaction {} closed in state {}", id, state.get()); + } + + @Override + public String toString() { + return "Transaction{" + + "id='" + id + '\'' + + ", isolationLevel=" + isolationLevel + + ", state=" + state.get() + + ", startTime=" + startTime + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/storage/impl/transaction/TransactionManagerImpl.java b/src/main/java/fr/inria/corese/core/storage/impl/transaction/TransactionManagerImpl.java new file mode 100644 index 000000000..1ff599a1c --- /dev/null +++ b/src/main/java/fr/inria/corese/core/storage/impl/transaction/TransactionManagerImpl.java @@ -0,0 +1,192 @@ +package fr.inria.corese.core.storage.impl.transaction; + +import fr.inria.corese.core.Graph; +import fr.inria.corese.core.storage.api.datamanager.support.exception.DataManagerException; +import fr.inria.corese.core.storage.api.datamanager.support.exception.ErrorCode; +import fr.inria.corese.core.storage.api.datamanager.transaction.IsolationLevel; +import fr.inria.corese.core.storage.api.datamanager.transaction.Transaction; +import fr.inria.corese.core.storage.api.datamanager.transaction.TransactionManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.EnumSet; +import java.util.Optional; +import java.util.Set; + +/** + * Transaction manager implementation for CoreseGraphDataManager. + */ +public class TransactionManagerImpl implements TransactionManager { + + private static final Logger logger = LoggerFactory.getLogger(TransactionManagerImpl.class); + + private final Graph graph; + private final boolean transactionSupport; + private final IsolationLevel defaultIsolationLevel; + + /** + * Thread-local storage to track the current active transaction handle for the calling thread. + */ + private final ThreadLocal currentTransaction = new ThreadLocal<>(); + + /** + * Constructs a new TransactionManager. + * + * @param graph the underlying Corese Graph. + * @param transactionSupport flag indicating if transaction features are enabled. + * @param defaultIsolationLevel the isolation level to use when none is specified. + * @throws IllegalArgumentException if graph or defaultIsolationLevel is null. + */ + public TransactionManagerImpl( + Graph graph, + boolean transactionSupport, + IsolationLevel defaultIsolationLevel) { + + if (graph == null) { + throw new IllegalArgumentException("Graph cannot be null"); + } + if (defaultIsolationLevel == null) { + throw new IllegalArgumentException("Default isolation level cannot be null"); + } + + this.graph = graph; + this.transactionSupport = transactionSupport; + this.defaultIsolationLevel = defaultIsolationLevel; + + logger.info("TransactionManager initialized: support={}, defaultLevel={}", + transactionSupport, defaultIsolationLevel); + } + + @Override + public boolean supportsTransactions() { + return transactionSupport; + } + + @Override + public Transaction beginTransaction() throws DataManagerException { + return beginTransaction(defaultIsolationLevel); + } + + @Override + public Transaction beginTransaction(IsolationLevel isolationLevel) throws DataManagerException { + if (!supportsTransactions()) { + throw new UnsupportedOperationException("Transactions are not supported by this manager configuration."); + } + + if (isolationLevel == null) { + throw new IllegalArgumentException("Isolation level cannot be null"); + } + + if (!getSupportedIsolationLevels().contains(isolationLevel)) { + throw new IllegalArgumentException( + "Isolation level " + isolationLevel + " is not supported by the current storage backend." + ); + } + + // Check for existing active transaction to prevent unsupported nesting + TransactionImpl existing = currentTransaction.get(); + if (existing != null && existing.isActive()) { + logger.warn("Attempted to start a new transaction while transaction {} is still active.", + existing.getId()); + throw new DataManagerException( + ErrorCode.TRANSACTION_ERROR, + "Nested transactions are not supported. Current active transaction: " + existing.getId() + ); + } + + TransactionImpl.TransactionCallback callback = new TransactionImpl.TransactionCallback() { + @Override + public void onCommit(TransactionImpl transaction) throws DataManagerException { + performCommit(transaction); + } + + @Override + public void onRollback(TransactionImpl transaction) throws DataManagerException { + performRollback(transaction); + } + }; + + // Create the new transaction handle and register it to the current thread + TransactionImpl transaction = new TransactionImpl(isolationLevel, callback); + currentTransaction.set(transaction); + + logger.info("Started transaction {} with isolation level {}", + transaction.getId(), isolationLevel); + + return transaction; + } + + @Override + public Optional getCurrentTransaction() { + return Optional.ofNullable(currentTransaction.get()); + } + + @Override + public IsolationLevel getDefaultIsolationLevel() { + return defaultIsolationLevel; + } + + @Override + public Set getSupportedIsolationLevels() { + // Corese currently supports standard levels, but this may be restricted by specific backends + return EnumSet.allOf(IsolationLevel.class); + } + + /** + * Executes the internal commit logic for a transaction. + * This method is triggered by the {@link TransactionImpl#commit()} method via the callback. + * + * @param transaction the transaction to commit. + * @throws DataManagerException if the graph commit fails. + */ + private void performCommit(TransactionImpl transaction) throws DataManagerException { + try { + logger.debug("Performing commit for transaction {}", transaction.getId()); + + graph.init(); + + // Clear the thread-local reference as the transaction lifecycle is ending + currentTransaction.remove(); + + logger.debug("Commit completed for transaction {}", transaction.getId()); + + } catch (Exception e) { + logger.error("Failed to commit transaction {}", transaction.getId(), e); + throw new DataManagerException( + ErrorCode.TRANSACTION_ERROR, + "Failed to commit transaction changes to the graph: " + e.getMessage(), + e + ); + } + } + + /** + * Executes the internal rollback logic for a transaction. + * This method is triggered by the {@link TransactionImpl#rollback()} method via the callback. + * + * @param transaction the transaction to roll back. + * @throws DataManagerException if the rollback operation fails. + */ + private void performRollback(TransactionImpl transaction) throws DataManagerException { + try { + logger.debug("Performing rollback for transaction {}", transaction.getId()); + + // Placeholder for rolling back volatile or buffered changes in the graph + graph.init(); + + // Clear the thread-local reference + currentTransaction.remove(); + + logger.debug("Rollback completed for transaction {}", transaction.getId()); + + } catch (Exception e) { + logger.error("Failed to rollback transaction {}", transaction.getId(), e); + throw new DataManagerException( + ErrorCode.TRANSACTION_ERROR, + "Failed to rollback transaction changes: " + e.getMessage(), + e + ); + } + } + +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/transform/Transformer.java b/src/main/java/fr/inria/corese/core/transform/Transformer.java index 0b2cf3770..0b090b5a5 100644 --- a/src/main/java/fr/inria/corese/core/transform/Transformer.java +++ b/src/main/java/fr/inria/corese/core/transform/Transformer.java @@ -25,7 +25,7 @@ import fr.inria.corese.core.sparql.triple.parser.*; import fr.inria.corese.core.sparql.triple.parser.Access.Feature; import fr.inria.corese.core.sparql.triple.parser.Access.Level; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; import fr.inria.corese.core.visitor.solver.QuerySolverVisitorTransformer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/fr/inria/corese/core/workflow/Data.java b/src/main/java/fr/inria/corese/core/workflow/Data.java index 914c6c3a8..9f908dc25 100644 --- a/src/main/java/fr/inria/corese/core/workflow/Data.java +++ b/src/main/java/fr/inria/corese/core/workflow/Data.java @@ -10,7 +10,7 @@ import fr.inria.corese.core.sparql.triple.function.term.Binding; import fr.inria.corese.core.sparql.triple.parser.Context; import fr.inria.corese.core.sparql.triple.parser.Dataset; -import fr.inria.corese.core.storage.api.dataManager.DataManager; +import fr.inria.corese.core.storage.api.datamanager.DataManager; import fr.inria.corese.core.transform.TemplateVisitor; import fr.inria.corese.core.visitor.solver.QuerySolverVisitorTransformer; diff --git a/src/test/java/fr/inria/corese/core/query/QueryEngineTest.java b/src/test/java/fr/inria/corese/core/query/QueryEngineTest.java index eab91369f..8f198c8e0 100644 --- a/src/test/java/fr/inria/corese/core/query/QueryEngineTest.java +++ b/src/test/java/fr/inria/corese/core/query/QueryEngineTest.java @@ -181,6 +181,7 @@ void testClearCompilationCache() throws EngineException { } @Test + @SuppressWarnings("unchecked") void testCleanMethod() { ArrayList internalList = null; try { diff --git a/src/test/java/fr/inria/corese/core/storage/CoreseGraphDataManagerIntegrationTest.java b/src/test/java/fr/inria/corese/core/storage/CoreseGraphDataManagerIntegrationTest.java new file mode 100644 index 000000000..fc2e83ec6 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/storage/CoreseGraphDataManagerIntegrationTest.java @@ -0,0 +1,122 @@ +package fr.inria.corese.core.storage; + +import fr.inria.corese.core.kgram.api.core.Edge; +import fr.inria.corese.core.kgram.api.core.Node; +import fr.inria.corese.core.storage.api.datamanager.support.exception.DataManagerException; +import fr.inria.corese.core.storage.api.datamanager.support.model.EdgePattern; +import fr.inria.corese.core.storage.api.datamanager.support.model.GraphStatistics; +import fr.inria.corese.core.storage.api.datamanager.support.model.MutationResult; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Set; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Integration tests for CoresGraphDataManager. + * + */ +@DisplayName("CoreseGraphDataManager Integration Tests") +class CoreseGraphDataManagerIntegrationTest { + + private CoreseGraphDataManager dataManager; + + @BeforeEach + void setUp() { + dataManager = new CoreseGraphDataManagerBuilder().build(); + + } + + @Test + @DisplayName("Bulk insert of 100 edges should succeed") + void testBulkOperations() throws DataManagerException { + java.util.List edges = new java.util.ArrayList<>(); + + for (int i = 0; i < 100; i++) { + Node s = dataManager.getGraph().addResource("http://example.org/s" + i); + Node p = dataManager.getGraph().addProperty("http://example.org/p"); + Node o = dataManager.getGraph().addResource("http://example.org/o" + i); + edges.add(dataManager.getGraph().create(dataManager.getGraph().getDefaultGraphNode(), s, p, o)); + } + + MutationResult result = dataManager.getBulkOperations().insertBatch(edges); + + assertTrue(result.isCompleteSuccess()); + assertEquals(100, result.getSuccessCount()); + assertEquals(100, dataManager.getGraph().size()); + } + + @Test + @DisplayName("Metadata operations should return correct statistics") + void testMetadataOperations() throws DataManagerException { + // Add test data + Node s = dataManager.getGraph().addResource("http://example.org/subject"); + Node p1 = dataManager.getGraph().addProperty("http://example.org/p1"); + Node p2 = dataManager.getGraph().addProperty("http://example.org/p2"); + Node o = dataManager.getGraph().addResource("http://example.org/object"); + + Edge edge1 = dataManager.getGraph().create(dataManager.getGraph().getDefaultGraphNode(), s, p1, o); + Edge edge2 = dataManager.getGraph().create(dataManager.getGraph().getDefaultGraphNode(), s, p2, o); + + dataManager.getMutationOperations().insertEdge(edge1); + dataManager.getMutationOperations().insertEdge(edge2); + + // Get metadata + Set predicates = dataManager.getMetadataOperations().getAllPredicates(); + assertTrue(predicates.size() >= 2); + + GraphStatistics stats = dataManager.getMetadataOperations().getStatistics(); + assertEquals(2, stats.getEdgeCount()); + assertTrue(stats.getNodeCount() >= 2); + assertTrue(stats.getPredicateCount() >= 2); + } + + @Test + @DisplayName("Query with predicate pattern should find matching edges") + void testQueryWithPattern() throws DataManagerException { + // Add test data + Node s = dataManager.getGraph().addResource("http://example.org/Alice"); + Node p = dataManager.getGraph().addProperty("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"); + Node o = dataManager.getGraph().addResource("http://example.org/Person"); + Edge edge = dataManager.getGraph().create(dataManager.getGraph().getDefaultGraphNode(), s, p, o); + + dataManager.getMutationOperations().insertEdge(edge); + + // Query with pattern + EdgePattern pattern = EdgePattern.builder() + .predicate(p) + .build(); + + try (Stream edges = dataManager.getQueryOperations().query(pattern)) { + long count = edges.count(); + assertEquals(1, count); + } + } + + @Test + @DisplayName("Clear all data should empty the graph") + void testClearAllData() throws DataManagerException { + // Add data + for (int i = 0; i < 10; i++) { + Node s = dataManager.getGraph().addResource("http://example.org/s" + i); + Node p = dataManager.getGraph().addProperty("http://example.org/p"); + Node o = dataManager.getGraph().addResource("http://example.org/o" + i); + dataManager.getMutationOperations().insertEdge(s, p, o, null); + } + + assertTrue(dataManager.getGraph().size() > 0); + + // Clear all + MutationResult result = dataManager.getBulkOperations().clearAll(); + // For bulk operations, check if there were deletions + assertTrue(result.getTotalAttempted() > 0 || dataManager.getGraph().size() == 0); + + assertEquals(0, dataManager.getGraph().size()); + } + + +} \ No newline at end of file diff --git a/src/test/java/fr/inria/corese/core/storage/impl/lifecycle/LifecycleManagerImplTest.java b/src/test/java/fr/inria/corese/core/storage/impl/lifecycle/LifecycleManagerImplTest.java new file mode 100644 index 000000000..8df049bf5 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/storage/impl/lifecycle/LifecycleManagerImplTest.java @@ -0,0 +1,192 @@ +package fr.inria.corese.core.storage.impl.lifecycle; + +import fr.inria.corese.core.Graph; +import fr.inria.corese.core.storage.api.datamanager.lifecycle.LifecycleState; +import fr.inria.corese.core.storage.api.datamanager.support.config.DataManagerConfig; +import fr.inria.corese.core.storage.api.datamanager.support.exception.DataManagerException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for LifecycleManagerImpl. + */ +@DisplayName("LifecycleManagerImpl Tests") +class LifecycleManagerImplTest { + + + private LifecycleManagerImpl lifecycleManager; + + @BeforeEach + void setUp() { + Graph graph = new Graph(); + lifecycleManager = new LifecycleManagerImpl(graph); + } + + /** + * Helper method to create a default test configuration. + */ + private DataManagerConfig createDefaultConfig() { + return DataManagerConfig.builder() + .build(); + } + + /** + * Helper method to create a test configuration with debug enabled. + */ + private DataManagerConfig createDebugConfig() { + return DataManagerConfig.builder() + .debug(true) + .build(); + } + + @Test + @DisplayName("Initial state should be NOT_INITIALIZED") + void testInitialStateIsNotInitialized() { + assertEquals(LifecycleState.NOT_INITIALIZED, lifecycleManager.getState()); + assertFalse(lifecycleManager.isInitialized()); + } + + @Test + @DisplayName("Initialize with valid config should succeed") + void testInitializeSuccess() throws DataManagerException { + DataManagerConfig config = createDebugConfig(); + lifecycleManager.initialize(config); + + assertEquals(LifecycleState.RUNNING, lifecycleManager.getState()); + assertTrue(lifecycleManager.isInitialized()); + } + + @Test + @DisplayName("Initialize with null config should throw IllegalArgumentException") + void testInitializeWithNullConfigThrows() { + assertThrows(IllegalArgumentException.class, () -> lifecycleManager.initialize(null)); + } + + + @Test + @DisplayName("Shutdown after initialization should succeed") + void testShutdownSuccess() throws DataManagerException { + DataManagerConfig config = createDefaultConfig(); + lifecycleManager.initialize(config); + + lifecycleManager.shutdown(); + + assertEquals(LifecycleState.SHUTDOWN, lifecycleManager.getState()); + assertFalse(lifecycleManager.isInitialized()); + } + + @Test + @DisplayName("Restart should reinitialize successfully") + void testRestartSuccess() throws DataManagerException { + DataManagerConfig config1 = DataManagerConfig.builder() + .debug(false) + .build(); + lifecycleManager.initialize(config1); + + DataManagerConfig config2 = createDebugConfig(); + lifecycleManager.restart(config2); + + assertEquals(LifecycleState.RUNNING, lifecycleManager.getState()); + assertTrue(lifecycleManager.isInitialized()); + } + + @Test + @DisplayName("checkUsable when RUNNING should not throw") + void testCheckUsableWhenRunning() throws DataManagerException { + DataManagerConfig config = createDefaultConfig(); + lifecycleManager.initialize(config); + + // Should not throw + assertDoesNotThrow(() -> lifecycleManager.checkUsable()); + } + + @Test + @DisplayName("checkUsable when NOT_INITIALIZED should throw IllegalStateException") + void testCheckUsableWhenNotInitialized() { + assertThrows(IllegalStateException.class, () -> lifecycleManager.checkUsable()); + } + + @Test + @DisplayName("checkUsable after shutdown should throw IllegalStateException") + void testCheckUsableAfterShutdown() throws DataManagerException { + DataManagerConfig config = createDefaultConfig(); + lifecycleManager.initialize(config); + lifecycleManager.shutdown(); + + assertThrows(IllegalStateException.class, () -> lifecycleManager.checkUsable()); + } + + @Test + @DisplayName("Initialize with custom storage path") + void testInitializeWithCustomStoragePath() throws DataManagerException { + String customPath = "http://example.org/custom/storage"; + DataManagerConfig config = DataManagerConfig.builder() + .build(); + + lifecycleManager.initialize(config); + + assertTrue(lifecycleManager.isInitialized()); + assertEquals(LifecycleState.RUNNING, lifecycleManager.getState()); + } + + @Test + @DisplayName("Initialize with transaction support enabled") + void testInitializeWithTransactionSupport() throws DataManagerException { + DataManagerConfig config = DataManagerConfig.builder() + .transactionSupport(true) + .build(); + + lifecycleManager.initialize(config); + + assertTrue(lifecycleManager.isInitialized()); + } + + @Test + @DisplayName("Initialize with custom properties") + void testInitializeWithCustomProperties() throws DataManagerException { + DataManagerConfig config = DataManagerConfig.builder() + .property("custom.key", "custom.value") + .property("another.key", "another.value") + .build(); + + lifecycleManager.initialize(config); + + assertTrue(lifecycleManager.isInitialized()); + assertEquals(LifecycleState.RUNNING, lifecycleManager.getState()); + } + + @Test + @DisplayName("Multiple restarts should work correctly") + void testMultipleRestarts() throws DataManagerException { + DataManagerConfig config1 = DataManagerConfig.builder() + .debug(false) + .build(); + lifecycleManager.initialize(config1); + + for (int i = 0; i < 3; i++) { + DataManagerConfig config = DataManagerConfig.builder() + .debug(i % 2 == 0) + .build(); + lifecycleManager.restart(config); + + assertEquals(LifecycleState.RUNNING, lifecycleManager.getState()); + assertTrue(lifecycleManager.isInitialized()); + } + } + + @Test + @DisplayName("State transitions should be correct") + void testStateTransitions() throws DataManagerException { + assertEquals(LifecycleState.NOT_INITIALIZED, lifecycleManager.getState()); + + DataManagerConfig config = createDefaultConfig(); + lifecycleManager.initialize(config); + assertEquals(LifecycleState.RUNNING, lifecycleManager.getState()); + + lifecycleManager.shutdown(); + assertEquals(LifecycleState.SHUTDOWN, lifecycleManager.getState()); + } +} \ No newline at end of file diff --git a/src/test/java/fr/inria/corese/core/storage/impl/operations/BulkOperationsImplTest.java b/src/test/java/fr/inria/corese/core/storage/impl/operations/BulkOperationsImplTest.java new file mode 100644 index 000000000..80effdaa0 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/storage/impl/operations/BulkOperationsImplTest.java @@ -0,0 +1,226 @@ +package fr.inria.corese.core.storage.impl.operations; + +import fr.inria.corese.core.Graph; +import fr.inria.corese.core.kgram.api.core.Edge; +import fr.inria.corese.core.kgram.api.core.Node; +import fr.inria.corese.core.storage.api.datamanager.support.exception.DataManagerException; +import fr.inria.corese.core.storage.api.datamanager.support.model.MutationResult; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for BulkOperationsImpl. + */ +@DisplayName("BulkOperationsImpl Tests") +class BulkOperationsImplTest { + + private BulkOperationsImpl bulkOperations; + private Graph graph; + + @BeforeEach + void setUp() { + graph = new Graph(); + bulkOperations = new BulkOperationsImpl(graph); + } + + @Test + @DisplayName("Constructor with null graph should throw IllegalArgumentException") + void testConstructorWithNullGraphThrows() { + assertThrows(IllegalArgumentException.class, () -> new BulkOperationsImpl(null)); + } + + @Test + @DisplayName("Insert batch of 5 edges should succeed") + void testInsertBatchSuccess() throws DataManagerException { + List edges = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + Node s = graph.addResource("http://example.org/s" + i); + Node p = graph.addProperty("http://example.org/p"); + Node o = graph.addResource("http://example.org/o" + i); + edges.add(graph.create(graph.getDefaultGraphNode(), s, p, o)); + } + + MutationResult result = bulkOperations.insertBatch(edges); + + assertTrue(result.isBulk()); + assertEquals(5, result.getTotalAttempted()); + assertEquals(5, result.getSuccessCount()); + assertEquals(0, result.getFailureCount()); + assertEquals(5, graph.size()); + } + + @Test + @DisplayName("Insert batch with empty list should throw IllegalArgumentException") + void testInsertBatchWithEmptyListThrows() { + assertThrows(IllegalArgumentException.class, () -> bulkOperations.insertBatch(new ArrayList<>())); + } + + @Test + @DisplayName("Insert batch with null list should throw IllegalArgumentException") + void testInsertBatchWithNullListThrows() { + assertThrows(IllegalArgumentException.class, () -> bulkOperations.insertBatch(null)); + } + + @Test + @DisplayName("Delete batch of 3 edges should succeed") + void testDeleteBatchSuccess() throws DataManagerException { + List edges = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + Node s = graph.addResource("http://example.org/s" + i); + Node p = graph.addProperty("http://example.org/p"); + Node o = graph.addResource("http://example.org/o" + i); + edges.add(graph.addEdge(s, p, o)); + } + + MutationResult result = bulkOperations.deleteBatch(edges); + + assertTrue(result.isBulk()); + assertEquals(3, result.getSuccessCount()); + assertEquals(0, graph.size()); + } + + + @Test + @DisplayName("Clear all should remove all edges from graph") + void testClearAll() throws DataManagerException { + // Add some data + for (int i = 0; i < 10; i++) { + Node s = graph.addResource("http://example.org/s" + i); + Node p = graph.addProperty("http://example.org/p"); + Node o = graph.addResource("http://example.org/o" + i); + graph.addEdge(s, p, o); + } + + MutationResult result = bulkOperations.clearAll(); + + assertTrue(result.isBulk()); + assertEquals(0, graph.size()); + } + + @Test + @DisplayName("Declare context should succeed") + void testDeclareContext() throws DataManagerException { + Node context = graph.addResource("http://example.org/context"); + + MutationResult result = bulkOperations.declareContext(context); + + assertTrue(result.isSuccess()); + } + + @Test + @DisplayName("Declare context with null should throw IllegalArgumentException") + void testDeclareContextWithNullThrows() { + assertThrows(IllegalArgumentException.class, () -> bulkOperations.declareContext(null)); + } + + @Test + @DisplayName("Undeclare context should remove context and its edges") + void testUndeclareContext() throws DataManagerException { + Node context = graph.addResource("http://example.org/context"); + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + + graph.addEdge(s, p, o, context); + + MutationResult result = bulkOperations.undeclareContext(context); + + assertTrue(result.isBulk()); + } + + @Test + @DisplayName("Insert batch with partial failure should track failures") + void testInsertBatchPartialFailure() throws DataManagerException { + List edges = new ArrayList<>(); + + // Valid edge + Node s1 = graph.addResource("http://example.org/s1"); + Node p = graph.addProperty("http://example.org/p"); + Node o1 = graph.addResource("http://example.org/o1"); + edges.add(graph.create(graph.getDefaultGraphNode(), s1, p, o1)); + + graph.addEdge(s1, p, o1); + + MutationResult result = bulkOperations.insertBatch(edges); + + assertEquals(1, result.getTotalAttempted()); + } + + @Test + @DisplayName("Success rate should be correctly calculated") + void testGetSuccessRate() throws DataManagerException { + List edges = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Node s = graph.addResource("http://example.org/s" + i); + Node p = graph.addProperty("http://example.org/p"); + Node o = graph.addResource("http://example.org/o" + i); + edges.add(graph.create(graph.getDefaultGraphNode(), s, p, o)); + } + + MutationResult result = bulkOperations.insertBatch(edges); + + assertEquals(1.0, result.getSuccessRate(), 0.001); + } + + @Test + @DisplayName("Large batch insert (100 edges) should succeed") + void testLargeBatchInsert() throws DataManagerException { + List edges = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + Node s = graph.addResource("http://example.org/s" + i); + Node p = graph.addProperty("http://example.org/p"); + Node o = graph.addResource("http://example.org/o" + i); + edges.add(graph.create(graph.getDefaultGraphNode(), s, p, o)); + } + + MutationResult result = bulkOperations.insertBatch(edges); + + assertTrue(result.isCompleteSuccess()); + assertEquals(100, result.getSuccessCount()); + assertEquals(100, graph.size()); + } + + + @Test + @DisplayName("Clear all on empty graph should succeed with no deletions") + void testClearAllOnEmptyGraph() throws DataManagerException { + MutationResult result = bulkOperations.clearAll(); + + assertTrue(result.isBulk()); + assertEquals(0, graph.size()); + } + + @Test + @DisplayName("Multiple batch operations should all succeed") + void testMultipleBatchOperations() throws DataManagerException { + // First batch + List batch1 = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + Node s = graph.addResource("http://example.org/s" + i); + Node p = graph.addProperty("http://example.org/p1"); + Node o = graph.addResource("http://example.org/o" + i); + batch1.add(graph.create(graph.getDefaultGraphNode(), s, p, o)); + } + bulkOperations.insertBatch(batch1); + + assertEquals(5, graph.size()); + + // Second batch + List batch2 = new ArrayList<>(); + for (int i = 5; i < 10; i++) { + Node s = graph.addResource("http://example.org/s" + i); + Node p = graph.addProperty("http://example.org/p2"); + Node o = graph.addResource("http://example.org/o" + i); + batch2.add(graph.create(graph.getDefaultGraphNode(), s, p, o)); + } + bulkOperations.insertBatch(batch2); + + assertEquals(10, graph.size()); + } +} \ No newline at end of file diff --git a/src/test/java/fr/inria/corese/core/storage/impl/operations/MetadataOperationsImplTest.java b/src/test/java/fr/inria/corese/core/storage/impl/operations/MetadataOperationsImplTest.java new file mode 100644 index 000000000..a300ac771 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/storage/impl/operations/MetadataOperationsImplTest.java @@ -0,0 +1,316 @@ +package fr.inria.corese.core.storage.impl.operations; + +import fr.inria.corese.core.Graph; +import fr.inria.corese.core.kgram.api.core.Node; +import fr.inria.corese.core.storage.api.datamanager.support.exception.DataManagerException; +import fr.inria.corese.core.storage.api.datamanager.support.model.GraphStatistics; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for MetadataOperationsImpl. + */ +@DisplayName("MetadataOperationsImpl Tests") +class MetadataOperationsImplTest { + + private MetadataOperationsImpl metadataOperations; + private Graph graph; + + @BeforeEach + void setUp() { + graph = new Graph(); + metadataOperations = new MetadataOperationsImpl(graph); + } + + @Test + @DisplayName("Constructor with null graph should throw IllegalArgumentException") + void testConstructorWithNullGraphThrows() { + assertThrows(IllegalArgumentException.class, () -> new MetadataOperationsImpl(null)); + } + + @Test + @DisplayName("Get all predicates on empty graph should return empty set") + void testGetAllPredicatesOnEmptyGraph() throws DataManagerException { + Set predicates = metadataOperations.getAllPredicates(); + + assertNotNull(predicates); + assertTrue(predicates.isEmpty()); + } + + + @Test + @DisplayName("Get all predicates should return all unique predicates") + void testGetAllPredicates() throws DataManagerException { + // Add test data with different predicates + Node s = graph.addResource("http://example.org/subject"); + Node p1 = graph.addProperty("http://example.org/predicate1"); + Node p2 = graph.addProperty("http://example.org/predicate2"); + Node o = graph.addResource("http://example.org/object"); + + graph.addEdge(s, p1, o); + graph.addEdge(s, p2, o); + + Set predicates = metadataOperations.getAllPredicates(); + + assertNotNull(predicates); + assertTrue(predicates.size() >= 2); + assertTrue(predicates.contains(p1)); + assertTrue(predicates.contains(p2)); + } + + @Test + @DisplayName("Get predicates for specific context") + void testGetPredicatesForContext() throws DataManagerException { + Node context = graph.addGraph("http://example.org/graph1"); + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + + graph.addEdge(context, s, p, o); + + Set predicates = metadataOperations.getPredicates(context); + + assertNotNull(predicates); + assertFalse(predicates.isEmpty()); + assertTrue(predicates.contains(p)); + } + + @Test + @DisplayName("Get all nodes on empty graph should return empty set") + void testGetAllNodesOnEmptyGraph() throws DataManagerException { + Set nodes = metadataOperations.getAllNodes(); + + assertNotNull(nodes); + assertTrue(nodes.isEmpty()); + } + + @Test + @DisplayName("Get all nodes should return all subjects and objects") + void testGetAllNodes() throws DataManagerException { + Node s1 = graph.addResource("http://example.org/subject1"); + Node s2 = graph.addResource("http://example.org/subject2"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o1 = graph.addResource("http://example.org/object1"); + Node o2 = graph.addResource("http://example.org/object2"); + + graph.addEdge(s1, p, o1); + graph.addEdge(s2, p, o2); + + Set nodes = metadataOperations.getAllNodes(); + + assertNotNull(nodes); + assertTrue(nodes.size() >= 4); + assertTrue(nodes.contains(s1)); + assertTrue(nodes.contains(s2)); + assertTrue(nodes.contains(o1)); + assertTrue(nodes.contains(o2)); + } + + @Test + @DisplayName("Get nodes for specific context") + void testGetNodesForContext() throws DataManagerException { + Node context = graph.addGraph("http://example.org/graph1"); + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + + graph.addEdge(context, s, p, o); + + Set nodes = metadataOperations.getNodes(context); + + assertNotNull(nodes); + assertTrue(nodes.size() >= 2); + assertTrue(nodes.contains(s)); + assertTrue(nodes.contains(o)); + } + + @Test + @DisplayName("Get contexts on empty graph should return empty set") + void testGetContextsOnEmptyGraph() throws DataManagerException { + Set contexts = metadataOperations.getContexts(); + + assertNotNull(contexts); + // May contain default graph + assertTrue(contexts.isEmpty() || contexts.size() == 1); + } + + @Test + @DisplayName("Get contexts should return all named graphs") + void testGetContexts() throws DataManagerException { + Node context1 = graph.addGraph("http://example.org/graph1"); + Node context2 = graph.addGraph("http://example.org/graph2"); + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + + graph.addEdge(context1, s, p, o); + graph.addEdge(context2, s, p, o); + + Set contexts = metadataOperations.getContexts(); + + assertNotNull(contexts); + assertTrue(contexts.size() >= 2); + assertTrue(contexts.contains(context1)); + assertTrue(contexts.contains(context2)); + } + + @Test + @DisplayName("Get statistics on empty graph should return zero counts") + void testGetStatisticsOnEmptyGraph() throws DataManagerException { + GraphStatistics stats = metadataOperations.getStatistics(); + + assertNotNull(stats); + assertEquals(0, stats.getEdgeCount()); + assertEquals(0, stats.getNodeCount()); + assertEquals(0, stats.getPredicateCount()); + assertTrue(stats.isEmpty()); + } + + @Test + @DisplayName("Get statistics should return correct counts") + void testGetStatistics() throws DataManagerException { + // Add test data + Node s1 = graph.addResource("http://example.org/s1"); + Node s2 = graph.addResource("http://example.org/s2"); + Node p1 = graph.addProperty("http://example.org/p1"); + Node p2 = graph.addProperty("http://example.org/p2"); + Node o1 = graph.addResource("http://example.org/o1"); + Node o2 = graph.addResource("http://example.org/o2"); + + graph.addEdge(s1, p1, o1); + graph.addEdge(s1, p2, o2); + graph.addEdge(s2, p1, o1); + + GraphStatistics stats = metadataOperations.getStatistics(); + + assertNotNull(stats); + assertEquals(3, stats.getEdgeCount()); + assertTrue(stats.getNodeCount() >= 4); + assertTrue(stats.getPredicateCount() >= 2); + assertFalse(stats.isEmpty()); + assertTrue(stats.hasData()); + } + + @Test + @DisplayName("Get statistics should calculate density") + void testGetStatisticsWithDensity() throws DataManagerException { + // Add some edges + for (int i = 0; i < 5; i++) { + Node s = graph.addResource("http://example.org/s" + i); + Node p = graph.addProperty("http://example.org/p"); + Node o = graph.addResource("http://example.org/o" + i); + graph.addEdge(s, p, o); + } + + GraphStatistics stats = metadataOperations.getStatistics(); + + assertNotNull(stats); + assertTrue(stats.getEdgeCount() > 0); + assertTrue(stats.getDensity() >= 0.0); + assertTrue(stats.getDensity() <= 1.0); + } + + @Test + @DisplayName("Get statistics should calculate average degree") + void testGetStatisticsWithAverageDegree() throws DataManagerException { + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + + for (int i = 0; i < 3; i++) { + Node o = graph.addResource("http://example.org/o" + i); + graph.addEdge(s, p, o); + } + + GraphStatistics stats = metadataOperations.getStatistics(); + + assertNotNull(stats); + assertTrue(stats.getAverageDegree() > 0.0); + } + + @Test + @DisplayName("Get predicates with null context should return all predicates") + void testGetPredicatesWithNullContext() throws DataManagerException { + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + + graph.addEdge(s, p, o); + + Set predicates = metadataOperations.getPredicates(null); + + assertNotNull(predicates); + assertTrue(predicates.contains(p)); + } + + @Test + @DisplayName("Get nodes with null context should return all nodes") + void testGetNodesWithNullContext() throws DataManagerException { + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + + graph.addEdge(s, p, o); + + Set nodes = metadataOperations.getNodes(null); + + assertNotNull(nodes); + assertTrue(nodes.contains(s)); + assertTrue(nodes.contains(o)); + } + + @Test + @DisplayName("Multiple contexts should be tracked separately") + void testMultipleContexts() throws DataManagerException { + Node ctx1 = graph.addGraph("http://example.org/graph1"); + Node ctx2 = graph.addGraph("http://example.org/graph2"); + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + + graph.addEdge(ctx1, s, p, o); + graph.addEdge(ctx2, s, p, o); + + Set contexts = metadataOperations.getContexts(); + + assertNotNull(contexts); + assertTrue(contexts.contains(ctx1)); + assertTrue(contexts.contains(ctx2)); + } + + @Test + @DisplayName("Statistics should track context count") + void testStatisticsContextCount() throws DataManagerException { + Node ctx1 = graph.addGraph("http://example.org/graph1"); + Node ctx2 = graph.addGraph("http://example.org/graph2"); + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + + graph.addEdge(ctx1, s, p, o); + graph.addEdge(ctx2, s, p, o); + + GraphStatistics stats = metadataOperations.getStatistics(); + + assertNotNull(stats); + assertTrue(stats.getContextCount() >= 2); + } + + @Test + @DisplayName("Returned sets should be unmodifiable") + void testReturnedSetsAreUnmodifiable() throws DataManagerException { + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + + graph.addEdge(s, p, o); + + Set predicates = metadataOperations.getAllPredicates(); + + assertThrows(UnsupportedOperationException.class, () -> predicates.add(graph.addProperty("http://example.org/another"))); + } +} \ No newline at end of file diff --git a/src/test/java/fr/inria/corese/core/storage/impl/operations/MutationOperationsImplTest.java b/src/test/java/fr/inria/corese/core/storage/impl/operations/MutationOperationsImplTest.java new file mode 100644 index 000000000..ca70d236d --- /dev/null +++ b/src/test/java/fr/inria/corese/core/storage/impl/operations/MutationOperationsImplTest.java @@ -0,0 +1,288 @@ +package fr.inria.corese.core.storage.impl.operations; + +import fr.inria.corese.core.Graph; +import fr.inria.corese.core.kgram.api.core.Edge; +import fr.inria.corese.core.kgram.api.core.Node; +import fr.inria.corese.core.storage.api.datamanager.support.exception.DataManagerException; +import fr.inria.corese.core.storage.api.datamanager.support.model.MutationResult; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for MutationOperationsImpl. + * + */ +@DisplayName("MutationOperationsImpl Tests") +class MutationOperationsImplTest { + + private MutationOperationsImpl mutationOperations; + private Graph graph; + + @BeforeEach + void setUp() { + graph = new Graph(); + mutationOperations = new MutationOperationsImpl(graph); + } + + @Test + @DisplayName("Constructor with null graph should throw IllegalArgumentException") + void testConstructorWithNullGraphThrows() { + assertThrows(IllegalArgumentException.class, () -> new MutationOperationsImpl(null)); + } + + @Test + @DisplayName("Insert edge should succeed and return success result") + void testInsertEdgeSuccess() throws DataManagerException { + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + Edge edge = graph.create(graph.getDefaultGraphNode(), s, p, o); + + MutationResult result = mutationOperations.insertEdge(edge); + + assertTrue(result.isSuccess()); + assertNotNull(result.getAffectedEdge()); + assertEquals(1, graph.size()); + } + + @Test + @DisplayName("Insert edge with null should throw IllegalArgumentException") + void testInsertEdgeWithNullThrows() { + assertThrows(IllegalArgumentException.class, () -> mutationOperations.insertEdge(null)); + } + + @Test + @DisplayName("Insert edge with pattern (s, p, o) should succeed") + void testInsertEdgeWithPattern() throws DataManagerException { + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + + MutationResult result = mutationOperations.insertEdge(s, p, o, null); + + assertTrue(result.isSuccess()); + assertEquals(1, graph.size()); + } + + @Test + @DisplayName("Insert edge with null subject should throw IllegalArgumentException") + void testInsertEdgeWithNullSubjectThrows() { + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + + assertThrows(IllegalArgumentException.class, () -> mutationOperations.insertEdge(null, p, o, null)); + } + + @Test + @DisplayName("Insert edge with null predicate should throw IllegalArgumentException") + void testInsertEdgeWithNullPredicateThrows() { + Node s = graph.addResource("http://example.org/subject"); + Node o = graph.addResource("http://example.org/object"); + + assertThrows(IllegalArgumentException.class, () -> mutationOperations.insertEdge(s, null, o, null)); + } + + @Test + @DisplayName("Insert edge with null object should throw IllegalArgumentException") + void testInsertEdgeWithNullObjectThrows() { + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + + assertThrows(IllegalArgumentException.class, () -> mutationOperations.insertEdge(s, p, null, null)); + } + + @Test + @DisplayName("Delete edge should succeed and return success result") + void testDeleteEdgeSuccess() throws DataManagerException { + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + + // Setup: insert edge using graph directly + graph.addEdge(s, p, o); + + // Create edge reference for deletion + Edge edge = graph.create(graph.getDefaultGraphNode(), s, p, o); + + MutationResult result = mutationOperations.deleteEdge(edge); + + assertTrue(result.isSuccess()); + assertEquals(0, graph.size()); + } + + @Test + @DisplayName("Delete edge with null should throw IllegalArgumentException") + void testDeleteEdgeWithNullThrows() { + assertThrows(IllegalArgumentException.class, () -> mutationOperations.deleteEdge(null)); + } + + @Test + @DisplayName("Delete non-existent edge should return failure result") + void testDeleteNonExistentEdge() throws DataManagerException { + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + Edge edge = graph.create(graph.getDefaultGraphNode(), s, p, o); + + MutationResult result = mutationOperations.deleteEdge(edge); + + assertTrue(result.isFailure()); + } + + @Test + @DisplayName("Generate blank node should return unique identifier") + void testGenerateBlankNode() throws DataManagerException { + String blankId1 = mutationOperations.generateBlankNode(); + String blankId2 = mutationOperations.generateBlankNode(); + + assertNotNull(blankId1); + assertNotNull(blankId2); + assertTrue(blankId1.startsWith("_:")); + assertTrue(blankId2.startsWith("_:")); + assertNotEquals(blankId1, blankId2); + } + + @Test + @DisplayName("Update edge should succeed") + void testUpdateEdge() throws DataManagerException { + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o1 = graph.addResource("http://example.org/object1"); + Node o2 = graph.addResource("http://example.org/object2"); + + // Setup: add old edge + graph.addEdge(s, p, o1); + Edge oldEdge = graph.create(graph.getDefaultGraphNode(), s, p, o1); + Edge newEdge = graph.create(graph.getDefaultGraphNode(), s, p, o2); + + MutationResult result = mutationOperations.updateEdge(oldEdge, newEdge); + + assertTrue(result.isSuccess()); + assertEquals(1, graph.size()); + } + + @Test + @DisplayName("Update edge with null old edge should throw IllegalArgumentException") + void testUpdateEdgeWithNullOldEdgeThrows() { + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + Edge newEdge = graph.create(graph.getDefaultGraphNode(), s, p, o); + + assertThrows(IllegalArgumentException.class, () -> mutationOperations.updateEdge(null, newEdge)); + } + + @Test + @DisplayName("Update edge with null new edge should throw IllegalArgumentException") + void testUpdateEdgeWithNullNewEdgeThrows() { + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + + // Setup + graph.addEdge(s, p, o); + Edge oldEdge = graph.create(graph.getDefaultGraphNode(), s, p, o); + + assertThrows(IllegalArgumentException.class, () -> mutationOperations.updateEdge(oldEdge, null)); + } + + @Test + @DisplayName("Insert edge with specific context should use that context") + void testInsertEdgeWithContext() throws DataManagerException { + Node context = graph.addGraph("http://example.org/myGraph"); + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + + MutationResult result = mutationOperations.insertEdge(s, p, o, List.of(context)); + + assertTrue(result.isSuccess()); + assertEquals(1, graph.size()); + } + + @Test + @DisplayName("Insert duplicate edge should handle gracefully") + void testInsertDuplicateEdge() throws DataManagerException { + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + + // Insert first time + MutationResult result1 = mutationOperations.insertEdge(s, p, o, null); + assertTrue(result1.isSuccess()); + + // Insert duplicate + MutationResult result2 = mutationOperations.insertEdge(s, p, o, null); + + // May succeed or fail depending on Graph implementation + assertNotNull(result2); + } + + @Test + @DisplayName("Delete with pattern and specific context should delete from that context only") + void testDeleteEdgesWithContext() throws DataManagerException { + Node ctx1 = graph.addGraph("http://example.org/graph1"); + Node ctx2 = graph.addGraph("http://example.org/graph2"); + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + + // Setup: add edges to both contexts + graph.addEdge(ctx1, s, p, o); + graph.addEdge(ctx2, s, p, o); + + MutationResult result = mutationOperations.deleteEdges(s, p, o, List.of(ctx1)); + + assertTrue(result.isBulk()); + assertTrue(result.getSuccessCount() >= 1); + } + + @Test + @DisplayName("MutationResult should contain correct edge information") + void testMutationResultContainsEdgeInfo() throws DataManagerException { + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + Edge edge = graph.create(graph.getDefaultGraphNode(), s, p, o); + + MutationResult result = mutationOperations.insertEdge(edge); + + assertTrue(result.isSuccess()); + assertNotNull(result.getAffectedEdge()); + assertEquals(edge.getSubjectNode(), result.getAffectedEdge().getSubjectNode()); + assertEquals(edge.getPropertyNode(), result.getAffectedEdge().getPropertyNode()); + assertEquals(edge.getObjectNode(), result.getAffectedEdge().getObjectNode()); + } + + @Test + @DisplayName("Multiple inserts should accumulate in graph") + void testMultipleInserts() throws DataManagerException { + for (int i = 0; i < 5; i++) { + Node s = graph.addResource("http://example.org/s" + i); + Node p = graph.addProperty("http://example.org/p"); + Node o = graph.addResource("http://example.org/o" + i); + + MutationResult result = mutationOperations.insertEdge(s, p, o, null); + assertTrue(result.isSuccess()); + } + + assertEquals(5, graph.size()); + } + + @Test + @DisplayName("Delete pattern with no matches should return empty result") + void testDeletePatternNoMatches() throws DataManagerException { + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + + MutationResult result = mutationOperations.deleteEdges(s, p, null, null); + + assertTrue(result.isBulk()); + assertEquals(0, result.getSuccessCount()); + } +} \ No newline at end of file diff --git a/src/test/java/fr/inria/corese/core/storage/impl/operations/QueryOperationsImplTest.java b/src/test/java/fr/inria/corese/core/storage/impl/operations/QueryOperationsImplTest.java new file mode 100644 index 000000000..4f266ed16 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/storage/impl/operations/QueryOperationsImplTest.java @@ -0,0 +1,291 @@ +package fr.inria.corese.core.storage.impl.operations; + +import fr.inria.corese.core.Graph; +import fr.inria.corese.core.kgram.api.core.Edge; +import fr.inria.corese.core.kgram.api.core.Node; +import fr.inria.corese.core.storage.api.datamanager.support.exception.DataManagerException; +import fr.inria.corese.core.storage.api.datamanager.support.model.EdgePattern; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for QueryOperationsImpl. + * + */ +@DisplayName("QueryOperationsImpl Tests") +class QueryOperationsImplTest { + + private QueryOperationsImpl queryOperations; + private Graph graph; + + @BeforeEach + void setUp() { + graph = new Graph(); + queryOperations = new QueryOperationsImpl(graph); + } + + @Test + @DisplayName("Constructor with null graph should throw IllegalArgumentException") + void testConstructorWithNullGraphThrows() { + assertThrows(IllegalArgumentException.class, () -> new QueryOperationsImpl(null)); + } + + @Test + @DisplayName("Query with null pattern should throw IllegalArgumentException") + void testQueryWithNullPatternThrows() { + assertThrows(IllegalArgumentException.class, () -> queryOperations.query(null)); + } + + @Test + @DisplayName("Query with match-all pattern on empty graph should return empty stream") + void testQueryMatchAllOnEmptyGraph() throws DataManagerException { + EdgePattern pattern = EdgePattern.builder().build(); + + try (Stream edges = queryOperations.query(pattern)) { + assertEquals(0, edges.count()); + } + } + + @Test + @DisplayName("Query with predicate pattern should return matching edges") + void testQueryWithPredicatePattern() throws DataManagerException { + // Add test data + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + graph.addEdge(s, p, o); + + EdgePattern pattern = EdgePattern.builder() + .predicate(p) + .build(); + + try (Stream edges = queryOperations.query(pattern)) { + assertEquals(1, edges.count()); + } + } + + @Test + @DisplayName("Query with subject and predicate pattern should match specific edges") + void testQueryWithSubjectAndPredicatePattern() throws DataManagerException { + Node s1 = graph.addResource("http://example.org/s1"); + Node s2 = graph.addResource("http://example.org/s2"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + + graph.addEdge(s1, p, o); + graph.addEdge(s2, p, o); + + EdgePattern pattern = EdgePattern.builder() + .subject(s1) + .predicate(p) + .build(); + + try (Stream edges = queryOperations.query(pattern)) { + long count = edges.count(); + assertEquals(1, count); + } + } + + + @Test + @DisplayName("Count with match-all pattern should return total edge count") + void testCountWithMatchAllPattern() throws DataManagerException { + // Add 5 edges + for (int i = 0; i < 5; i++) { + Node s = graph.addResource("http://example.org/s" + i); + Node p = graph.addProperty("http://example.org/p"); + Node o = graph.addResource("http://example.org/o" + i); + graph.addEdge(s, p, o); + } + + EdgePattern pattern = EdgePattern.builder().build(); + + long count = queryOperations.count(pattern); + + assertEquals(5, count); + } + + @Test + @DisplayName("Count on empty graph should return 0") + void testCountOnEmptyGraph() throws DataManagerException { + EdgePattern pattern = EdgePattern.builder().build(); + + long count = queryOperations.count(pattern); + + assertEquals(0, count); + } + + @Test + @DisplayName("Count with specific predicate should return matching count") + void testCountWithPredicatePattern() throws DataManagerException { + Node p1 = graph.addProperty("http://example.org/p1"); + Node p2 = graph.addProperty("http://example.org/p2"); + Node s = graph.addResource("http://example.org/subject"); + Node o = graph.addResource("http://example.org/object"); + + graph.addEdge(s, p1, o); + graph.addEdge(s, p1, o); + graph.addEdge(s, p2, o); + + EdgePattern pattern = EdgePattern.builder() + .predicate(p1) + .build(); + + long count = queryOperations.count(pattern); + + assertTrue(count >= 1); + } + + @Test + @DisplayName("Exists should return true when pattern matches") + void testExistsReturnsTrueWhenMatches() throws DataManagerException { + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + graph.addEdge(s, p, o); + + EdgePattern pattern = EdgePattern.builder() + .predicate(p) + .build(); + + boolean exists = queryOperations.exists(pattern); + + assertTrue(exists); + } + + @Test + @DisplayName("Exists should return false when pattern does not match") + void testExistsReturnsFalseWhenNoMatches() throws DataManagerException { + Node p = graph.addProperty("http://example.org/nonexistent"); + + EdgePattern pattern = EdgePattern.builder() + .predicate(p) + .build(); + + boolean exists = queryOperations.exists(pattern); + + assertFalse(exists); + } + + + @Test + @DisplayName("Find with null edge should throw IllegalArgumentException") + void testFindWithNullEdgeThrows() { + assertThrows(IllegalArgumentException.class, () -> queryOperations.find(null)); + } + + @Test + @DisplayName("Query with context filter should return edges from that context") + void testQueryWithContextFilter() throws DataManagerException { + Node context = graph.addGraph("http://example.org/graph1"); + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + + graph.addEdge(context, s, p, o); + + EdgePattern pattern = EdgePattern.builder() + .contexts(java.util.List.of(context)) + .build(); + + try (Stream edges = queryOperations.query(pattern)) { + assertTrue(edges.findAny().isPresent()); + } + } + + @Test + @DisplayName("Query should support multiple predicates") + void testQueryWithMultiplePredicates() throws DataManagerException { + Node s = graph.addResource("http://example.org/subject"); + Node p1 = graph.addProperty("http://example.org/p1"); + Node p2 = graph.addProperty("http://example.org/p2"); + Node p3 = graph.addProperty("http://example.org/p3"); + Node o = graph.addResource("http://example.org/object"); + + graph.addEdge(s, p1, o); + graph.addEdge(s, p2, o); + graph.addEdge(s, p3, o); + + EdgePattern pattern = EdgePattern.builder() + .subject(s) + .build(); + + try (Stream edges = queryOperations.query(pattern)) { + assertEquals(3, edges.count()); + } + } + + + @Test + @DisplayName("Count should use optimization when available") + void testCountOptimization() throws DataManagerException { + // Add many edges + for (int i = 0; i < 100; i++) { + Node s = graph.addResource("http://example.org/s" + i); + Node p = graph.addProperty("http://example.org/p"); + Node o = graph.addResource("http://example.org/o" + i); + graph.addEdge(s, p, o); + } + + EdgePattern pattern = EdgePattern.builder().build(); + + long count = queryOperations.count(pattern); + + assertEquals(100, count); + } + + @Test + @DisplayName("Query with object pattern should match edges") + void testQueryWithObjectPattern() throws DataManagerException { + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o1 = graph.addResource("http://example.org/object1"); + Node o2 = graph.addResource("http://example.org/object2"); + + graph.addEdge(s, p, o1); + graph.addEdge(s, p, o2); + + EdgePattern pattern = EdgePattern.builder() + .object(o1) + .build(); + + try (Stream edges = queryOperations.query(pattern)) { + long count = edges.count(); + assertTrue(count >= 1); + } + } + + @Test + @DisplayName("Exists on empty graph should return false") + void testExistsOnEmptyGraph() throws DataManagerException { + EdgePattern pattern = EdgePattern.builder().build(); + + boolean exists = queryOperations.exists(pattern); + + assertFalse(exists); + } + + @Test + @DisplayName("Multiple queries should return consistent results") + void testMultipleQueriesConsistency() throws DataManagerException { + Node s = graph.addResource("http://example.org/subject"); + Node p = graph.addProperty("http://example.org/predicate"); + Node o = graph.addResource("http://example.org/object"); + graph.addEdge(s, p, o); + + EdgePattern pattern = EdgePattern.builder() + .predicate(p) + .build(); + + for (int i = 0; i < 3; i++) { + try (Stream edges = queryOperations.query(pattern)) { + assertEquals(1, edges.count()); + } + } + } +} \ No newline at end of file