From 2bd9d797423496886c5b5f87b7b87ab4cc0dd3e3 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Tue, 3 Mar 2026 10:11:28 +0100 Subject: [PATCH 1/8] Plugin system to choose StorageManager implementation #281 --- .../api/plugin/PluginException.java | 18 ++ .../api/plugin/PluginNotFoundException.java | 11 ++ .../api/plugin/StoragePlugin.java | 48 +++++ .../api/plugin/StoragePluginManager.java | 175 ++++++++++++++++++ .../api/support/config/StorageConfig.java | 38 +++- .../api/support/exception/ErrorCode.java | 8 +- .../impl/graph/plugin/GraphStoragePlugin.java | 55 ++++++ .../memory/plugin/MemoryStoragePlugin.java | 50 +++++ ...xt.storagemanager.api.plugin.StoragePlugin | 2 + .../api/plugin/StoragePluginManagerTest.java | 48 +++++ 10 files changed, 447 insertions(+), 6 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/PluginException.java create mode 100644 src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/PluginNotFoundException.java create mode 100644 src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/StoragePlugin.java create mode 100644 src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/StoragePluginManager.java create mode 100644 src/main/java/fr/inria/corese/core/next/storagemanager/impl/graph/plugin/GraphStoragePlugin.java create mode 100644 src/main/java/fr/inria/corese/core/next/storagemanager/impl/memory/plugin/MemoryStoragePlugin.java create mode 100644 src/main/resources/META-INF/services/fr.inria.corese.core.next.storagemanager.api.plugin.StoragePlugin create mode 100644 src/test/java/fr/inria/corese/core/next/storagemanager/api/plugin/StoragePluginManagerTest.java 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..d4aa28de7 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/StoragePlugin.java @@ -0,0 +1,48 @@ +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(); + } + + + + /** + * 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. + * + * @return the plugin priority + */ + default int getPriority() { + return 0; + } + +} 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..d383029ef --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/StoragePluginManager.java @@ -0,0 +1,175 @@ +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 java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * Manager for discovering and creating StorageManager plugins. + */ +public class StoragePluginManager { + + /** + * ServiceLoader for discovering plugins + */ + private static final ServiceLoader serviceLoader = + ServiceLoader.load(StoragePlugin.class); + + /** + * 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() + .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(), availableTypes) + ); + } + + // Select plugin with highest priority + StoragePlugin selectedPlugin = supportingPlugins.getFirst(); + + // Log warning if multiple plugins match + if (supportingPlugins.size() > 1) { + String otherPlugins = supportingPlugins.stream() + .skip(1) + .map(p -> p.getName() + " (priority=" + p.getPriority() + ")") + .collect(Collectors.joining(", ")); + + System.out.println("WARNING: Multiple plugins support this configuration. " + + "Selected '" + selectedPlugin.getName() + "' (priority=" + + selectedPlugin.getPriority() + "). Ignored: " + otherPlugins); + } + + // Create StorageManager + 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) { + List plugins = new ArrayList<>(); + + // Reload ServiceLoader to discover new plugins + serviceLoader.reload(); + + // Collect all plugins + for (StoragePlugin plugin : serviceLoader) { + plugins.add(plugin); + pluginsByName.put(plugin.getName(), plugin); + } + + // Sort by priority (highest first) + plugins.sort(Comparator.comparingInt(StoragePlugin::getPriority).reversed()); + + // Make immutable + cachedPlugins = Collections.unmodifiableList(plugins); + } + } + } + + return cachedPlugins; + } + + /** + * 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)); + } + + + /** + * Returns the names of all available plugins. + * + * @return unmodifiable set of plugin names (never null, may be empty) + */ + public static Set getPluginNames() { + return getAvailablePlugins().stream() + .map(StoragePlugin::getName) + .collect(Collectors.collectingAndThen( + Collectors.toSet(), + Collections::unmodifiableSet + )); + } + + /** + * Reloads all plugins from the classpath. + */ + public static void reload() { + synchronized (StoragePluginManager.class) { + cachedPlugins = null; + pluginsByName.clear(); + } + } + + +} 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..c8ebfbeaa 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 isTransactionSupport() { + 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/plugin/GraphStoragePlugin.java b/src/main/java/fr/inria/corese/core/next/storagemanager/impl/graph/plugin/GraphStoragePlugin.java new file mode 100644 index 000000000..141078855 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/storagemanager/impl/graph/plugin/GraphStoragePlugin.java @@ -0,0 +1,55 @@ +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 StorageManager create(StorageConfig config) throws PluginException { + try { + Graph graph = config.getProperty("graph", Graph.class) + .orElseThrow(() -> new PluginException("Graph instance required")); + + ValueFactory factory = config.getProperty("valueFactory", ValueFactory.class) + .orElseThrow(() -> new PluginException("ValueFactory required")); + + 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; + } +} 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..36f11b869 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/storagemanager/impl/memory/plugin/MemoryStoragePlugin.java @@ -0,0 +1,50 @@ +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 StorageManager create(StorageConfig config) throws PluginException { + if (config == null) { + throw new IllegalArgumentException("StorageConfig must not be null"); + } + + + try { + // Build MemoryStorageManager (no dependencies required) + 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/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()); + } +} From 9183d873e38309d0d457fd38beb9c0c048bf8b67 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Tue, 3 Mar 2026 15:25:25 +0100 Subject: [PATCH 2/8] Plugin system to choose StorageManager implementation #281 --- .../api/plugin/StoragePlugin.java | 13 ++++++++--- .../api/plugin/StoragePluginManager.java | 22 ++++++++++--------- .../impl/graph/plugin/GraphStoragePlugin.java | 15 ++++++++++--- .../memory/plugin/MemoryStoragePlugin.java | 10 ++++++++- .../turtle/TurtleSerializerTest.java | 3 ++- 5 files changed, 45 insertions(+), 18 deletions(-) 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 index d4aa28de7..a452de387 100644 --- 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 @@ -24,7 +24,15 @@ 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. @@ -44,5 +52,4 @@ default String getDescription() { 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 index d383029ef..bec7a28fb 100644 --- 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 @@ -2,6 +2,8 @@ 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; @@ -12,6 +14,8 @@ */ public class StoragePluginManager { + private static final Logger logger = LoggerFactory.getLogger(StoragePluginManager.class); + /** * ServiceLoader for discovering plugins */ @@ -55,6 +59,7 @@ public static StorageManager create(StorageConfig config) throws PluginException // Find plugins that support this configuration List supportingPlugins = allPlugins.stream() + .filter(plugin -> plugin.supports(config)) .sorted(Comparator.comparingInt(StoragePlugin::getPriority).reversed()) .toList(); @@ -65,26 +70,26 @@ public static StorageManager create(StorageConfig config) throws PluginException throw new PluginNotFoundException( String.format("No plugin found for storage type '%s'. Available types: [%s]", - config.getType(), availableTypes) + config.getType().orElse("not specified"), availableTypes) ); } // Select plugin with highest priority StoragePlugin selectedPlugin = supportingPlugins.getFirst(); - // Log warning if multiple plugins match if (supportingPlugins.size() > 1) { String otherPlugins = supportingPlugins.stream() .skip(1) .map(p -> p.getName() + " (priority=" + p.getPriority() + ")") .collect(Collectors.joining(", ")); - System.out.println("WARNING: Multiple plugins support this configuration. " + - "Selected '" + selectedPlugin.getName() + "' (priority=" + - selectedPlugin.getPriority() + "). Ignored: " + otherPlugins); + logger.warn("Multiple plugins support this configuration. " + + "Selected '{}' (priority={}). Ignored: {}", + selectedPlugin.getName(), + selectedPlugin.getPriority(), + otherPlugins); } - // Create StorageManager try { return selectedPlugin.create(config); } catch (PluginException e) { @@ -146,7 +151,6 @@ public static Optional findPlugin(String name) { return Optional.ofNullable(pluginsByName.get(name)); } - /** * Returns the names of all available plugins. * @@ -170,6 +174,4 @@ public static void reload() { pluginsByName.clear(); } } - - -} +} \ No newline at end of file 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 index 141078855..dd2e06614 100644 --- 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 @@ -23,15 +23,24 @@ 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")); + .orElseThrow(() -> new PluginException("Graph instance required in config properties")); ValueFactory factory = config.getProperty("valueFactory", ValueFactory.class) - .orElseThrow(() -> new PluginException("ValueFactory required")); + .orElseThrow(() -> new PluginException("ValueFactory required in config properties")); GraphStorageManager storage = GraphStorageManager.builder() .graph(graph) @@ -52,4 +61,4 @@ public StorageManager create(StorageConfig config) throws PluginException { 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 index 36f11b869..c7cc49e91 100644 --- 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 @@ -21,6 +21,15 @@ 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 { @@ -30,7 +39,6 @@ public StorageManager create(StorageConfig config) throws PluginException { try { - // Build MemoryStorageManager (no dependencies required) MemoryStorageManager storage = MemoryStorageManager.builder().build(); // Initialize lifecycle 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); + } /** From 5395d781fb07c85fbcebd2c5ad5c514b3b8b6197 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Mon, 9 Mar 2026 13:47:58 +0100 Subject: [PATCH 3/8] revue Plugin system to choose StorageManager implementation --- .../api/plugin/StoragePlugin.java | 27 ++++++++++++++++++- .../api/support/config/StorageConfig.java | 2 +- 2 files changed, 27 insertions(+), 2 deletions(-) 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 index a452de387..bda07f7c5 100644 --- 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 @@ -47,7 +47,32 @@ default String getDescription() { /** * Returns the priority of this plugin. * - * @return the plugin priority + *

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; 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 c8ebfbeaa..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 @@ -67,7 +67,7 @@ public Map getProperties() { * * @return {@code true} if transaction support is enabled */ - public boolean isTransactionSupport() { + public boolean hasTransactionSupport() { return transactionSupport; } From 8134ce26adc1043904b7c51fd823dee8f410de27 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Mon, 9 Mar 2026 15:06:29 +0100 Subject: [PATCH 4/8] revue Plugin system to choose StorageManager implementation --- .../core/next/data/factory/ModelFactory.java | 176 ++++++++++ .../impl/graph/GraphAdapter.java | 147 +++++--- .../next/data/factory/ModelFactoryTest.java | 332 ++++++++++++++++++ 3 files changed, 606 insertions(+), 49 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/data/factory/ModelFactory.java create mode 100644 src/test/java/fr/inria/corese/core/next/data/factory/ModelFactoryTest.java 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..15f7e2d9d --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/data/factory/ModelFactory.java @@ -0,0 +1,176 @@ +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 class ModelFactory { + + private final 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(ValueFactory valueFactory) { + if (valueFactory == null) { + throw new NullPointerException("ValueFactory cannot be null"); + } + this.valueFactory = valueFactory; + } + + /** + * Creates a new Model instance with the specified storage backend type. + * + *

This method provides a convenient way to create models without manually + * configuring the StorageManager and StorageConfig. The storage backend is + * selected based on the provided type string. + * + * @param storageType the storage backend type ("memory" or "graph") + * @return a new Model instance backed by the specified storage type + * @throws PluginException if the memory storage fails to initialize + */ + public Model createModel(String storageType) throws PluginException { + StorageConfig config = switch (storageType) { + case "memory" -> createMemoryConfig(); + case "graph" -> createGraphConfig(); + default -> throw new IllegalArgumentException( + "Unknown storage type: '" + storageType + "'. " + + "Supported types: [memory, graph]"); + }; + + 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() + */ + public Model createMemoryModel() throws PluginException { + StorageConfig config = createMemoryConfig(); + + return StorageModel.builder() + .storage(StoragePluginManager.create(config)) + .valueFactory(valueFactory) + .build(); + } + + /** + * 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() + */ + public Model createGraphModel() throws PluginException { + StorageConfig config = createGraphConfig(); + + return StorageModel.builder() + .storage(StoragePluginManager.create(config)) + .valueFactory(valueFactory) + .build(); + } + + /** + * 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) + .build(); + + return StorageModel.builder() + .storage(StoragePluginManager.create(config)) + .valueFactory(valueFactory) + .build(); + } + + /** + * Returns the ValueFactory used by this factory. + * + *

The ValueFactory is used to create RDF values (IRIs, Literals, BNodes) + * for all models created by this factory. + * + * @return the ValueFactory instance + */ + public ValueFactory getValueFactory() { + return valueFactory; + } + + /** + * 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/impl/graph/GraphAdapter.java b/src/main/java/fr/inria/corese/core/next/storagemanager/impl/graph/GraphAdapter.java index 90dcafd2d..46a4198af 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 @@ -4,6 +4,7 @@ import fr.inria.corese.core.kgram.api.core.Edge; import fr.inria.corese.core.kgram.api.core.Node; import fr.inria.corese.core.next.data.api.*; +import fr.inria.corese.core.next.storagemanager.api.plugin.PluginException; import fr.inria.corese.core.sparql.api.IDatatype; import java.util.Arrays; @@ -32,8 +33,24 @@ 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) : null; + + // Use Graph.addTriple() or Graph.add() instead of create() + addEdge() + // This bypasses the Edge creation issue + Edge edge; + if (context == null) { + edge = graph.addEdge(subject, predicate, object); + } else { + edge = graph.addEdge(subject, predicate, object, context); + } + + return edge != null; } /** @@ -78,14 +95,20 @@ public Set find(Resource s, IRI p, Value o, Resource[] contexts) { edges = graph.getEdgesRDF4J(subject, predicate, object); } else { Node[] ctxNodes = Arrays.stream(contexts) - .map(this::resourceToNode) + .map(ctx -> ctx != null ? resourceToNode(ctx) : null) .toArray(Node[]::new); 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 +147,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 +166,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 +185,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,7 +204,13 @@ 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; } @@ -171,16 +218,32 @@ public Set getContexts() { /** * Converts a Graph {@link Edge} to a Storage {@link Statement}. * - *

Extracts subject, predicate, object and optional context from the Edge - * and creates the corresponding Statement using the ValueFactory.

- * * @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(); @@ -195,21 +258,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 +282,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 +302,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 { @@ -287,10 +352,6 @@ private Edge statementToEdge(Statement stmt) { /** * 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 +367,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 +374,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/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..dbac6ccfb --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/data/factory/ModelFactoryTest.java @@ -0,0 +1,332 @@ +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 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.getValueFactory()); + } + + @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() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> factory.createModel("unknown") + ); + + assertTrue(exception.getMessage().contains("Unknown storage type")); + assertTrue(exception.getMessage().contains("unknown")); + assertTrue(exception.getMessage().contains("memory, graph")); + } + + + @Test + @DisplayName("Should throw IllegalArgumentException for empty type") + void shouldThrowExceptionForEmptyType() { + assertThrows( + IllegalArgumentException.class, + () -> factory.createModel("") + ); + } + + @Test + @DisplayName("Should be case-sensitive for storage type") + void shouldBeCaseSensitiveForStorageType() { + // "MEMORY" should fail (case-sensitive) + assertThrows( + IllegalArgumentException.class, + () -> factory.createModel("MEMORY") + ); + + // "Graph" should fail (case-sensitive) + assertThrows( + IllegalArgumentException.class, + () -> factory.createModel("Graph") + ); + } + } + + @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(); + + Model graphModel1 = factory.createModel("graph"); + Model graphModel2 = factory.createGraphModel(); + + // Add data + memoryModel1.add(subject, predicate, object); + memoryModel2.add(subject, predicate, object); + graphModel1.add(subject, predicate, object); + graphModel2.add(subject, predicate, object); + + // All should behave the same + assertEquals(memoryModel1.size(), memoryModel2.size()); + assertEquals(graphModel1.size(), graphModel2.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("Unknown storage type")); + } + + @Test + @DisplayName("Should handle whitespace-only string as storage type") + void shouldHandleWhitespaceOnlyStringAsStorageType() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> factory.createModel(" ") + ); + + assertTrue(exception.getMessage().contains("Unknown storage type")); + } + } + + @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 From 047d03ba4524012c5e60121e556bf26eda97b92d Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Tue, 10 Mar 2026 09:36:06 +0100 Subject: [PATCH 5/8] revue Plugin system to choose StorageManager implementation --- .../core/next/data/factory/ModelFactory.java | 15 +++++++-------- .../core/next/data/factory/ModelFactoryTest.java | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) 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 index 15f7e2d9d..e3f1fde93 100644 --- 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 @@ -26,9 +26,7 @@ * @see StoragePluginManager * @see ValueFactory */ -public class ModelFactory { - - private final ValueFactory valueFactory; +public record ModelFactory(ValueFactory valueFactory) { /** * Constructs a new ModelFactory with the specified ValueFactory. @@ -36,11 +34,10 @@ public class ModelFactory { * @param valueFactory the ValueFactory to use for creating RDF values * @throws NullPointerException if valueFactory is null */ - public ModelFactory(ValueFactory valueFactory) { + public ModelFactory { if (valueFactory == null) { throw new NullPointerException("ValueFactory cannot be null"); } - this.valueFactory = valueFactory; } /** @@ -118,8 +115,8 @@ public Model createGraphModel() throws PluginException { * * @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 + * @throws NullPointerException if graph is null + * @throws PluginException if the graph storage fails to initialize * @see #createGraphModel() */ public Model createGraphModel(Graph graph) throws PluginException { @@ -130,6 +127,7 @@ public Model createGraphModel(Graph graph) throws PluginException { StorageConfig config = StorageConfig.builder() .property("type", "graph") .property("graph", graph) + .property("valueFactory", valueFactory) .build(); return StorageModel.builder() @@ -146,7 +144,8 @@ public Model createGraphModel(Graph graph) throws PluginException { * * @return the ValueFactory instance */ - public ValueFactory getValueFactory() { + @Override + public ValueFactory valueFactory() { return valueFactory; } 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 index dbac6ccfb..423ec6d2b 100644 --- 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 @@ -48,7 +48,7 @@ void shouldCreateFactoryWithValidValueFactory() { ModelFactory factory = new ModelFactory(valueFactory); assertNotNull(factory); - assertEquals(valueFactory, factory.getValueFactory()); + assertEquals(valueFactory, factory.valueFactory()); } @Test From 70c3f4b8f8dd521aa05d613dcdaf27269676f647 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Tue, 17 Mar 2026 07:31:25 +0100 Subject: [PATCH 6/8] Plugin system to choose StorageManager implementation --- .../core/next/data/factory/ModelFactory.java | 83 +++++----- .../api/plugin/ExternalPluginLoader.java | 95 +++++++++++ .../api/plugin/StoragePluginManager.java | 75 ++++++--- .../impl/graph/GraphAdapter.java | 25 ++- .../next/data/factory/ModelFactoryTest.java | 32 +--- .../api/plugin/TestExternalPlugin.java | 155 ++++++++++++++++++ src/test/resources/log4j2-test.xml | 1 + .../demo-storage-plugin-1.0.0.jar | Bin 0 -> 12866 bytes 8 files changed, 365 insertions(+), 101 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/ExternalPluginLoader.java create mode 100644 src/test/java/fr/inria/corese/core/next/storagemanager/api/plugin/TestExternalPlugin.java create mode 100644 src/test/resources/storage-plugin/demo-storage-plugin-1.0.0.jar 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 index e3f1fde93..98453706c 100644 --- 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 @@ -43,22 +43,43 @@ public record ModelFactory(ValueFactory valueFactory) { /** * Creates a new Model instance with the specified storage backend type. * - *

This method provides a convenient way to create models without manually - * configuring the StorageManager and StorageConfig. The storage backend is - * selected based on the provided type string. - * - * @param storageType the storage backend type ("memory" or "graph") + * @param storageType the storage backend type (case-insensitive) * @return a new Model instance backed by the specified storage type - * @throws PluginException if the memory storage fails to initialize + * @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 { - StorageConfig config = switch (storageType) { - case "memory" -> createMemoryConfig(); - case "graph" -> createGraphConfig(); - default -> throw new IllegalArgumentException( - "Unknown storage type: '" + storageType + "'. " + - "Supported types: [memory, graph]"); - }; + 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)) @@ -75,14 +96,10 @@ public Model createModel(String storageType) throws PluginException { * @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 { - StorageConfig config = createMemoryConfig(); - - return StorageModel.builder() - .storage(StoragePluginManager.create(config)) - .valueFactory(valueFactory) - .build(); + return createModel(createMemoryConfig()); } /** @@ -96,14 +113,10 @@ public Model createMemoryModel() throws PluginException { * @throws PluginException if the graph storage fails to initialize * @see #createGraphModel(Graph) * @see #createMemoryModel() + * @see #createModel(String) */ public Model createGraphModel() throws PluginException { - StorageConfig config = createGraphConfig(); - - return StorageModel.builder() - .storage(StoragePluginManager.create(config)) - .valueFactory(valueFactory) - .build(); + return createModel(createGraphConfig()); } /** @@ -116,7 +129,7 @@ public Model createGraphModel() throws PluginException { * @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 + * @throws PluginException if the graph storage fails to initialize * @see #createGraphModel() */ public Model createGraphModel(Graph graph) throws PluginException { @@ -130,23 +143,7 @@ public Model createGraphModel(Graph graph) throws PluginException { .property("valueFactory", valueFactory) .build(); - return StorageModel.builder() - .storage(StoragePluginManager.create(config)) - .valueFactory(valueFactory) - .build(); - } - - /** - * Returns the ValueFactory used by this factory. - * - *

The ValueFactory is used to create RDF values (IRIs, Literals, BNodes) - * for all models created by this factory. - * - * @return the ValueFactory instance - */ - @Override - public ValueFactory valueFactory() { - return valueFactory; + return createModel(config); } /** 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/StoragePluginManager.java b/src/main/java/fr/inria/corese/core/next/storagemanager/api/plugin/StoragePluginManager.java index bec7a28fb..9f7b092d7 100644 --- 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 @@ -19,8 +19,12 @@ public class StoragePluginManager { /** * ServiceLoader for discovering plugins */ - private static final ServiceLoader serviceLoader = - ServiceLoader.load(StoragePlugin.class); + 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) @@ -110,22 +114,38 @@ public static List getAvailablePlugins() { if (cachedPlugins == null) { synchronized (StoragePluginManager.class) { if (cachedPlugins == null) { - List plugins = new ArrayList<>(); - - // Reload ServiceLoader to discover new plugins - serviceLoader.reload(); - - // Collect all plugins - for (StoragePlugin plugin : serviceLoader) { - plugins.add(plugin); - pluginsByName.put(plugin.getName(), plugin); + 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(", "))); } } } @@ -133,6 +153,25 @@ public static List getAvailablePlugins() { 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. * @@ -151,19 +190,6 @@ public static Optional findPlugin(String name) { return Optional.ofNullable(pluginsByName.get(name)); } - /** - * Returns the names of all available plugins. - * - * @return unmodifiable set of plugin names (never null, may be empty) - */ - public static Set getPluginNames() { - return getAvailablePlugins().stream() - .map(StoragePlugin::getName) - .collect(Collectors.collectingAndThen( - Collectors.toSet(), - Collections::unmodifiableSet - )); - } /** * Reloads all plugins from the classpath. @@ -172,6 +198,7 @@ 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/impl/graph/GraphAdapter.java b/src/main/java/fr/inria/corese/core/next/storagemanager/impl/graph/GraphAdapter.java index 46a4198af..32e2cc603 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 @@ -4,7 +4,6 @@ import fr.inria.corese.core.kgram.api.core.Edge; import fr.inria.corese.core.kgram.api.core.Node; import fr.inria.corese.core.next.data.api.*; -import fr.inria.corese.core.next.storagemanager.api.plugin.PluginException; import fr.inria.corese.core.sparql.api.IDatatype; import java.util.Arrays; @@ -41,13 +40,12 @@ public boolean add(Statement stmt) { Resource ctxResource = stmt.getContext(); Node context = (ctxResource != null) ? resourceToNode(ctxResource) : null; - // Use Graph.addTriple() or Graph.add() instead of create() + addEdge() - // This bypasses the Edge creation issue + // Use Graph.addEdge() directly to bypass Edge creation issues Edge edge; if (context == null) { edge = graph.addEdge(subject, predicate, object); } else { - edge = graph.addEdge(subject, predicate, object, context); + edge = graph.addEdge(context, subject, predicate, object); } return edge != null; @@ -82,7 +80,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) { @@ -94,9 +92,8 @@ 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(ctx -> ctx != null ? resourceToNode(ctx) : null) - .toArray(Node[]::new); + // Normalize contexts: convert null to default graph node + Node[] ctxNodes = normalizeContexts(contexts); edges = graph.getEdgesRDF4J(subject, predicate, object, ctxNodes); } @@ -215,6 +212,18 @@ public Set getContexts() { return contexts; } + /** + * Normalizes context array for Graph backend. + * + * @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}. * 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 index 423ec6d2b..df6087569 100644 --- 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 @@ -6,6 +6,7 @@ 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; @@ -80,14 +81,13 @@ void shouldCreateMemoryModelWhenTypeIsMemory() throws PluginException { @Test @DisplayName("Should throw IllegalArgumentException for unknown type") void shouldThrowExceptionForUnknownType() { - IllegalArgumentException exception = assertThrows( - IllegalArgumentException.class, + PluginNotFoundException exception = assertThrows( + PluginNotFoundException.class, () -> factory.createModel("unknown") ); - assertTrue(exception.getMessage().contains("Unknown storage type")); + assertTrue(exception.getMessage().contains("No plugin found")); assertTrue(exception.getMessage().contains("unknown")); - assertTrue(exception.getMessage().contains("memory, graph")); } @@ -100,21 +100,7 @@ void shouldThrowExceptionForEmptyType() { ); } - @Test - @DisplayName("Should be case-sensitive for storage type") - void shouldBeCaseSensitiveForStorageType() { - // "MEMORY" should fail (case-sensitive) - assertThrows( - IllegalArgumentException.class, - () -> factory.createModel("MEMORY") - ); - // "Graph" should fail (case-sensitive) - assertThrows( - IllegalArgumentException.class, - () -> factory.createModel("Graph") - ); - } } @Nested @@ -242,18 +228,12 @@ void createModelShouldProduceSameBehaviorAsSpecializedMethods() throws PluginExc Model memoryModel1 = factory.createModel("memory"); Model memoryModel2 = factory.createMemoryModel(); - Model graphModel1 = factory.createModel("graph"); - Model graphModel2 = factory.createGraphModel(); // Add data memoryModel1.add(subject, predicate, object); memoryModel2.add(subject, predicate, object); - graphModel1.add(subject, predicate, object); - graphModel2.add(subject, predicate, object); - // All should behave the same assertEquals(memoryModel1.size(), memoryModel2.size()); - assertEquals(graphModel1.size(), graphModel2.size()); } } @@ -271,7 +251,7 @@ void shouldHandleEmptyStringAsStorageType() { () -> factory.createModel("") ); - assertTrue(exception.getMessage().contains("Unknown storage type")); + assertTrue(exception.getMessage().contains("cannot be null or empty")); } @Test @@ -282,7 +262,7 @@ void shouldHandleWhitespaceOnlyStringAsStorageType() { () -> factory.createModel(" ") ); - assertTrue(exception.getMessage().contains("Unknown storage type")); + assertTrue(exception.getMessage().contains("cannot be null or empty")); } } 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 0000000000000000000000000000000000000000..433ea8c3bc64cd4517abf4af87c4685589abdf78 GIT binary patch literal 12866 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

Y&14Wdg%(E56DFM5Xt~5)=677>`HQmutD?n!I8& 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 literal 0 HcmV?d00001 From 0f74e4ff3cbcedea6c6413f5e96566bd6e857ad2 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Tue, 17 Mar 2026 15:19:47 +0100 Subject: [PATCH 7/8] Plugin system to choose StorageManager implementation --- .../core/next/storagemanager/impl/graph/GraphAdapter.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 32e2cc603..83cc3dd19 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 @@ -354,9 +354,13 @@ private Edge statementToEdge(Statement stmt) { Node object = valueToNode(stmt.getObject()); Resource ctxResource = stmt.getContext(); - Node context = (ctxResource != null) ? resourceToNode(ctxResource) : null; - return graph.create(subject, predicate, object, context); + if (ctxResource == null) { + return graph.create(null, subject, predicate, object); + } else { + Node context = resourceToNode(ctxResource); + return graph.create(context, subject, predicate, object); + } } /** From eed6db0e038c4e1f8dc1cbff3c5a623c2eba1551 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Wed, 18 Mar 2026 08:50:38 +0100 Subject: [PATCH 8/8] Plugin system to choose StorageManager implementation --- .../impl/graph/GraphAdapter.java | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) 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 83cc3dd19..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 @@ -38,15 +38,12 @@ public boolean add(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(); - // Use Graph.addEdge() directly to bypass Edge creation issues - Edge edge; - if (context == null) { - edge = graph.addEdge(subject, predicate, object); - } else { - edge = graph.addEdge(context, subject, predicate, object); - } + // Always use 4-argument form with explicit context + Edge edge = graph.addEdge(context, subject, predicate, object); return edge != null; } @@ -254,9 +251,10 @@ private Statement edgeToStatement(Edge edge) { 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); @@ -354,13 +352,11 @@ private Edge statementToEdge(Statement stmt) { Node object = valueToNode(stmt.getObject()); Resource ctxResource = stmt.getContext(); + Node context = (ctxResource != null) + ? resourceToNode(ctxResource) + : graph.getDefaultGraphNode(); - if (ctxResource == null) { - return graph.create(null, subject, predicate, object); - } else { - Node context = resourceToNode(ctxResource); - return graph.create(context, subject, predicate, object); - } + return graph.create(context, subject, predicate, object); } /**