diff --git a/src/main/java/fr/inria/corese/core/next/data/factory/ModelFactory.java b/src/main/java/fr/inria/corese/core/next/data/factory/ModelFactory.java
new file mode 100644
index 000000000..98453706c
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/data/factory/ModelFactory.java
@@ -0,0 +1,172 @@
+package fr.inria.corese.core.next.data.factory;
+
+import fr.inria.corese.core.Graph;
+import fr.inria.corese.core.next.data.api.Model;
+import fr.inria.corese.core.next.data.api.ValueFactory;
+import fr.inria.corese.core.next.data.impl.StorageModel;
+import fr.inria.corese.core.next.storagemanager.api.plugin.PluginException;
+import fr.inria.corese.core.next.storagemanager.api.plugin.StoragePluginManager;
+import fr.inria.corese.core.next.storagemanager.api.support.config.StorageConfig;
+
+/**
+ * Factory class for creating Model instances with different storage backends.
+ *
+ *
This factory simplifies the creation of models by providing convenient methods
+ * to create models backed by different storage implementations (Memory, Graph, etc.)
+ * without requiring explicit StorageManager configuration.
+ *
+ *
Supported Storage Types
+ *
+ * - "memory" - In-memory HashMap-based storage (fast, no persistence)
+ * - "graph" - Legacy Graph-based storage (indexed, persistent)
+ *
+ *
+ * @see Model
+ * @see StorageModel
+ * @see StoragePluginManager
+ * @see ValueFactory
+ */
+public record ModelFactory(ValueFactory valueFactory) {
+
+ /**
+ * Constructs a new ModelFactory with the specified ValueFactory.
+ *
+ * @param valueFactory the ValueFactory to use for creating RDF values
+ * @throws NullPointerException if valueFactory is null
+ */
+ public ModelFactory {
+ if (valueFactory == null) {
+ throw new NullPointerException("ValueFactory cannot be null");
+ }
+ }
+
+ /**
+ * Creates a new Model instance with the specified storage backend type.
+ *
+ * @param storageType the storage backend type (case-insensitive)
+ * @return a new Model instance backed by the specified storage type
+ * @throws IllegalArgumentException if storageType is null or empty
+ * @throws PluginException if the storage backend fails to initialize
+ * @see #createModel(StorageConfig)
+ */
+ public Model createModel(String storageType) throws PluginException {
+ if (storageType == null || storageType.trim().isEmpty()) {
+ throw new IllegalArgumentException("Storage type cannot be null or empty");
+ }
+
+ // Create minimal config with just the type
+ // Let StoragePluginManager discover and select the appropriate plugin
+ StorageConfig config = StorageConfig.builder()
+ .property("type", storageType.trim())
+ .build();
+
+ return createModel(config);
+ }
+
+ /**
+ * Creates a new Model instance from a {@link StorageConfig}.
+ *
+ * This is the most flexible method for creating models, allowing full control
+ * over storage configuration. The {@link StoragePluginManager} will select the
+ * appropriate plugin based on the config.
+ *
+ * @param config the storage configuration (must not be null)
+ * @return a new Model instance backed by the configured storage
+ * @throws IllegalArgumentException if config is null
+ * @throws PluginException if the storage backend fails to initialize
+ * @see #createModel(String)
+ */
+ public Model createModel(StorageConfig config) throws PluginException {
+ if (config == null) {
+ throw new IllegalArgumentException("StorageConfig cannot be null");
+ }
+
+ return StorageModel.builder()
+ .storage(StoragePluginManager.create(config))
+ .valueFactory(valueFactory)
+ .build();
+ }
+
+ /**
+ * Creates a new memory-based Model instance.
+ *
+ * Memory storage uses a ConcurrentHashMap backend for fast, in-memory storage.
+ * This is ideal for testing, prototyping, and small datasets (< 100K statements).
+ *
+ * @return a new Model instance backed by memory storage
+ * @throws PluginException if the memory storage fails to initialize
+ * @see #createGraphModel()
+ * @see #createModel(String)
+ */
+ public Model createMemoryModel() throws PluginException {
+ return createModel(createMemoryConfig());
+ }
+
+ /**
+ * Creates a new graph-based Model instance.
+ *
+ *
Graph storage wraps the legacy Corese Graph implementation, providing
+ * indexed access, persistence support, and excellent performance for large datasets.
+ * This is the recommended choice for production use.
+ *
+ * @return a new Model instance backed by a new Graph storage
+ * @throws PluginException if the graph storage fails to initialize
+ * @see #createGraphModel(Graph)
+ * @see #createMemoryModel()
+ * @see #createModel(String)
+ */
+ public Model createGraphModel() throws PluginException {
+ return createModel(createGraphConfig());
+ }
+
+ /**
+ * Creates a new graph-based Model instance backed by the specified Graph.
+ *
+ *
This method allows you to wrap an existing Graph instance with the Model API,
+ * enabling you to use legacy Graph-based code with the new Model interface.
+ * All operations on the returned Model will delegate to the underlying Graph.
+ *
+ * @param graph the Graph instance to wrap with the Model API
+ * @return a new Model instance backed by the specified Graph
+ * @throws NullPointerException if graph is null
+ * @throws PluginException if the graph storage fails to initialize
+ * @see #createGraphModel()
+ */
+ public Model createGraphModel(Graph graph) throws PluginException {
+ if (graph == null) {
+ throw new NullPointerException("Graph cannot be null");
+ }
+
+ StorageConfig config = StorageConfig.builder()
+ .property("type", "graph")
+ .property("graph", graph)
+ .property("valueFactory", valueFactory)
+ .build();
+
+ return createModel(config);
+ }
+
+ /**
+ * Creates a StorageConfig for memory-based storage.
+ *
+ * @return a StorageConfig configured for memory storage
+ */
+ private StorageConfig createMemoryConfig() {
+ return StorageConfig.builder()
+ .property("type", "memory")
+ .build();
+ }
+
+ /**
+ * Creates a StorageConfig for graph-based storage with a new Graph instance.
+ *
+ * @return a StorageConfig configured for graph storage
+ */
+ private StorageConfig createGraphConfig() {
+ return StorageConfig.builder()
+ .property("type", "graph")
+ .property("graph", Graph.create())
+ .property("valueFactory", valueFactory)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/ExternalPluginLoader.java b/src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/ExternalPluginLoader.java
new file mode 100644
index 000000000..b0a9335cb
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/ExternalPluginLoader.java
@@ -0,0 +1,95 @@
+package fr.inria.corese.core.next.storagemanager.api.plugin;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ServiceLoader;
+
+/**
+ * Utility class for loading StoragePlugin instances from external JAR files.
+ */
+public class ExternalPluginLoader {
+
+ private static final Logger logger = LoggerFactory.getLogger(ExternalPluginLoader.class);
+
+ /**
+ * List of loaded plugin class loaders (to prevent garbage collection).
+ */
+ private static final List loadedClassLoaders = new ArrayList<>();
+
+ /**
+ * Private constructor - this is a utility class.
+ */
+ private ExternalPluginLoader() {
+ throw new AssertionError("ExternalPluginLoader is a utility class");
+ }
+
+ /**
+ * Loads all plugins from a specific JAR file.
+ *
+ * @param jarFile the JAR file containing plugins
+ * @return the number of plugins loaded from this JAR
+ * @throws IllegalArgumentException if jarFile is null or doesn't exist
+ * @throws Exception if loading fails
+ */
+ public static int loadPluginsFromJar(File jarFile) throws Exception {
+ if (jarFile == null) {
+ throw new IllegalArgumentException("JAR file cannot be null");
+ }
+ if (!jarFile.exists() || !jarFile.isFile()) {
+ throw new IllegalArgumentException("JAR file does not exist: " + jarFile);
+ }
+
+ logger.info("Loading plugins from JAR: {} ({} bytes)",
+ jarFile.getName(), jarFile.length());
+
+ // Create ClassLoader for the JAR
+ URLClassLoader classLoader = new URLClassLoader(
+ new URL[]{jarFile.toURI().toURL()},
+ ExternalPluginLoader.class.getClassLoader()
+ );
+
+ // Keep reference to prevent garbage collection
+ loadedClassLoaders.add(classLoader);
+
+ // Register ClassLoader with StoragePluginManager
+ StoragePluginManager.registerClassLoader(classLoader);
+
+ // Load plugins using ServiceLoader
+ ServiceLoader loader = ServiceLoader.load(
+ StoragePlugin.class,
+ classLoader
+ );
+
+ int count = 0;
+ for (StoragePlugin plugin : loader) {
+ logger.info("Loaded plugin: {} (priority={}, jar={})",
+ plugin.getName(),
+ plugin.getPriority(),
+ jarFile.getName());
+ count++;
+ }
+
+ // Refresh the plugin cache
+ if (count > 0) {
+ StoragePluginManager.reload();
+ logger.info("Plugin cache refreshed");
+ }
+
+ return count;
+ }
+
+
+ /**
+ * Clears all loaded class loaders.
+ */
+ public static void clear() {
+ logger.info("Clearing {} loaded class loaders", loadedClassLoaders.size());
+ loadedClassLoaders.clear();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/PluginException.java b/src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/PluginException.java
new file mode 100644
index 000000000..6e6cdb9f1
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/PluginException.java
@@ -0,0 +1,18 @@
+package fr.inria.corese.core.next.storagemanager.api.plugin;
+
+import fr.inria.corese.core.next.storagemanager.api.support.exception.ErrorCode;
+import fr.inria.corese.core.next.storagemanager.api.support.exception.StorageException;
+
+/**
+ * Exception thrown when a plugin fails to create a StorageManager instance.
+ */
+public class PluginException extends StorageException {
+
+ public PluginException(String message) {
+ super(ErrorCode.PLUGIN_CREATION_FAILED, message);
+ }
+
+ public PluginException(String message, Throwable cause) {
+ super(ErrorCode.PLUGIN_CREATION_FAILED, message, cause);
+ }
+}
diff --git a/src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/PluginNotFoundException.java b/src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/PluginNotFoundException.java
new file mode 100644
index 000000000..f3db04b78
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/PluginNotFoundException.java
@@ -0,0 +1,11 @@
+package fr.inria.corese.core.next.storagemanager.api.plugin;
+
+/**
+ * Exception thrown when no plugin is found for the requested configuration.
+ */
+public class PluginNotFoundException extends PluginException {
+
+ public PluginNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/StoragePlugin.java b/src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/StoragePlugin.java
new file mode 100644
index 000000000..bda07f7c5
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/StoragePlugin.java
@@ -0,0 +1,80 @@
+package fr.inria.corese.core.next.storagemanager.api.plugin;
+
+import fr.inria.corese.core.next.storagemanager.api.StorageManager;
+import fr.inria.corese.core.next.storagemanager.api.support.config.StorageConfig;
+
+/**
+ * Service Provider Interface (SPI) for StorageManager plugins.
+ */
+public interface StoragePlugin {
+
+ /**
+ * Returns the unique name of this plugin.
+ *
+ * @return the plugin name (must be unique and non-null)
+ */
+ String getName();
+
+ /**
+ * Returns a human-readable description of this plugin.
+ *
+ * @return the plugin description (never null)
+ */
+ default String getDescription() {
+ return "StorageManager plugin: " + getName();
+ }
+
+ /**
+ * Checks if this plugin supports the given configuration.
+ *
+ *
+ * @param config the storage configuration to check (never null)
+ * @return {@code true} if this plugin can create a StorageManager for this config
+ * @throws IllegalArgumentException if config is null
+ */
+ boolean supports(StorageConfig config);
+
+ /**
+ * Creates a StorageManager instance from the given configuration.
+ *
+ * @param config the storage configuration (never null)
+ * @return a configured StorageManager instance (never null)
+ * @throws PluginException if the StorageManager cannot be created
+ * @throws IllegalArgumentException if config is null
+ */
+ StorageManager create(StorageConfig config) throws PluginException;
+
+ /**
+ * Returns the priority of this plugin.
+ *
+ * When multiple plugins support the same configuration (i.e., their
+ * {@link #supports(StorageConfig)} method returns {@code true}), the plugin
+ * with the highest priority is selected.
+ *
+ *
Priority Semantics:
+ *
+ * - Higher values = Higher priority: A plugin with priority 100 will be
+ * selected over one with priority 50
+ * - Default is 0: If this method is not overridden, the plugin has priority 0
+ * - Negative values are allowed: Use negative priorities for fallback plugins
+ * that should only be used when no other plugin is available
+ *
+ *
+ *
+ * Built-in Priorities:
+ *
+ * - GraphStoragePlugin: 100
+ * - MemoryStoragePlugin: 50
+ *
+ *
+ *
+ * Tie-Breaking: If multiple plugins have the same priority and support
+ * the same configuration, the selection is non-deterministic. Avoid this by ensuring
+ * each plugin has a unique priority for overlapping configurations.
+ *
+ * @return the plugin priority (higher values = higher priority, default is 0)
+ */
+ default int getPriority() {
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/StoragePluginManager.java b/src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/StoragePluginManager.java
new file mode 100644
index 000000000..9f7b092d7
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/StoragePluginManager.java
@@ -0,0 +1,204 @@
+package fr.inria.corese.core.next.storagemanager.api.plugin;
+
+import fr.inria.corese.core.next.storagemanager.api.StorageManager;
+import fr.inria.corese.core.next.storagemanager.api.support.config.StorageConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+/**
+ * Manager for discovering and creating StorageManager plugins.
+ */
+public class StoragePluginManager {
+
+ private static final Logger logger = LoggerFactory.getLogger(StoragePluginManager.class);
+
+ /**
+ * ServiceLoader for discovering plugins
+ */
+ private static final List classLoaders = Collections.synchronizedList(new ArrayList<>());
+
+ static {
+ // Always include the system ClassLoader for internal plugins
+ classLoaders.add(StoragePluginManager.class.getClassLoader());
+ }
+
+ /**
+ * Cache of discovered plugins (thread-safe)
+ */
+ private static volatile List cachedPlugins;
+
+ /**
+ * Plugin lookup cache by name (thread-safe)
+ */
+ private static final Map pluginsByName =
+ new ConcurrentHashMap<>();
+
+ /**
+ * Private constructor - this is a static utility class.
+ */
+ private StoragePluginManager() {
+ throw new AssertionError("StoragePluginManager is a utility class");
+ }
+
+ /**
+ * Creates a StorageManager instance from the given configuration.
+ *
+ * @param config the storage configuration (must not be null)
+ * @return a configured StorageManager instance
+ * @throws IllegalArgumentException if config is null
+ * @throws PluginNotFoundException if no plugin supports the configuration
+ * @throws PluginException if the StorageManager cannot be created
+ */
+ public static StorageManager create(StorageConfig config) throws PluginException {
+ if (config == null) {
+ throw new IllegalArgumentException("StorageConfig must not be null");
+ }
+
+ // Get all available plugins
+ List allPlugins = getAvailablePlugins();
+
+ // Find plugins that support this configuration
+ List supportingPlugins = allPlugins.stream()
+ .filter(plugin -> plugin.supports(config))
+ .sorted(Comparator.comparingInt(StoragePlugin::getPriority).reversed())
+ .toList();
+
+ if (supportingPlugins.isEmpty()) {
+ String availableTypes = allPlugins.stream()
+ .map(StoragePlugin::getName)
+ .collect(Collectors.joining(", "));
+
+ throw new PluginNotFoundException(
+ String.format("No plugin found for storage type '%s'. Available types: [%s]",
+ config.getType().orElse("not specified"), availableTypes)
+ );
+ }
+
+ // Select plugin with highest priority
+ StoragePlugin selectedPlugin = supportingPlugins.getFirst();
+
+ if (supportingPlugins.size() > 1) {
+ String otherPlugins = supportingPlugins.stream()
+ .skip(1)
+ .map(p -> p.getName() + " (priority=" + p.getPriority() + ")")
+ .collect(Collectors.joining(", "));
+
+ logger.warn("Multiple plugins support this configuration. " +
+ "Selected '{}' (priority={}). Ignored: {}",
+ selectedPlugin.getName(),
+ selectedPlugin.getPriority(),
+ otherPlugins);
+ }
+
+ try {
+ return selectedPlugin.create(config);
+ } catch (PluginException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new PluginException(
+ "Failed to create StorageManager with plugin '" +
+ selectedPlugin.getName() + "'", e);
+ }
+ }
+
+ /**
+ * Returns all available StoragePlugin implementations.
+ *
+ * @return unmodifiable list of available plugins (never null, may be empty)
+ */
+ public static List getAvailablePlugins() {
+ if (cachedPlugins == null) {
+ synchronized (StoragePluginManager.class) {
+ if (cachedPlugins == null) {
+ Map uniquePlugins = new HashMap<>();
+
+ // Search all registered ClassLoaders
+ for (ClassLoader classLoader : classLoaders) {
+ ServiceLoader loader = ServiceLoader.load(
+ StoragePlugin.class,
+ classLoader
+ );
+
+ for (StoragePlugin plugin : loader) {
+ // Keep only the first occurrence of each plugin name
+ // (external plugins can override internal ones if loaded first)
+ uniquePlugins.putIfAbsent(plugin.getName(), plugin);
+ }
+ }
+
+ // Update name cache
+ pluginsByName.clear();
+ pluginsByName.putAll(uniquePlugins);
+
+ // Sort by priority (highest first)
+ List plugins = new ArrayList<>(uniquePlugins.values());
+ plugins.sort(Comparator.comparingInt(StoragePlugin::getPriority).reversed());
+
+ // Make immutable
+ cachedPlugins = Collections.unmodifiableList(plugins);
+
+ logger.debug("Discovered {} plugin(s): {}",
+ cachedPlugins.size(),
+ cachedPlugins.stream()
+ .map(p -> p.getName() + "(" + p.getPriority() + ")")
+ .collect(Collectors.joining(", ")));
+ }
+ }
+ }
+
+ return cachedPlugins;
+ }
+
+ /**
+ * Registers a ClassLoader to search for plugins.
+ *
+ * @param classLoader the ClassLoader to register (must not be null)
+ * @throws IllegalArgumentException if classLoader is null
+ */
+ public static void registerClassLoader(ClassLoader classLoader) {
+ if (classLoader == null) {
+ throw new IllegalArgumentException("ClassLoader cannot be null");
+ }
+
+ synchronized (classLoaders) {
+ if (!classLoaders.contains(classLoader)) {
+ classLoaders.add(classLoader);
+ logger.debug("Registered ClassLoader: {}", classLoader.getClass().getSimpleName());
+ }
+ }
+ }
+
+ /**
+ * Finds a plugin by its name.
+ *
+ * @param name the plugin name (case-sensitive)
+ * @return the plugin with the given name, or empty if not found
+ * @throws IllegalArgumentException if name is null
+ */
+ public static Optional findPlugin(String name) {
+ if (name == null) {
+ throw new IllegalArgumentException("Plugin name must not be null");
+ }
+
+ // Ensure plugins are loaded
+ getAvailablePlugins();
+
+ return Optional.ofNullable(pluginsByName.get(name));
+ }
+
+
+ /**
+ * Reloads all plugins from the classpath.
+ */
+ public static void reload() {
+ synchronized (StoragePluginManager.class) {
+ cachedPlugins = null;
+ pluginsByName.clear();
+ logger.debug("Plugin cache cleared");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/inria/corese/core/next/storagemanager/api/support/config/StorageConfig.java b/src/main/java/fr/inria/corese/core/next/storagemanager/api/support/config/StorageConfig.java
index cd32d5c0a..8bd4db9ca 100644
--- a/src/main/java/fr/inria/corese/core/next/storagemanager/api/support/config/StorageConfig.java
+++ b/src/main/java/fr/inria/corese/core/next/storagemanager/api/support/config/StorageConfig.java
@@ -19,7 +19,6 @@ public final class StorageConfig {
*/
private final boolean transactionSupport;
-
/**
* Private constructor — use {@link #builder()} to create instances.
*
@@ -63,6 +62,26 @@ public Map getProperties() {
return properties;
}
+ /**
+ * Returns whether transaction support is enabled.
+ *
+ * @return {@code true} if transaction support is enabled
+ */
+ public boolean hasTransactionSupport() {
+ return transactionSupport;
+ }
+
+ /**
+ * Returns the storage type from properties.
+ *
+ * This is a convenience method for {@code getProperty("type", String.class)}.
+ * The type is used by the plugin system to select the appropriate StorageManager.
+ *
+ * @return the storage type, or empty if not set
+ */
+ public Optional getType() {
+ return getProperty("type", String.class);
+ }
/**
* Creates a new {@link Builder} for constructing {@code StorageConfig} instances.
@@ -76,7 +95,8 @@ public static Builder builder() {
@Override
public String toString() {
return "StorageConfig{" +
- "transactionSupport=" + transactionSupport +
+ "type=" + getType().orElse("not set") +
+ ", transactionSupport=" + transactionSupport +
", properties=" + properties +
'}';
}
@@ -86,10 +106,8 @@ public String toString() {
*/
public static final class Builder {
-
private final Map properties = new HashMap<>();
-
- private final boolean transactionSupport = false;
+ private boolean transactionSupport = false; // NOT final!
/**
* Private constructor — use {@link StorageConfig#builder()}.
@@ -117,6 +135,16 @@ public Builder property(String key, Object value) {
return this;
}
+ /**
+ * Enables or disables transaction support.
+ *
+ * @param enable {@code true} to enable transaction support
+ * @return this builder for method chaining
+ */
+ public Builder transactionSupport(boolean enable) {
+ this.transactionSupport = enable;
+ return this;
+ }
/**
* Builds the {@link StorageConfig} instance with the current configuration.
diff --git a/src/main/java/fr/inria/corese/core/next/storagemanager/api/support/exception/ErrorCode.java b/src/main/java/fr/inria/corese/core/next/storagemanager/api/support/exception/ErrorCode.java
index 5d940e5b3..726a3a6a9 100644
--- a/src/main/java/fr/inria/corese/core/next/storagemanager/api/support/exception/ErrorCode.java
+++ b/src/main/java/fr/inria/corese/core/next/storagemanager/api/support/exception/ErrorCode.java
@@ -36,7 +36,13 @@ public enum ErrorCode {
/** Restart failed and rollback also failed (critical) */
RESTART_FAILED_ROLLBACK_FAILED("RESTART_FAIL_ROLLBACK_FAIL",
- "Restart failed and unable to restore previous configuration"),;
+ "Restart failed and unable to restore previous configuration"),
+
+ /**
+ * Plugin failed to create StorageManager instance
+ */
+ PLUGIN_CREATION_FAILED("PLUGIN_CREATION_FAILED", "Plugin failed to create StorageManager instance"),
+ ;
private final String code;
private final String description;
diff --git a/src/main/java/fr/inria/corese/core/next/storagemanager/impl/graph/GraphAdapter.java b/src/main/java/fr/inria/corese/core/next/storagemanager/impl/graph/GraphAdapter.java
index 90dcafd2d..eb6c13b8b 100644
--- a/src/main/java/fr/inria/corese/core/next/storagemanager/impl/graph/GraphAdapter.java
+++ b/src/main/java/fr/inria/corese/core/next/storagemanager/impl/graph/GraphAdapter.java
@@ -32,8 +32,20 @@ public record GraphAdapter(Graph graph, ValueFactory valueFactory) {
* @return true if the statement was added, false if it already existed
*/
public boolean add(Statement stmt) {
- Edge edge = statementToEdge(stmt);
- return graph.addEdge(edge) != null;
+ // Convert statement components to nodes
+ Node subject = resourceToNode(stmt.getSubject());
+ Node predicate = iriToNode(stmt.getPredicate());
+ Node object = valueToNode(stmt.getObject());
+
+ Resource ctxResource = stmt.getContext();
+ Node context = (ctxResource != null)
+ ? resourceToNode(ctxResource)
+ : graph.getDefaultGraphNode();
+
+ // Always use 4-argument form with explicit context
+ Edge edge = graph.addEdge(context, subject, predicate, object);
+
+ return edge != null;
}
/**
@@ -65,7 +77,7 @@ public boolean contains(Statement stmt) {
* @param s subject filter, or null for any
* @param p predicate filter, or null for any
* @param o object filter, or null for any
- * @param contexts context filters; empty or null means any context
+ * @param contexts context filters; null/empty = wildcard, {null} = default graph only
* @return set of matching statements
*/
public Set find(Resource s, IRI p, Value o, Resource[] contexts) {
@@ -77,15 +89,20 @@ public Set find(Resource s, IRI p, Value o, Resource[] contexts) {
if (contexts == null || contexts.length == 0) {
edges = graph.getEdgesRDF4J(subject, predicate, object);
} else {
- Node[] ctxNodes = Arrays.stream(contexts)
- .map(this::resourceToNode)
- .toArray(Node[]::new);
+ // Normalize contexts: convert null to default graph node
+ Node[] ctxNodes = normalizeContexts(contexts);
edges = graph.getEdgesRDF4J(subject, predicate, object, ctxNodes);
}
Set results = new HashSet<>();
for (Edge edge : edges) {
- results.add(edgeToStatement(edge));
+ if (edge != null) {
+ try {
+ results.add(edgeToStatement(edge));
+ } catch (IllegalArgumentException e) {
+ // Skip edges that cannot be converted
+ }
+ }
}
return results;
}
@@ -124,7 +141,13 @@ public void clearContext(Resource context) {
public Set getSubjects() {
Set subjects = new HashSet<>();
for (Edge edge : graph.getEdges()) {
- subjects.add(nodeToResource(edge.getSubjectNode()));
+ if (edge != null && edge.getSubjectNode() != null) {
+ try {
+ subjects.add(nodeToResource(edge.getSubjectNode()));
+ } catch (IllegalArgumentException e) {
+ // Skip invalid nodes
+ }
+ }
}
return subjects;
}
@@ -137,7 +160,13 @@ public Set getSubjects() {
public Set getPredicates() {
Set predicates = new HashSet<>();
for (Edge edge : graph.getEdges()) {
- predicates.add(nodeToIRI(edge.getEdgeNode()));
+ if (edge != null && edge.getEdgeNode() != null) {
+ try {
+ predicates.add(nodeToIRI(edge.getEdgeNode()));
+ } catch (IllegalArgumentException e) {
+ // Skip invalid nodes
+ }
+ }
}
return predicates;
}
@@ -150,7 +179,13 @@ public Set getPredicates() {
public Set getObjects() {
Set objects = new HashSet<>();
for (Edge edge : graph.getEdges()) {
- objects.add(nodeToValue(edge.getObjectNode()));
+ if (edge != null && edge.getObjectNode() != null) {
+ try {
+ objects.add(nodeToValue(edge.getObjectNode()));
+ } catch (IllegalArgumentException e) {
+ // Skip invalid nodes
+ }
+ }
}
return objects;
}
@@ -163,28 +198,63 @@ public Set getObjects() {
public Set getContexts() {
Set contexts = new HashSet<>();
for (Node ctx : graph.getGraphNodes()) {
- contexts.add(nodeToResource(ctx));
+ if (ctx != null) {
+ try {
+ contexts.add(nodeToResource(ctx));
+ } catch (IllegalArgumentException e) {
+ // Skip invalid nodes
+ }
+ }
}
return contexts;
}
/**
- * Converts a Graph {@link Edge} to a Storage {@link Statement}.
+ * Normalizes context array for Graph backend.
*
- * Extracts subject, predicate, object and optional context from the Edge
- * and creates the corresponding Statement using the ValueFactory.
+ * @param contexts the context resources (may contain null)
+ * @return normalized array of Graph nodes (never contains null)
+ */
+ private Node[] normalizeContexts(Resource[] contexts) {
+ return Arrays.stream(contexts)
+ .map(ctx -> ctx != null ? resourceToNode(ctx) : graph.getDefaultGraphNode())
+ .toArray(Node[]::new);
+ }
+
+ /**
+ * Converts a Graph {@link Edge} to a Storage {@link Statement}.
*
* @param edge the Graph edge to convert
* @return the corresponding Statement
+ * @throws IllegalArgumentException if edge or any of its required nodes is null
*/
private Statement edgeToStatement(Edge edge) {
- Resource subject = nodeToResource(edge.getSubjectNode());
- IRI predicate = nodeToIRI(edge.getEdgeNode());
- Value object = nodeToValue(edge.getObjectNode());
+ if (edge == null) {
+ throw new IllegalArgumentException("Edge cannot be null");
+ }
+
+ Node subjectNode = edge.getSubjectNode();
+ Node predicateNode = edge.getEdgeNode();
+ Node objectNode = edge.getObjectNode();
+
+ if (subjectNode == null) {
+ throw new IllegalArgumentException("Edge subject node is null");
+ }
+ if (predicateNode == null) {
+ throw new IllegalArgumentException("Edge predicate node is null");
+ }
+ if (objectNode == null) {
+ throw new IllegalArgumentException("Edge object node is null");
+ }
+
+ Resource subject = nodeToResource(subjectNode);
+ IRI predicate = nodeToIRI(predicateNode);
+ Value object = nodeToValue(objectNode);
- // Context (named graph)
Node graphNode = edge.getGraph();
- Resource context = (graphNode != null) ? nodeToResource(graphNode) : null;
+ Resource context = (graphNode == null || graph.isDefaultGraphNode(graphNode))
+ ? null
+ : nodeToResource(graphNode);
if (context == null) {
return valueFactory.createStatement(subject, predicate, object);
@@ -195,21 +265,22 @@ private Statement edgeToStatement(Edge edge) {
/**
* Converts a Graph {@link Node} to a Storage {@link Resource} (IRI or BNode).
- *
- * @param node the Graph node to convert
- * @return the corresponding Resource (IRI or BNode)
- * @throws IllegalArgumentException if the node is not a URI, blank, or triple reference
*/
private Resource nodeToResource(Node node) {
+ if (node == null) {
+ throw new IllegalArgumentException("Node cannot be null");
+ }
+
IDatatype dt = node.getDatatypeValue();
+ if (dt == null) {
+ throw new IllegalArgumentException("Node datatype is null: " + node);
+ }
if (dt.isURI()) {
return valueFactory.createIRI(dt.getLabel());
} else if (dt.isBlank()) {
return valueFactory.createBNode(dt.getLabel());
} else if (dt.isTriple()) {
- // RDF-star: triple reference node
- // For now, treat as blank node
return valueFactory.createBNode(dt.getLabel());
} else {
throw new IllegalArgumentException("Node is not a Resource: " + node);
@@ -218,13 +289,16 @@ private Resource nodeToResource(Node node) {
/**
* Converts a Graph {@link Node} (predicate) to a Storage {@link IRI}.
- *
- * @param node the Graph predicate node to convert
- * @return the corresponding IRI
- * @throws IllegalArgumentException if the node is not a URI
*/
private IRI nodeToIRI(Node node) {
+ if (node == null) {
+ throw new IllegalArgumentException("Node cannot be null");
+ }
+
IDatatype dt = node.getDatatypeValue();
+ if (dt == null) {
+ throw new IllegalArgumentException("Node datatype is null: " + node);
+ }
if (!dt.isURI()) {
throw new IllegalArgumentException("Node is not an IRI: " + node);
@@ -235,32 +309,30 @@ private IRI nodeToIRI(Node node) {
/**
* Converts a Graph {@link Node} to a Storage {@link Value} (Resource or Literal).
- *
- * @param node the Graph node to convert
- * @return the corresponding Value (Resource or Literal)
- * @throws IllegalArgumentException if the node type is unknown
*/
private Value nodeToValue(Node node) {
+ if (node == null) {
+ throw new IllegalArgumentException("Node cannot be null");
+ }
+
IDatatype dt = node.getDatatypeValue();
+ if (dt == null) {
+ throw new IllegalArgumentException("Node datatype is null: " + node);
+ }
if (dt.isURI() || dt.isBlank() || dt.isTriple()) {
- // It's a Resource
return nodeToResource(node);
} else if (dt.isLiteral()) {
- // It's a Literal
String label = dt.getLabel();
String lang = dt.getLang();
String datatypeIRI = dt.getDatatypeURI();
if (lang != null && !lang.isEmpty()) {
- // Language-tagged string
return valueFactory.createLiteral(label, lang);
} else if (datatypeIRI != null && !datatypeIRI.isEmpty()) {
- // Typed literal
IRI datatype = valueFactory.createIRI(datatypeIRI);
return valueFactory.createLiteral(label, datatype);
} else {
- // Plain string (xsd:string)
return valueFactory.createLiteral(label);
}
} else {
@@ -280,17 +352,15 @@ private Edge statementToEdge(Statement stmt) {
Node object = valueToNode(stmt.getObject());
Resource ctxResource = stmt.getContext();
- Node context = (ctxResource != null) ? resourceToNode(ctxResource) : null;
+ Node context = (ctxResource != null)
+ ? resourceToNode(ctxResource)
+ : graph.getDefaultGraphNode();
- return graph.create(subject, predicate, object, context);
+ return graph.create(context, subject, predicate, object);
}
/**
* Converts a Storage {@link Resource} to a Graph {@link Node}.
- *
- * @param resource the Resource to convert (IRI or BNode)
- * @return the corresponding Graph Node
- * @throws IllegalArgumentException if the resource type is unknown
*/
private Node resourceToNode(Resource resource) {
if (resource.isIRI()) {
@@ -306,9 +376,6 @@ private Node resourceToNode(Resource resource) {
/**
* Converts a Storage {@link IRI} to a Graph {@link Node} (property).
- *
- * @param iri the IRI to convert (predicate)
- * @return the corresponding Graph Node
*/
private Node iriToNode(IRI iri) {
return graph.addProperty(iri.stringValue());
@@ -316,30 +383,21 @@ private Node iriToNode(IRI iri) {
/**
* Converts a Storage {@link Value} to a Graph {@link Node}.
- *
- * @param value the Value to convert (Resource or Literal)
- * @return the corresponding Graph Node
- * @throws IllegalArgumentException if the value type is unknown
*/
private Node valueToNode(Value value) {
if (value.isResource()) {
- // It's a Resource (IRI or BNode)
return resourceToNode((Resource) value);
} else if (value.isLiteral()) {
- // It's a Literal
Literal literal = (Literal) value;
String label = literal.getLabel();
if (literal.getLanguage().isPresent()) {
- // Language-tagged string
String lang = literal.getLanguage().get();
return graph.addLiteral(label, null, lang);
} else if (literal.getDatatype() != null) {
- // Typed literal
String datatypeIRI = literal.getDatatype().stringValue();
return graph.addLiteral(label, datatypeIRI);
} else {
- // Plain string
return graph.addLiteral(label);
}
} else {
diff --git a/src/main/java/fr/inria/corese/core/next/storagemanager/impl/graph/plugin/GraphStoragePlugin.java b/src/main/java/fr/inria/corese/core/next/storagemanager/impl/graph/plugin/GraphStoragePlugin.java
new file mode 100644
index 000000000..dd2e06614
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/storagemanager/impl/graph/plugin/GraphStoragePlugin.java
@@ -0,0 +1,64 @@
+package fr.inria.corese.core.next.storagemanager.impl.graph.plugin;
+
+import fr.inria.corese.core.Graph;
+import fr.inria.corese.core.next.data.api.ValueFactory;
+import fr.inria.corese.core.next.storagemanager.api.StorageManager;
+import fr.inria.corese.core.next.storagemanager.api.plugin.PluginException;
+import fr.inria.corese.core.next.storagemanager.api.plugin.StoragePlugin;
+import fr.inria.corese.core.next.storagemanager.api.support.config.StorageConfig;
+import fr.inria.corese.core.next.storagemanager.impl.graph.GraphStorageManager;
+
+/**
+ * Plugin for GraphStorageManager - wraps legacy Corese Graph backend.
+ */
+public class GraphStoragePlugin implements StoragePlugin {
+
+ @Override
+ public String getName() {
+ return "graph";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Legacy Corese Graph backend (production-ready, indexed, thread-safe)";
+ }
+
+ @Override
+ public boolean supports(StorageConfig config) {
+ if (config == null) {
+ return false;
+ }
+ return config.getType()
+ .map("graph"::equalsIgnoreCase)
+ .orElse(false);
+ }
+
+ @Override
+ public StorageManager create(StorageConfig config) throws PluginException {
+ try {
+ Graph graph = config.getProperty("graph", Graph.class)
+ .orElseThrow(() -> new PluginException("Graph instance required in config properties"));
+
+ ValueFactory factory = config.getProperty("valueFactory", ValueFactory.class)
+ .orElseThrow(() -> new PluginException("ValueFactory required in config properties"));
+
+ GraphStorageManager storage = GraphStorageManager.builder()
+ .graph(graph)
+ .valueFactory(factory)
+ .build();
+
+ storage.getLifecycle().initialize(config);
+
+ return storage;
+ } catch (PluginException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new PluginException("Failed to create GraphStorageManager", e);
+ }
+ }
+
+ @Override
+ public int getPriority() {
+ return 100;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/inria/corese/core/next/storagemanager/impl/memory/plugin/MemoryStoragePlugin.java b/src/main/java/fr/inria/corese/core/next/storagemanager/impl/memory/plugin/MemoryStoragePlugin.java
new file mode 100644
index 000000000..c7cc49e91
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/storagemanager/impl/memory/plugin/MemoryStoragePlugin.java
@@ -0,0 +1,58 @@
+package fr.inria.corese.core.next.storagemanager.impl.memory.plugin;
+
+import fr.inria.corese.core.next.storagemanager.api.StorageManager;
+import fr.inria.corese.core.next.storagemanager.api.plugin.PluginException;
+import fr.inria.corese.core.next.storagemanager.api.plugin.StoragePlugin;
+import fr.inria.corese.core.next.storagemanager.api.support.config.StorageConfig;
+import fr.inria.corese.core.next.storagemanager.impl.memory.MemoryStorageManager;
+
+/**
+ * Plugin for MemoryStorageManager
+ */
+public class MemoryStoragePlugin implements StoragePlugin {
+
+ @Override
+ public String getName() {
+ return "memory";
+ }
+
+ @Override
+ public String getDescription() {
+ return "In-memory HashMap backend (testing only, no persistence)";
+ }
+
+ @Override
+ public boolean supports(StorageConfig config) {
+ if (config == null) {
+ return false;
+ }
+ return config.getType()
+ .map("memory"::equalsIgnoreCase)
+ .orElse(false);
+ }
+
+ @Override
+ public StorageManager create(StorageConfig config) throws PluginException {
+ if (config == null) {
+ throw new IllegalArgumentException("StorageConfig must not be null");
+ }
+
+
+ try {
+ MemoryStorageManager storage = MemoryStorageManager.builder().build();
+
+ // Initialize lifecycle
+ storage.getLifecycle().initialize(config);
+
+ return storage;
+
+ } catch (Exception e) {
+ throw new PluginException("Failed to create MemoryStorageManager", e);
+ }
+ }
+
+ @Override
+ public int getPriority() {
+ return 50;
+ }
+}
diff --git a/src/main/resources/META-INF/services/fr.inria.corese.core.next.storagemanager.api.plugin.StoragePlugin b/src/main/resources/META-INF/services/fr.inria.corese.core.next.storagemanager.api.plugin.StoragePlugin
new file mode 100644
index 000000000..3cacc6690
--- /dev/null
+++ b/src/main/resources/META-INF/services/fr.inria.corese.core.next.storagemanager.api.plugin.StoragePlugin
@@ -0,0 +1,2 @@
+fr.inria.corese.core.next.storagemanager.impl.graph.plugin.GraphStoragePlugin
+fr.inria.corese.core.next.storagemanager.impl.memory.plugin.MemoryStoragePlugin
\ No newline at end of file
diff --git a/src/test/java/fr/inria/corese/core/next/data/factory/ModelFactoryTest.java b/src/test/java/fr/inria/corese/core/next/data/factory/ModelFactoryTest.java
new file mode 100644
index 000000000..df6087569
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/data/factory/ModelFactoryTest.java
@@ -0,0 +1,312 @@
+package fr.inria.corese.core.next.data.factory;
+
+import fr.inria.corese.core.next.data.api.IRI;
+import fr.inria.corese.core.next.data.api.Literal;
+import fr.inria.corese.core.next.data.api.Model;
+import fr.inria.corese.core.next.data.api.ValueFactory;
+import fr.inria.corese.core.next.data.impl.temp.CoreseAdaptedValueFactory;
+import fr.inria.corese.core.next.storagemanager.api.plugin.PluginException;
+import fr.inria.corese.core.next.storagemanager.api.plugin.PluginNotFoundException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for ModelFactory.
+ */
+@DisplayName("ModelFactory Tests")
+class ModelFactoryTest {
+
+ private ValueFactory valueFactory;
+ private ModelFactory factory;
+
+ // Test data
+ private IRI subject;
+ private IRI predicate;
+ private Literal object;
+
+ @BeforeEach
+ void setUp() {
+ valueFactory = new CoreseAdaptedValueFactory();
+ factory = new ModelFactory(valueFactory);
+
+ // Create test data
+ subject = valueFactory.createIRI("http://example.org/subject");
+ predicate = valueFactory.createIRI("http://example.org/predicate");
+ object = valueFactory.createLiteral("test value");
+ }
+
+ @Nested
+ @DisplayName("Constructor Tests")
+ class ConstructorTests {
+
+ @Test
+ @DisplayName("Should create factory with valid ValueFactory")
+ void shouldCreateFactoryWithValidValueFactory() {
+ ModelFactory factory = new ModelFactory(valueFactory);
+
+ assertNotNull(factory);
+ assertEquals(valueFactory, factory.valueFactory());
+ }
+
+ @Test
+ @DisplayName("Should throw NullPointerException when ValueFactory is null")
+ void shouldThrowExceptionWhenValueFactoryIsNull() {
+ NullPointerException exception = assertThrows(
+ NullPointerException.class,
+ () -> new ModelFactory(null)
+ );
+
+ assertEquals("ValueFactory cannot be null", exception.getMessage());
+ }
+ }
+
+ @Nested
+ @DisplayName("createModel(String) Tests")
+ class CreateModelByTypeTests {
+
+ @Test
+ @DisplayName("Should create memory model when type is 'memory'")
+ void shouldCreateMemoryModelWhenTypeIsMemory() throws PluginException {
+ Model model = factory.createModel("memory");
+
+ assertNotNull(model);
+ assertEquals(0, model.size());
+ assertTrue(model.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should throw IllegalArgumentException for unknown type")
+ void shouldThrowExceptionForUnknownType() {
+ PluginNotFoundException exception = assertThrows(
+ PluginNotFoundException.class,
+ () -> factory.createModel("unknown")
+ );
+
+ assertTrue(exception.getMessage().contains("No plugin found"));
+ assertTrue(exception.getMessage().contains("unknown"));
+ }
+
+
+ @Test
+ @DisplayName("Should throw IllegalArgumentException for empty type")
+ void shouldThrowExceptionForEmptyType() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> factory.createModel("")
+ );
+ }
+
+
+ }
+
+ @Nested
+ @DisplayName("createMemoryModel() Tests")
+ class CreateMemoryModelTests {
+
+ @Test
+ @DisplayName("Should create empty memory model")
+ void shouldCreateEmptyMemoryModel() throws PluginException {
+ Model model = factory.createMemoryModel();
+
+ assertNotNull(model);
+ assertEquals(0, model.size());
+ assertTrue(model.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should create functional memory model")
+ void shouldCreateFunctionalMemoryModel() throws PluginException {
+ Model model = factory.createMemoryModel();
+
+ // Add statement
+ model.add(subject, predicate, object);
+
+ assertEquals(1, model.size());
+ assertFalse(model.isEmpty());
+ assertTrue(model.contains(subject, predicate, object));
+ }
+
+ @Test
+ @DisplayName("Should create independent memory models")
+ void shouldCreateIndependentMemoryModels() throws PluginException {
+ Model model1 = factory.createMemoryModel();
+ Model model2 = factory.createMemoryModel();
+
+ model1.add(subject, predicate, object);
+
+ assertEquals(1, model1.size());
+ assertEquals(0, model2.size());
+ }
+
+
+ }
+
+ @Nested
+ @DisplayName("createGraphModel() Tests")
+ class CreateGraphModelTests {
+
+ @Test
+ @DisplayName("Should create empty graph model")
+ void shouldCreateEmptyGraphModel() throws PluginException {
+ Model model = factory.createGraphModel();
+
+ assertNotNull(model);
+ assertEquals(0, model.size());
+ assertTrue(model.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should create functional graph model")
+ void shouldCreateFunctionalGraphModel() throws PluginException {
+ Model model = factory.createGraphModel();
+
+ // Add statement
+ model.add(subject, predicate, object);
+
+ assertEquals(1, model.size());
+ assertFalse(model.isEmpty());
+ assertTrue(model.contains(subject, predicate, object));
+ }
+
+ @Test
+ @DisplayName("Should create independent graph models")
+ void shouldCreateIndependentGraphModels() throws PluginException {
+ Model model1 = factory.createGraphModel();
+ Model model2 = factory.createGraphModel();
+
+ model1.add(subject, predicate, object);
+
+ assertEquals(1, model1.size());
+ assertEquals(0, model2.size());
+ }
+
+ @Test
+ @DisplayName("Graph model should support multiple statements")
+ void graphModelShouldSupportMultipleStatements() throws PluginException {
+ Model model = factory.createGraphModel();
+
+ IRI subject2 = valueFactory.createIRI("http://example.org/subject2");
+ Literal object2 = valueFactory.createLiteral("test value 2");
+
+ model.add(subject, predicate, object);
+ model.add(subject2, predicate, object2);
+
+ assertTrue(model.contains(subject, predicate, object));
+ assertTrue(model.contains(subject2, predicate, object2));
+ }
+ }
+
+
+ @Nested
+ @DisplayName("Storage Type Comparison Tests")
+ class StorageTypeComparisonTests {
+
+ @Test
+ @DisplayName("Memory and Graph models should be functionally equivalent")
+ void memoryAndGraphModelsShouldBeFunctionallyEquivalent() throws PluginException {
+ Model memoryModel = factory.createMemoryModel();
+ Model graphModel = factory.createGraphModel();
+
+ // Add same data to both
+ memoryModel.add(subject, predicate, object);
+ graphModel.add(subject, predicate, object);
+
+ // Both should behave the same
+ assertEquals(1, memoryModel.size());
+ assertEquals(1, graphModel.size());
+ assertTrue(memoryModel.contains(subject, predicate, object));
+ assertTrue(graphModel.contains(subject, predicate, object));
+ }
+
+ @Test
+ @DisplayName("createModel should produce same behavior as specialized methods")
+ void createModelShouldProduceSameBehaviorAsSpecializedMethods() throws PluginException {
+ Model memoryModel1 = factory.createModel("memory");
+ Model memoryModel2 = factory.createMemoryModel();
+
+
+ // Add data
+ memoryModel1.add(subject, predicate, object);
+ memoryModel2.add(subject, predicate, object);
+ // All should behave the same
+ assertEquals(memoryModel1.size(), memoryModel2.size());
+ }
+ }
+
+
+ @Nested
+ @DisplayName("Edge Cases and Error Handling Tests")
+ class EdgeCasesTests {
+
+
+ @Test
+ @DisplayName("Should handle empty string as storage type")
+ void shouldHandleEmptyStringAsStorageType() {
+ IllegalArgumentException exception = assertThrows(
+ IllegalArgumentException.class,
+ () -> factory.createModel("")
+ );
+
+ assertTrue(exception.getMessage().contains("cannot be null or empty"));
+ }
+
+ @Test
+ @DisplayName("Should handle whitespace-only string as storage type")
+ void shouldHandleWhitespaceOnlyStringAsStorageType() {
+ IllegalArgumentException exception = assertThrows(
+ IllegalArgumentException.class,
+ () -> factory.createModel(" ")
+ );
+
+ assertTrue(exception.getMessage().contains("cannot be null or empty"));
+ }
+ }
+
+ @Nested
+ @DisplayName("Integration Tests")
+ class IntegrationTests {
+
+ @Test
+ @DisplayName("Complete workflow: create, populate, query")
+ void completeWorkflowCreatePopulateQuery() throws PluginException {
+ // Create model
+ Model model = factory.createMemoryModel();
+
+ // Populate
+ IRI alice = valueFactory.createIRI("http://example.org/Alice");
+ IRI bob = valueFactory.createIRI("http://example.org/Bob");
+ IRI knows = valueFactory.createIRI("http://xmlns.com/foaf/0.1/knows");
+
+ model.add(alice, knows, bob);
+
+ // Query
+ assertTrue(model.contains(alice, knows, bob));
+ assertEquals(1, model.size());
+ assertFalse(model.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should work with different ValueFactory implementations")
+ void shouldWorkWithDifferentValueFactoryImplementations() throws PluginException {
+ // This test assumes CoreseAdaptedValueFactory works
+ ValueFactory vf = new CoreseAdaptedValueFactory();
+ ModelFactory factory = new ModelFactory(vf);
+
+ Model model = factory.createMemoryModel();
+ assertNotNull(model);
+
+ IRI testIRI = vf.createIRI("http://test.org/resource");
+ Literal testLiteral = vf.createLiteral("value");
+
+ assertDoesNotThrow(() -> {
+ model.add(testIRI, predicate, testLiteral);
+ });
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/fr/inria/corese/core/next/data/impl/io/serialization/turtle/TurtleSerializerTest.java b/src/test/java/fr/inria/corese/core/next/data/impl/io/serialization/turtle/TurtleSerializerTest.java
index cf3cff000..015b9ed05 100644
--- a/src/test/java/fr/inria/corese/core/next/data/impl/io/serialization/turtle/TurtleSerializerTest.java
+++ b/src/test/java/fr/inria/corese/core/next/data/impl/io/serialization/turtle/TurtleSerializerTest.java
@@ -344,7 +344,8 @@ void testBlankNodeSerializarionWithoutId() {
turtleSerializer.write(writer);
String actual = writer.toString().replace("\r\n", "\n");
- System.out.println(actual);
+ logger.debug("Serialized Turtle output:\n{}", actual);
+
}
/**
diff --git a/src/test/java/fr/inria/corese/core/next/storagemanager/api/plugin/StoragePluginManagerTest.java b/src/test/java/fr/inria/corese/core/next/storagemanager/api/plugin/StoragePluginManagerTest.java
new file mode 100644
index 000000000..4de35b3f2
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/storagemanager/api/plugin/StoragePluginManagerTest.java
@@ -0,0 +1,48 @@
+package fr.inria.corese.core.next.storagemanager.api.plugin;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests for StoragePluginManager.
+ */
+class StoragePluginManagerTest {
+
+ @BeforeEach
+ void setUp() {
+ StoragePluginManager.reload();
+ }
+
+ @Test
+ @DisplayName("Should discover available plugins")
+ void shouldDiscoverAvailablePlugins() {
+ List plugins = StoragePluginManager.getAvailablePlugins();
+
+ assertNotNull(plugins);
+ assertTrue(plugins.size() >= 2);
+ }
+
+ @Test
+ @DisplayName("Should find Graph plugin")
+ void shouldFindGraphPlugin() {
+ Optional plugin = StoragePluginManager.findPlugin("graph");
+
+ assertTrue(plugin.isPresent());
+ assertEquals("graph", plugin.get().getName());
+ }
+
+ @Test
+ @DisplayName("Should find Memory plugin")
+ void shouldFindMemoryPlugin() {
+ Optional plugin = StoragePluginManager.findPlugin("memory");
+
+ assertTrue(plugin.isPresent());
+ assertEquals("memory", plugin.get().getName());
+ }
+}
diff --git a/src/test/java/fr/inria/corese/core/next/storagemanager/api/plugin/TestExternalPlugin.java b/src/test/java/fr/inria/corese/core/next/storagemanager/api/plugin/TestExternalPlugin.java
new file mode 100644
index 000000000..668ce9dbd
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/storagemanager/api/plugin/TestExternalPlugin.java
@@ -0,0 +1,155 @@
+package fr.inria.corese.core.next.storagemanager.api.plugin;
+
+import fr.inria.corese.core.next.data.api.IRI;
+import fr.inria.corese.core.next.data.api.Statement;
+import fr.inria.corese.core.next.data.api.ValueFactory;
+import fr.inria.corese.core.next.data.impl.temp.CoreseAdaptedValueFactory;
+import fr.inria.corese.core.next.storagemanager.api.StorageManager;
+import fr.inria.corese.core.next.storagemanager.api.support.config.StorageConfig;
+import fr.inria.corese.core.next.storagemanager.api.support.model.StatementPattern;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.net.URL;
+
+/**
+ * Test class for loading and using external plugins from resources.
+ *
+ */
+public class TestExternalPlugin {
+
+ private static final Logger logger = LoggerFactory.getLogger(TestExternalPlugin.class);
+
+ /**
+ * Path to the plugin JAR file in test resources.
+ */
+ private static final String PLUGIN_JAR_PATH = "storage-plugin/demo-storage-plugin-1.0.0.jar";
+
+ /**
+ * Main entry point for the test.
+ *
+ * @param args command line arguments (not used)
+ */
+ public static void main(String[] args) {
+ logger.info("=== External Plugin Test (from resources) ===");
+
+ try {
+ // Load the external plugin from resources
+ loadExternalPlugin();
+
+ // Test the plugin using StoragePluginManager
+ testDemoPlugin();
+
+ logger.info("TEST PASSED");
+
+ } catch (Exception e) {
+ logger.error("TEST FAILED", e);
+ System.exit(1);
+ }
+ }
+
+ /**
+ * Loads the demo plugin from the test resources directory.
+ *
+ * This method locates the JAR file in {@code src/test/resources/storage-plugin/}
+ * and loads it using {@link ExternalPluginLoader}.
+ *
+ * @throws Exception if loading fails
+ */
+ private static void loadExternalPlugin() throws Exception {
+ logger.info("Loading plugin from resources: {}", PLUGIN_JAR_PATH);
+
+ // Get the JAR file from resources
+ File jarFile = getResourceFile();
+
+ logger.info("Found JAR: {} ({} bytes)", jarFile.getName(), jarFile.length());
+
+ // Load using ExternalPluginLoader utility
+ int count = ExternalPluginLoader.loadPluginsFromJar(jarFile);
+
+ logger.info("Loaded {} plugin(s) from JAR", count);
+
+ if (count == 0) {
+ throw new IllegalStateException("No plugins found in JAR: " + jarFile.getName());
+ }
+ }
+
+ /**
+ * Gets a file from the test resources directory.
+ *
+ * @return the File object
+ * @throws IllegalStateException if the resource is not found
+ */
+ private static File getResourceFile() {
+ URL resourceUrl = TestExternalPlugin.class.getClassLoader().getResource(TestExternalPlugin.PLUGIN_JAR_PATH);
+
+ if (resourceUrl == null) {
+ throw new IllegalStateException(
+ "Resource not found: " + TestExternalPlugin.PLUGIN_JAR_PATH + "\n" +
+ "Expected location: src/test/resources/" + TestExternalPlugin.PLUGIN_JAR_PATH
+ );
+ }
+
+ try {
+ return new File(resourceUrl.toURI());
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to load resource: " + TestExternalPlugin.PLUGIN_JAR_PATH, e);
+ }
+ }
+
+ /**
+ * Tests the demo plugin with basic operations.
+ *
+ * @throws Exception if any operation fails
+ */
+ private static void testDemoPlugin() throws Exception {
+ logger.info("Starting plugin tests");
+
+ // 1. Create StorageConfig with type="demo"
+ StorageConfig config = StorageConfig.builder()
+ .property("type", "demo")
+ .build();
+
+ // 2. Create StorageManager via StoragePluginManager
+ StorageManager storage = StoragePluginManager.create(config);
+ logger.info("StorageManager created via StoragePluginManager");
+
+ // 3. Create test data
+ ValueFactory vf = new CoreseAdaptedValueFactory();
+ IRI alice = vf.createIRI("http://example.org/Alice");
+ IRI knows = vf.createIRI("http://xmlns.com/foaf/0.1/knows");
+ IRI bob = vf.createIRI("http://example.org/Bob");
+ Statement stmt = vf.createStatement(alice, knows, bob);
+
+ // 4. Insert statement
+ storage.getMutationOperations().insertStatement(stmt);
+ logger.info("Statement inserted");
+
+ // 5. Count statements
+ long count = storage.getQueryOperations().count(
+ StatementPattern.builder().build()
+ );
+ logger.info("Count: {} statement(s)", count);
+
+ // 6. Check if statement exists
+ boolean exists = storage.getQueryOperations().exists(
+ StatementPattern.builder().subject(alice).build()
+ );
+ logger.info("Exists (alice): {}", exists);
+
+ // 7. Verify results
+ if (count != 1) {
+ throw new AssertionError("Expected count=1, got count=" + count);
+ }
+ if (!exists) {
+ throw new AssertionError("Expected exists=true, got exists=false");
+ }
+
+ // 8. Shutdown the storage
+ storage.getLifecycle().shutdown();
+ logger.info("StorageManager shut down");
+
+ logger.info("Tests completed successfully");
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/log4j2-test.xml b/src/test/resources/log4j2-test.xml
index c6d371920..cd9ccb1d6 100644
--- a/src/test/resources/log4j2-test.xml
+++ b/src/test/resources/log4j2-test.xml
@@ -31,6 +31,7 @@
+
diff --git a/src/test/resources/storage-plugin/demo-storage-plugin-1.0.0.jar b/src/test/resources/storage-plugin/demo-storage-plugin-1.0.0.jar
new file mode 100644
index 000000000..433ea8c3b
Binary files /dev/null and b/src/test/resources/storage-plugin/demo-storage-plugin-1.0.0.jar differ