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

+ * + * + * @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: + *

+ * + * + *

Built-in Priorities: + *

+ * + * + *

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