Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions src/main/java/fr/inria/corese/core/next/data/factory/ModelFactory.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <h2>Supported Storage Types</h2>
* <ul>
* <li><b>"memory"</b> - In-memory HashMap-based storage (fast, no persistence)</li>
* <li><b>"graph"</b> - Legacy Graph-based storage (indexed, persistent)</li>
* </ul>
*
* @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}.
*
* <p>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.</p>
*
* @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.
*
* <p>Memory storage uses a ConcurrentHashMap backend for fast, in-memory storage.
* This is ideal for testing, prototyping, and small datasets (&lt; 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.
*
* <p>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.
*
* <p>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();
}
}
Original file line number Diff line number Diff line change
@@ -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<ClassLoader> 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<StoragePlugin> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>When multiple plugins support the same configuration (i.e., their
* {@link #supports(StorageConfig)} method returns {@code true}), the plugin
* with the <b>highest priority</b> is selected.
*
* <p><b>Priority Semantics:</b>
* <ul>
* <li><b>Higher values = Higher priority:</b> A plugin with priority 100 will be
* selected over one with priority 50</li>
* <li><b>Default is 0:</b> If this method is not overridden, the plugin has priority 0</li>
* <li><b>Negative values are allowed:</b> Use negative priorities for fallback plugins
* that should only be used when no other plugin is available</li>
* </ul>
*
*
* <p><b>Built-in Priorities:</b>
* <ul>
* <li>GraphStoragePlugin: 100</li>
* <li>MemoryStoragePlugin: 50</li>
* </ul>
*
*
* <p><b>Tie-Breaking:</b> 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() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a comment in the class description about the intended usage of priority ?

return 0;
}
}
Loading
Loading