diff --git a/PLUGINS.md b/PLUGINS.md new file mode 100644 index 0000000000..d1cd3fdf4f --- /dev/null +++ b/PLUGINS.md @@ -0,0 +1,299 @@ +# ArcadeDB Plugin Architecture + +## Overview + +ArcadeDB supports a plugin architecture that allows extending the server functionality through isolated plugins. Each plugin runs in its own class loader, enabling plugins to have different versions of dependencies without conflicts. + +## Plugin Types + +ArcadeDB includes the following built-in plugins: + +- **Gremlin** - Apache TinkerPop Gremlin graph traversal language support +- **PostgreSQL Wire Protocol** - PostgreSQL protocol compatibility +- **MongoDB Wire Protocol** - MongoDB query language compatibility +- **Redis Wire Protocol** - Redis command compatibility +- **gRPC** - gRPC protocol support + +## Architecture + +### Class Loading + +The plugin system uses isolated class loaders with the following strategy: + +1. **Server API Classes** (`com.arcadedb.*`) - Loaded from parent class loader (shared across all plugins) +2. **Plugin Classes** - Loaded from plugin's own JAR first (isolated) +3. **Other Classes** - Fall back to parent class loader if not found in plugin JAR + +This approach ensures: +- Plugins can use different versions of third-party libraries +- Server APIs are shared for consistency and communication +- Memory efficiency through shared core classes + +### Components + +#### PluginManager +- Discovers plugins from `lib/plugins/` directory +- Manages plugin lifecycle (start, stop) +- Coordinates plugin loading with server initialization + +#### PluginClassLoader +- Custom class loader that isolates plugin dependencies +- Parent-first delegation for server API classes +- Child-first delegation for plugin-specific classes + +#### PluginDescriptor +- Metadata container for each plugin +- Tracks plugin state and lifecycle +- Associates plugin with its class loader + +## Plugin Lifecycle + +1. **Discovery** - Scan `lib/plugins/` directory for JAR files +2. **Class Loading** - Create isolated class loader for each plugin JAR +3. **Service Discovery** - Use Java ServiceLoader to find `ServerPlugin` implementations +4. **Configuration** - Call `configure()` with server instance and configuration +5. **Starting** - Call `startService()` based on installation priority +6. **Running** - Plugin provides functionality +7. **Stopping** - Call `stopService()` in reverse order +8. **Cleanup** - Close class loaders and release resources + +### Installation Priorities + +Plugins are started in phases based on their installation priority: + +1. `BEFORE_HTTP_ON` - Before HTTP server starts (default) +2. `AFTER_HTTP_ON` - After HTTP server starts +3. `AFTER_DATABASES_OPEN` - After databases are loaded + +## Creating a Plugin + +### 1. Implement ServerPlugin Interface + +```java +package com.example.myplugin; + +import com.arcadedb.ContextConfiguration; +import com.arcadedb.server.ArcadeDBServer; +import com.arcadedb.server.ServerPlugin; + +public class MyPlugin implements ServerPlugin { + private ArcadeDBServer server; + private ContextConfiguration configuration; + + @Override + public void configure(ArcadeDBServer arcadeDBServer, ContextConfiguration configuration) { + this.server = arcadeDBServer; + this.configuration = configuration; + // Initialize your plugin configuration + } + + @Override + public void startService() { + // Start your plugin services + System.out.println("MyPlugin started!"); + } + + @Override + public void stopService() { + // Stop your plugin services and clean up resources + System.out.println("MyPlugin stopped!"); + } + + @Override + public INSTALLATION_PRIORITY getInstallationPriority() { + return INSTALLATION_PRIORITY.AFTER_HTTP_ON; + } +} +``` + +### 2. Create Service Provider Configuration + +Create file: `src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin` + +Content: +``` +com.example.myplugin.MyPlugin +``` + +### 3. Build Plugin JAR + +```bash +mvn clean package +``` + +### 4. Deploy Plugin + +Copy the plugin JAR to the `lib/plugins/` directory in your ArcadeDB installation: + +```bash +cp target/myplugin-1.0.0.jar $ARCADEDB_HOME/lib/plugins/ +``` + +### 5. Start ArcadeDB + +The plugin will be automatically discovered and loaded when ArcadeDB starts: + +```bash +cd $ARCADEDB_HOME +bin/server.sh +``` + +Check the logs for: +``` +[INFO] Discovered plugin: myplugin from myplugin-1.0.0.jar +[INFO] - myplugin plugin started +``` + +## Plugin Dependencies + +### Server API Dependencies + +Plugin POMs should include server dependencies with `provided` scope: + +```xml + + com.arcadedb + arcadedb-server + ${arcadedb.version} + provided + +``` + +### Plugin-Specific Dependencies + +Plugin-specific dependencies use normal `compile` scope and will be packaged with the plugin: + +```xml + + com.example + my-library + 1.0.0 + compile + +``` + +## Building Distributions with Plugins + +### Maven Assembly + +The Maven assembly descriptor automatically places plugin JARs in `lib/plugins/`: + +```xml + + lib/plugins + + com.arcadedb:arcadedb-gremlin + com.arcadedb:arcadedb-postgresw + com.arcadedb:arcadedb-mongodbw + com.arcadedb:arcadedb-redisw + com.arcadedb:arcadedb-grpcw + + false + +``` + +## Advanced Topics + +### Accessing Server Resources + +Plugins have full access to the ArcadeDB server instance: + +```java +@Override +public void configure(ArcadeDBServer arcadeDBServer, ContextConfiguration configuration) { + this.server = arcadeDBServer; + + // Access databases + ServerDatabase db = server.getDatabase("mydb"); + + // Access HTTP server for custom endpoints + HttpServer httpServer = server.getHttpServer(); + + // Access security + ServerSecurity security = server.getSecurity(); +} +``` + +### Thread Context Class Loader + +The PluginManager automatically sets the thread context class loader during plugin operations: + +- During `configure()` - Set to plugin's class loader +- During `startService()` - Set to plugin's class loader +- During `stopService()` - Set to plugin's class loader + +This ensures proper class loading for frameworks that use the thread context class loader. + +### HTTP Endpoint Registration + +Plugins can register custom HTTP endpoints: + +```java +@Override +public void registerAPI(HttpServer httpServer, PathHandler routes) { + routes.addExactPath("/api/myplugin", exchange -> { + exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "application/json"); + exchange.getResponseSender().send("{\"status\":\"ok\"}"); + }); +} +``` + +## Troubleshooting + +### Plugin Not Discovered + +Check that: +1. Plugin JAR is in `lib/plugins/` directory +2. `META-INF/services/com.arcadedb.server.ServerPlugin` file exists +3. Service file contains correct plugin class name +4. Plugin class implements `ServerPlugin` interface + +### ClassNotFoundException + +If you see `ClassNotFoundException` for server classes: +- Ensure server dependencies use `provided` scope +- Check that class is in `com.arcadedb.*` package + +If you see `ClassNotFoundException` for plugin classes: +- Ensure dependency is included with `compile` scope +- Check that JAR contains the required class + +### Plugin Conflicts + +If two plugins have conflicting dependencies: +- This is the main benefit of isolated class loaders +- Each plugin can use its own version +- Ensure server API classes match across all plugins + +## Migration from Legacy Plugin Loading + +The new plugin system is backward compatible with the legacy configuration-based loading. Both systems can coexist: + +### Legacy Method (still supported) +```properties +arcadedb.server.plugins=gremlin:com.arcadedb.server.gremlin.GremlinServerPlugin +``` + +### New Method (recommended) +1. Place plugin JAR in `lib/plugins/` +2. Include `META-INF/services` file +3. No configuration needed + +## Best Practices + +1. **Use Provided Scope** - Server dependencies should always use `provided` scope +2. **Clean Shutdown** - Implement proper cleanup in `stopService()` +3. **Thread Safety** - Make plugin implementations thread-safe +4. **Logging** - Use `LogManager.instance().log()` for consistent logging +5. **Error Handling** - Handle exceptions gracefully, don't crash the server +6. **Resource Management** - Close all resources in `stopService()` +7. **Configuration** - Use `ContextConfiguration` for plugin settings + +## Examples + +See the built-in plugins for complete examples: +- `gremlin/` - Complex plugin with custom graph manager +- `postgresw/` - Network protocol plugin +- `mongodbw/` - Query language compatibility plugin +- `redisw/` - Simple protocol plugin +- `grpcw/` - gRPC service plugin diff --git a/bolt/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin b/bolt/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin new file mode 100644 index 0000000000..62331a7d2a --- /dev/null +++ b/bolt/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin @@ -0,0 +1 @@ +com.arcadedb.bolt.BoltProtocolPlugin diff --git a/e2e-js/src/js-pg-e2e.test.js b/e2e-js/src/js-pg-e2e.test.js index bd76f109fc..3e6fe7dcc3 100644 --- a/e2e-js/src/js-pg-e2e.test.js +++ b/e2e-js/src/js-pg-e2e.test.js @@ -30,7 +30,7 @@ describe("E2E tests using pg client", () => { .withExposedPorts(2480, 6379, 5432, 8182) .withEnvironment({ JAVA_OPTS: - "-Darcadedb.server.rootPassword=playwithdata -Darcadedb.server.defaultDatabases=beer[root]{import:https://github.com/ArcadeData/arcadedb-datasets/raw/main/orientdb/OpenBeer.gz} -Darcadedb.server.plugins=Postgres:com.arcadedb.postgres.PostgresProtocolPlugin,GremlinServer:com.arcadedb.server.gremlin.GremlinServerPlugin,PrometheusMetrics:com.arcadedb.metrics.prometheus.PrometheusMetricsPlugin", + "-Darcadedb.server.rootPassword=playwithdata -Darcadedb.server.defaultDatabases=beer[root]{import:https://github.com/ArcadeData/arcadedb-datasets/raw/main/orientdb/OpenBeer.gz} -Darcadedb.server.plugins=PostgresProtocolPlugin,GremlinServerPlugin,PrometheusMetricsPlugin", }) .withStartupTimeout(60000) .withWaitStrategy(Wait.forHttp("/api/v1/ready", 2480).forStatusCodeMatching((statusCode) => statusCode === 204)) diff --git a/e2e-perf/src/test/java/com/arcadedb/test/support/ContainersTestTemplate.java b/e2e-perf/src/test/java/com/arcadedb/test/support/ContainersTestTemplate.java index 3586c857d2..b15d19ad96 100644 --- a/e2e-perf/src/test/java/com/arcadedb/test/support/ContainersTestTemplate.java +++ b/e2e-perf/src/test/java/com/arcadedb/test/support/ContainersTestTemplate.java @@ -207,7 +207,7 @@ protected GenericContainer createArcadeContainer(String name, .withEnv("JAVA_OPTS", String.format(""" -Darcadedb.server.rootPassword=playwithdata - -Darcadedb.server.plugins=Postgres:com.arcadedb.postgres.PostgresProtocolPlugin,GRPC:com.arcadedb.server.grpc.GrpcServerPlugin,PrometheusMetrics:com.arcadedb.metrics.prometheus.PrometheusMetricsPlugin + -Darcadedb.server.plugins=PostgresProtocolPlugin,GrpcServerPlugin,PrometheusMetricsPlugin -Darcadedb.server.httpsIoThreads=30 -Darcadedb.bucketReuseSpaceMode=low -Darcadedb.server.name=%s diff --git a/e2e-python/tests/test_arcadedb.py b/e2e-python/tests/test_arcadedb.py index 890eade4c5..ff5bd26c04 100644 --- a/e2e-python/tests/test_arcadedb.py +++ b/e2e-python/tests/test_arcadedb.py @@ -31,7 +31,7 @@ .with_env("JAVA_OPTS", "-Darcadedb.server.rootPassword=playwithdata " "-Darcadedb.server.defaultDatabases=beer[root]{import:https://github.com/ArcadeData/arcadedb-datasets/raw/main/orientdb/OpenBeer.gz} " - "-Darcadedb.server.plugins=Postgres:com.arcadedb.postgres.PostgresProtocolPlugin,GremlinServer:com.arcadedb.server.gremlin.GremlinServerPlugin,PrometheusMetrics:com.arcadedb.metrics.prometheus.PrometheusMetricsPlugin")) + "-Darcadedb.server.plugins=PostgresProtocolPlugin,GremlinServerPlugin,PrometheusMetricsPlugin")) def get_connection_params(container): diff --git a/e2e-python/tests/test_asyncpg.py b/e2e-python/tests/test_asyncpg.py index 2f011bef66..5953cc6435 100644 --- a/e2e-python/tests/test_asyncpg.py +++ b/e2e-python/tests/test_asyncpg.py @@ -49,7 +49,7 @@ .with_env("JAVA_OPTS", "-Darcadedb.server.rootPassword=playwithdata " "-Darcadedb.server.defaultDatabases=beer[root]{import:https://github.com/ArcadeData/arcadedb-datasets/raw/main/orientdb/OpenBeer.gz} " - "-Darcadedb.server.plugins=Postgres:com.arcadedb.postgres.PostgresProtocolPlugin,GremlinServer:com.arcadedb.server.gremlin.GremlinServerPlugin,PrometheusMetrics:com.arcadedb.metrics.prometheus.PrometheusMetricsPlugin")) + "-Darcadedb.server.plugins=PostgresProtocolPlugin,GremlinServerPlugin,PrometheusMetricsPlugin")) def get_connection_params(container): diff --git a/e2e/src/test/java/com/arcadedb/e2e/ArcadeContainerTemplate.java b/e2e/src/test/java/com/arcadedb/e2e/ArcadeContainerTemplate.java index d19e38d118..f4ede9adb3 100644 --- a/e2e/src/test/java/com/arcadedb/e2e/ArcadeContainerTemplate.java +++ b/e2e/src/test/java/com/arcadedb/e2e/ArcadeContainerTemplate.java @@ -32,14 +32,14 @@ public abstract class ArcadeContainerTemplate { .withStartupTimeout(Duration.ofSeconds(90)) .withEnv("JAVA_OPTS", """ -Darcadedb.server.rootPassword=playwithdata - -Darcadedb.postgres.debug=true + -Darcadedb.postgres.debug=false -Darcadedb.grpc.enabled=true -Darcadedb.grpc.port=50051 -Darcadedb.grpc.mode=standard -Darcadedb.grpc.reflection.enabled=true -Darcadedb.grpc.health.enabled=true -Darcadedb.server.defaultDatabases=beer[root]{import:https://github.com/ArcadeData/arcadedb-datasets/raw/main/orientdb/OpenBeer.gz} - -Darcadedb.server.plugins=Postgres:com.arcadedb.postgres.PostgresProtocolPlugin,GremlinServer:com.arcadedb.server.gremlin.GremlinServerPlugin,GRPC:com.arcadedb.server.grpc.GrpcServerPlugin,PrometheusMetrics:com.arcadedb.metrics.prometheus.PrometheusMetricsPlugin + -Darcadedb.server.plugins=PostgresProtocolPlugin,GremlinServerPlugin,GrpcServerPlugin,PrometheusMetricsPlugin """) .waitingFor(Wait.forHttp("/api/v1/ready").forPort(2480).forStatusCode(204)); ARCADE.start(); diff --git a/engine/pom.xml b/engine/pom.xml index 36b861cbf6..32b733cace 100644 --- a/engine/pom.xml +++ b/engine/pom.xml @@ -157,11 +157,6 @@ antlr4-runtime ${antlr4.version} - - org.slf4j - slf4j-jdk14 - ${slf4j.version} - org.graalvm.sdk graal-sdk diff --git a/engine/src/main/java/com/arcadedb/database/LocalDatabase.java b/engine/src/main/java/com/arcadedb/database/LocalDatabase.java index 29f4616452..e4984f3eab 100644 --- a/engine/src/main/java/com/arcadedb/database/LocalDatabase.java +++ b/engine/src/main/java/com/arcadedb/database/LocalDatabase.java @@ -65,10 +65,10 @@ import com.arcadedb.query.opencypher.query.CypherPlanCache; import com.arcadedb.query.opencypher.query.CypherStatementCache; import com.arcadedb.query.select.Select; +import com.arcadedb.query.sql.SQLQueryEngine; import com.arcadedb.query.sql.executor.ResultSet; import com.arcadedb.query.sql.parser.ExecutionPlanCache; import com.arcadedb.query.sql.parser.StatementCache; -import com.arcadedb.query.sql.SQLQueryEngine; import com.arcadedb.schema.DocumentType; import com.arcadedb.schema.EdgeType; import com.arcadedb.schema.LocalDocumentType; @@ -85,15 +85,35 @@ import com.arcadedb.utility.MultiIterator; import com.arcadedb.utility.RWLockContext; -import java.io.*; -import java.nio.channels.*; -import java.nio.file.*; -import java.util.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.*; -import java.util.concurrent.locks.*; -import java.util.logging.*; -import java.util.stream.*; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.io.UncheckedIOException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.Level; +import java.util.stream.Stream; /** * Local implementation of {@link Database}. It is based on files opened on the local file system. @@ -137,8 +157,8 @@ public class LocalDatabase extends RWLockContext implements DatabaseInternal { private final Map>> callbacks; private final StatementCache statementCache; private final ExecutionPlanCache executionPlanCache; - private final CypherStatementCache cypherStatementCache; - private final CypherPlanCache cypherPlanCache; + private final CypherStatementCache cypherStatementCache; + private final CypherPlanCache cypherPlanCache; private final File configurationFile; private DatabaseInternal wrappedDatabaseInstance = this; private int edgeListSize = EDGE_LIST_INITIAL_CHUNK_SIZE; diff --git a/engine/src/main/java/com/arcadedb/query/QueryEngineManager.java b/engine/src/main/java/com/arcadedb/query/QueryEngineManager.java index 5ed903e5eb..3ff8ac4378 100644 --- a/engine/src/main/java/com/arcadedb/query/QueryEngineManager.java +++ b/engine/src/main/java/com/arcadedb/query/QueryEngineManager.java @@ -49,11 +49,13 @@ public QueryEngineManager() { register("com.arcadedb.redis.query.RedisQueryEngineFactory"); } + public void register(final String className) { try { + register((QueryEngine.QueryEngineFactory) Class.forName(className).getConstructor().newInstance()); } catch (final Exception e) { - LogManager.instance().log(this, Level.FINE, "Unable to register engine '%s' (%s)", className, e.getMessage()); + LogManager.instance().log(this, Level.WARNING, "Unable to register engine '%s' (%s)", className, e.getMessage()); } } diff --git a/gremlin/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin b/gremlin/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin new file mode 100644 index 0000000000..e825fbc860 --- /dev/null +++ b/gremlin/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin @@ -0,0 +1 @@ +com.arcadedb.server.gremlin.GremlinServerPlugin diff --git a/grpcw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin b/grpcw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin new file mode 100644 index 0000000000..eae577bba6 --- /dev/null +++ b/grpcw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin @@ -0,0 +1 @@ +com.arcadedb.server.grpc.GrpcServerPlugin diff --git a/metrics/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin b/metrics/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin new file mode 100644 index 0000000000..56d49460a5 --- /dev/null +++ b/metrics/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin @@ -0,0 +1 @@ +com.arcadedb.metrics.prometheus.PrometheusMetricsPlugin diff --git a/metrics/src/test/java/com/arcadedb/metrics/prometheus/PrometheusMetricsPluginNotAuthenticatedTest.java b/metrics/src/test/java/com/arcadedb/metrics/prometheus/PrometheusMetricsPluginNotAuthenticatedTest.java index 26bfab9f68..4627be1f36 100644 --- a/metrics/src/test/java/com/arcadedb/metrics/prometheus/PrometheusMetricsPluginNotAuthenticatedTest.java +++ b/metrics/src/test/java/com/arcadedb/metrics/prometheus/PrometheusMetricsPluginNotAuthenticatedTest.java @@ -37,7 +37,7 @@ class PrometheusMetricsPluginNotAuthenticatedTest extends BaseGraphServerTest { public void setTestConfiguration() { super.setTestConfiguration(); System.setProperty("arcadedb.serverMetrics.prometheus.requireAuthentication", "false"); - GlobalConfiguration.SERVER_PLUGINS.setValue("Prometheus:com.arcadedb.metrics.prometheus.PrometheusMetricsPlugin"); + GlobalConfiguration.SERVER_PLUGINS.setValue("PrometheusMetricsPlugin"); } @AfterEach diff --git a/mongodbw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin b/mongodbw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin new file mode 100644 index 0000000000..edb7f7cc6a --- /dev/null +++ b/mongodbw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin @@ -0,0 +1 @@ +com.arcadedb.mongo.MongoDBProtocolPlugin diff --git a/package/arcadedb-builder.sh b/package/arcadedb-builder.sh index 22bc270026..826e34a3b8 100755 --- a/package/arcadedb-builder.sh +++ b/package/arcadedb-builder.sh @@ -804,6 +804,17 @@ download_optional_modules() { local extracted_dir="$TEMP_DIR/arcadedb-${ARCADEDB_VERSION}" local lib_dir="${extracted_dir}/lib" +# local plugins_dir="${extracted_dir}/lib/plugins" +# +# # Create plugins directory if it doesn't exist +# if [[ "$DRY_RUN" != true ]]; then +# if [[ ! -d "$plugins_dir" ]]; then +# mkdir -p "$plugins_dir" +# log_verbose "Created plugins directory: $plugins_dir" +# fi +# else +# log_info "[DRY RUN] Would create plugins directory" +# fi # Split modules by comma IFS=',' read -ra modules <<<"$SELECTED_MODULES" @@ -813,12 +824,17 @@ download_optional_modules() { # Determine if shaded or regular JAR local classifier="" +# local dest_dir="$lib_dir" if [[ " $SHADED_MODULES " =~ " $module " ]]; then classifier="-shaded" + # Shaded modules are plugins and go to lib/plugins/ +# dest_dir="$plugins_dir" fi local artifact_id="arcadedb-${module}" local jar_filename="${artifact_id}-${ARCADEDB_VERSION}${classifier}.jar" +# local jar_file="${dest_dir}/${jar_filename}" + local jar_file="${lib_dir}/${jar_filename}" if [[ -n "$LOCAL_REPO" ]]; then diff --git a/package/pom.xml b/package/pom.xml index e24d1a1c6a..1aad442dde 100644 --- a/package/pom.xml +++ b/package/pom.xml @@ -187,6 +187,18 @@ + + com.arcadedb + arcadedb-bolt + ${project.parent.version} + shaded + + + * + * + + + com.arcadedb arcadedb-redisw diff --git a/package/src/main/assembly/base.xml b/package/src/main/assembly/base.xml index d7622c0b72..2422daf39c 100644 --- a/package/src/main/assembly/base.xml +++ b/package/src/main/assembly/base.xml @@ -95,9 +95,10 @@ + lib @@ -106,15 +107,17 @@ com.arcadedb:arcadedb-console com.arcadedb:arcadedb-gremlin - com.arcadedb:arcadedb-redisw + com.arcadedb:arcadedb-bolt + com.arcadedb:arcadedb-postgresw com.arcadedb:arcadedb-mongodbw + com.arcadedb:arcadedb-redisw + com.arcadedb:arcadedb-grpcw com.arcadedb:arcadedb-graphql com.arcadedb:arcadedb-studio - com.arcadedb:arcadedb-postgresw - com.arcadedb:arcadedb-grpcw com.arcadedb:arcadedb-metrics + diff --git a/package/src/main/assembly/full.xml b/package/src/main/assembly/full.xml index ba3b73a778..84a6918346 100644 --- a/package/src/main/assembly/full.xml +++ b/package/src/main/assembly/full.xml @@ -94,14 +94,42 @@ + lib *:jar:* + + com.arcadedb:arcadedb-gremlin + com.arcadedb:arcadedb-bolt + com.arcadedb:arcadedb-postgresw + com.arcadedb:arcadedb-mongodbw + com.arcadedb:arcadedb-redisw + com.arcadedb:arcadedb-grpcw + com.arcadedb:arcadedb-graphql + com.arcadedb:arcadedb-metrics + + + + + + lib + + + com.arcadedb:arcadedb-gremlin + com.arcadedb:arcadedb-bolt + com.arcadedb:arcadedb-postgresw + com.arcadedb:arcadedb-mongodbw + com.arcadedb:arcadedb-redisw + com.arcadedb:arcadedb-grpcw + com.arcadedb:arcadedb-graphql + com.arcadedb:arcadedb-metrics + + false diff --git a/package/src/main/assembly/headless.xml b/package/src/main/assembly/headless.xml index 7226e0b93f..bd9410f607 100644 --- a/package/src/main/assembly/headless.xml +++ b/package/src/main/assembly/headless.xml @@ -93,9 +93,10 @@ + lib @@ -103,12 +104,26 @@ com.arcadedb:arcadedb-gremlin - com.arcadedb:arcadedb-redisw + com.arcadedb:arcadedb-bolt + com.arcadedb:arcadedb-postgresw com.arcadedb:arcadedb-mongodbw + com.arcadedb:arcadedb-redisw + com.arcadedb:arcadedb-grpcw com.arcadedb:arcadedb-graphql com.arcadedb:arcadedb-studio + com.arcadedb:arcadedb-metrics + + + + lib + + + com.arcadedb:arcadedb-postgresw + + false + diff --git a/package/src/main/assembly/minimal.xml b/package/src/main/assembly/minimal.xml index 019ad17ebf..43c66dba22 100644 --- a/package/src/main/assembly/minimal.xml +++ b/package/src/main/assembly/minimal.xml @@ -93,9 +93,10 @@ + lib @@ -103,11 +104,25 @@ com.arcadedb:arcadedb-gremlin - com.arcadedb:arcadedb-redisw + com.arcadedb:arcadedb-bolt + com.arcadedb:arcadedb-postgresw com.arcadedb:arcadedb-mongodbw + com.arcadedb:arcadedb-redisw + com.arcadedb:arcadedb-grpcw com.arcadedb:arcadedb-graphql + com.arcadedb:arcadedb-metrics + + + + lib + + + com.arcadedb:arcadedb-postgresw + + false + diff --git a/pom.xml b/pom.xml index 411119c2e4..7ee9389ac0 100644 --- a/pom.xml +++ b/pom.xml @@ -412,6 +412,11 @@ + + org.slf4j + slf4j-jdk14 + ${slf4j.version} + org.junit.jupiter junit-jupiter diff --git a/postgresw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin b/postgresw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin new file mode 100644 index 0000000000..46abd8bbcb --- /dev/null +++ b/postgresw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin @@ -0,0 +1 @@ +com.arcadedb.postgres.PostgresProtocolPlugin diff --git a/redisw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin b/redisw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin new file mode 100644 index 0000000000..62063d53a7 --- /dev/null +++ b/redisw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin @@ -0,0 +1 @@ +com.arcadedb.redis.RedisProtocolPlugin diff --git a/server/src/main/java/com/arcadedb/server/ArcadeDBServer.java b/server/src/main/java/com/arcadedb/server/ArcadeDBServer.java index 258551e120..3703f2b732 100644 --- a/server/src/main/java/com/arcadedb/server/ArcadeDBServer.java +++ b/server/src/main/java/com/arcadedb/server/ArcadeDBServer.java @@ -40,6 +40,7 @@ import com.arcadedb.server.ha.HAServer; import com.arcadedb.server.ha.ReplicatedDatabase; import com.arcadedb.server.http.HttpServer; +import com.arcadedb.server.plugin.PluginManager; import com.arcadedb.server.security.ServerSecurity; import com.arcadedb.server.security.ServerSecurityException; import com.arcadedb.server.security.ServerSecurityUser; @@ -59,7 +60,15 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.logging.Level; @@ -77,14 +86,13 @@ public enum STATUS {OFFLINE, STARTING, ONLINE, SHUTTING_DOWN} private String hostAddress; private final boolean replicationLifecycleEventsEnabled; private FileServerEventLog eventLog; - private final Map plugins = - new LinkedHashMap<>(); + private final Map plugins = new LinkedHashMap<>(); + private PluginManager pluginManager; private String serverRootPath; private HAServer haServer; private ServerSecurity security; private HttpServer httpServer; - private final ConcurrentMap databases = - new ConcurrentHashMap<>(); + private final ConcurrentMap databases = new ConcurrentHashMap<>(); private final List testEventListeners = new ArrayList<>(); private volatile STATUS status = STATUS.OFFLINE; // private ServerMonitor serverMonitor; @@ -139,8 +147,11 @@ public synchronized void start() { throw new ServerException("Error on starting the server '" + serverName + "'"); } + // Discover plugins from lib/plugins directory + pluginManager.discoverPlugins(); + LogManager.instance().log(this, Level.INFO, "Starting ArcadeDB Server in %s mode with plugins %s ...", - GlobalConfiguration.SERVER_MODE.getValueAsString(), getPluginNames()); + GlobalConfiguration.SERVER_MODE.getValueAsString(), getAllPluginNames()); // START METRICS & CONNECTED JMX REPORTER if (configuration.getValueAsBoolean(GlobalConfiguration.SERVER_METRICS)) { @@ -171,7 +182,8 @@ public synchronized void start() { // START HTTP SERVER IMMEDIATELY. THE HTTP ADDRESS WILL BE USED BY HA httpServer = new HttpServer(this); - registerPlugins(ServerPlugin.INSTALLATION_PRIORITY.BEFORE_HTTP_ON); +// registerPlugins(ServerPlugin.PluginInstallationPriority.BEFORE_HTTP_ON); + pluginManager.startPlugins(ServerPlugin.PluginInstallationPriority.BEFORE_HTTP_ON); httpServer.startService(); @@ -180,14 +192,14 @@ public synchronized void start() { haServer.startService(); } - registerPlugins(ServerPlugin.INSTALLATION_PRIORITY.AFTER_HTTP_ON); + pluginManager.startPlugins(ServerPlugin.PluginInstallationPriority.AFTER_HTTP_ON); loadDefaultDatabases(); // RELOAD DATABASE IF A PLUGIN REGISTERED A NEW DATABASE (LIKE THE GREMLIN SERVER) loadDatabases(); - registerPlugins(ServerPlugin.INSTALLATION_PRIORITY.AFTER_DATABASES_OPEN); + pluginManager.startPlugins(ServerPlugin.PluginInstallationPriority.AFTER_DATABASES_OPEN); status = STATUS.ONLINE; @@ -276,7 +288,17 @@ private Set getPluginNames() { return result; } - private void registerPlugins(final ServerPlugin.INSTALLATION_PRIORITY installationPriority) { + private Set getAllPluginNames() { + final Set result = new LinkedHashSet<>(); + // Add legacy plugins + result.addAll(getPluginNames()); + // Add PluginManager plugins + if (pluginManager != null) + result.addAll(pluginManager.getPluginNames()); + return result; + } + + private void registerPlugins(final ServerPlugin.PluginInstallationPriority installationPriority) { final String registeredPlugins = configuration.getValueAsString(GlobalConfiguration.SERVER_PLUGINS); if (registeredPlugins != null && !registeredPlugins.isEmpty()) { final String[] pluginEntries = registeredPlugins.split(","); @@ -308,7 +330,7 @@ private void registerPlugins(final ServerPlugin.INSTALLATION_PRIORITY installati } // Auto-register backup scheduler plugin if backup.json exists and not already registered - if (installationPriority == ServerPlugin.INSTALLATION_PRIORITY.AFTER_DATABASES_OPEN + if (installationPriority == ServerPlugin.PluginInstallationPriority.AFTER_DATABASES_OPEN && !plugins.containsKey("auto-backup")) { registerAutoBackupPluginIfConfigured(); } @@ -354,6 +376,11 @@ public synchronized void stop() { status = STATUS.SHUTTING_DOWN; + // Stop plugins managed by PluginManager first + if (pluginManager != null) + pluginManager.stopPlugins(); + + // Stop legacy plugins for (final Map.Entry pEntry : plugins.entrySet()) { LogManager.instance().log(this, Level.INFO, "- Stop %s plugin", pEntry.getKey()); CodeUtils.executeIgnoringExceptions(() -> pEntry.getValue().stopService(), @@ -395,7 +422,10 @@ public synchronized void stop() { } public Collection getPlugins() { - return Collections.unmodifiableCollection(plugins.values()); + final List allPlugins = new ArrayList<>(plugins.values()); + if (pluginManager != null) + allPlugins.addAll(pluginManager.getPlugins()); + return Collections.unmodifiableCollection(allPlugins); } public ServerDatabase getDatabase(final String databaseName) { @@ -502,7 +532,7 @@ public String toString() { } public ServerDatabase getDatabase(final String databaseName, final boolean createIfNotExists, - final boolean allowLoad) { + final boolean allowLoad) { if (databaseName == null || databaseName.trim().isEmpty()) throw new IllegalArgumentException("Invalid database name " + databaseName); @@ -523,7 +553,7 @@ public ServerDatabase getDatabase(final String databaseName, final boolean creat ComponentFile.MODE defaultDbMode = configuration.getValueAsEnum(GlobalConfiguration.SERVER_DEFAULT_DATABASE_MODE, - ComponentFile.MODE.class); + ComponentFile.MODE.class); if (defaultDbMode == null) defaultDbMode = READ_WRITE; @@ -620,46 +650,46 @@ private void loadDefaultDatabases() { final String commandParams = command.substring(commandSeparator + 1); switch (commandType) { - case "restore": - // DROP THE DATABASE BECAUSE THE RESTORE OPERATION WILL TAKE CARE OF CREATING A NEW DATABASE - if (database != null) { - ((DatabaseInternal) database).getEmbedded().drop(); - databases.remove(dbName); - } - final String dbPath = - configuration.getValueAsString(GlobalConfiguration.SERVER_DATABASE_DIRECTORY) + File.separator + dbName; + case "restore": + // DROP THE DATABASE BECAUSE THE RESTORE OPERATION WILL TAKE CARE OF CREATING A NEW DATABASE + if (database != null) { + ((DatabaseInternal) database).getEmbedded().drop(); + databases.remove(dbName); + } + final String dbPath = + configuration.getValueAsString(GlobalConfiguration.SERVER_DATABASE_DIRECTORY) + File.separator + dbName; // new Restore(commandParams, dbPath).restoreDatabase(); - try { - final Class clazz = Class.forName("com.arcadedb.integration.restore.Restore"); - final Object restorer = clazz.getConstructor(String.class, String.class).newInstance(commandParams, - dbPath); + try { + final Class clazz = Class.forName("com.arcadedb.integration.restore.Restore"); + final Object restorer = clazz.getConstructor(String.class, String.class).newInstance(commandParams, + dbPath); - clazz.getMethod("restoreDatabase").invoke(restorer); + clazz.getMethod("restoreDatabase").invoke(restorer); - } catch (final ClassNotFoundException | NoSuchMethodException | IllegalAccessException | - InstantiationException e) { - throw new CommandExecutionException("Error on restoring database, restore libs not found in " + - "classpath", e); - } catch (final InvocationTargetException e) { - throw new CommandExecutionException("Error on restoring database", e.getTargetException()); - } + } catch (final ClassNotFoundException | NoSuchMethodException | IllegalAccessException | + InstantiationException e) { + throw new CommandExecutionException("Error on restoring database, restore libs not found in " + + "classpath", e); + } catch (final InvocationTargetException e) { + throw new CommandExecutionException("Error on restoring database", e.getTargetException()); + } - getDatabase(dbName); - break; + getDatabase(dbName); + break; - case "import": - if (database == null) { - // CREATE THE DATABASE - LogManager.instance().log(this, Level.INFO, "Creating default database '%s'...", null, dbName); - database = createDatabase(dbName, defaultDbMode); - } - database.command("sql", "import database " + commandParams); - break; + case "import": + if (database == null) { + // CREATE THE DATABASE + LogManager.instance().log(this, Level.INFO, "Creating default database '%s'...", null, dbName); + database = createDatabase(dbName, defaultDbMode); + } + database.command("sql", "import database " + commandParams); + break; - default: - LogManager.instance().log(this, Level.SEVERE, "Unsupported command %s in startup command: '%s'", null - , commandType); + default: + LogManager.instance().log(this, Level.SEVERE, "Unsupported command %s in startup command: '%s'", null + , commandType); } } } else { @@ -700,7 +730,7 @@ private void parseCredentials(final String dbName, final String credentials) { user = security.authenticate(userName, userPassword, dbName); // UPDATE DB LIST + GROUP - user.addDatabase(dbName, new String[]{userGroup}); + user.addDatabase(dbName, new String[] { userGroup }); security.saveUsers(); } catch (final ServerSecurityException e) { @@ -711,7 +741,7 @@ private void parseCredentials(final String dbName, final String credentials) { } } else { // UPDATE DB LIST - user.addDatabase(dbName, new String[]{userGroup}); + user.addDatabase(dbName, new String[] { userGroup }); security.saveUsers(); } } else { @@ -722,7 +752,7 @@ private void parseCredentials(final String dbName, final String credentials) { // UPDATE DB LIST + GROUP ServerSecurityUser user = security.getUser(userName); - user.addDatabase(dbName, new String[]{userGroup}); + user.addDatabase(dbName, new String[] { userGroup }); security.saveUsers(); } } @@ -745,6 +775,7 @@ private void loadConfiguration() { private void init() { eventLog = new FileServerEventLog(this); + pluginManager = new PluginManager(this, configuration); Runtime.getRuntime().addShutdownHook(new Thread(() -> { // Mark logger as shutting down to prevent NPE when handlers are closed (issue #2813) diff --git a/server/src/main/java/com/arcadedb/server/ServerPlugin.java b/server/src/main/java/com/arcadedb/server/ServerPlugin.java index 746993ed7a..9e31a180dc 100644 --- a/server/src/main/java/com/arcadedb/server/ServerPlugin.java +++ b/server/src/main/java/com/arcadedb/server/ServerPlugin.java @@ -22,10 +22,14 @@ import com.arcadedb.server.http.HttpServer; import io.undertow.server.handlers.PathHandler; -import static com.arcadedb.server.ServerPlugin.INSTALLATION_PRIORITY.BEFORE_HTTP_ON; +import static com.arcadedb.server.ServerPlugin.PluginInstallationPriority.BEFORE_HTTP_ON; public interface ServerPlugin { - enum INSTALLATION_PRIORITY {BEFORE_HTTP_ON, AFTER_HTTP_ON, AFTER_DATABASES_OPEN} + enum PluginInstallationPriority {BEFORE_HTTP_ON, AFTER_HTTP_ON, AFTER_DATABASES_OPEN} + + default String getName() { + return this.getClass().getSimpleName(); + } default void configure(ArcadeDBServer arcadeDBServer, ContextConfiguration configuration) { // DEFAULT IMPLEMENTATION @@ -41,7 +45,7 @@ default void registerAPI(final HttpServer httpServer, final PathHandler routes) // DEFAULT IMPLEMENTATION } - default INSTALLATION_PRIORITY getInstallationPriority() { + default PluginInstallationPriority getInstallationPriority() { return BEFORE_HTTP_ON; } } diff --git a/server/src/main/java/com/arcadedb/server/backup/AutoBackupSchedulerPlugin.java b/server/src/main/java/com/arcadedb/server/backup/AutoBackupSchedulerPlugin.java index 1509a42313..14ff5d58b5 100644 --- a/server/src/main/java/com/arcadedb/server/backup/AutoBackupSchedulerPlugin.java +++ b/server/src/main/java/com/arcadedb/server/backup/AutoBackupSchedulerPlugin.java @@ -193,9 +193,9 @@ public void stopService() { } @Override - public INSTALLATION_PRIORITY getInstallationPriority() { + public PluginInstallationPriority getInstallationPriority() { // Install after databases are open so we can schedule backups for all databases - return INSTALLATION_PRIORITY.AFTER_DATABASES_OPEN; + return PluginInstallationPriority.AFTER_DATABASES_OPEN; } /** diff --git a/server/src/main/java/com/arcadedb/server/plugin/PluginClassLoader.java b/server/src/main/java/com/arcadedb/server/plugin/PluginClassLoader.java new file mode 100644 index 0000000000..f218557469 --- /dev/null +++ b/server/src/main/java/com/arcadedb/server/plugin/PluginClassLoader.java @@ -0,0 +1,74 @@ +/* + * Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-FileCopyrightText: 2021-present Arcade Data Ltd (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.server.plugin; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; + +/** + * Custom class loader for plugins that provides isolation while allowing access to server APIs. + *

+ * This class loader follows a parent-first delegation model for server classes (com.arcadedb.server.*) + * and a child-first model for plugin-specific classes, allowing each plugin to have its own + * version of dependencies. + * + * @author Luca Garulli (l.garulli@arcadedata.com) + */ +public class PluginClassLoader extends URLClassLoader { + private static final String SERVER_PACKAGE_PREFIX = "com.arcadedb."; + + public PluginClassLoader(final String pluginName, final File pluginJarFile, final ClassLoader parent) + throws MalformedURLException { + super(new URL[]{pluginJarFile.toURI().toURL()}, parent); + } + + @Override + protected Class loadClass(final String name, final boolean resolve) throws ClassNotFoundException { + // Always delegate server API classes to parent to ensure shared instances + if (name.startsWith(SERVER_PACKAGE_PREFIX)) { + return super.loadClass(name, resolve); + } + + // For plugin classes, try to load from this class loader first + synchronized (getClassLoadingLock(name)) { + // Check if the class has already been loaded by this class loader + Class c = findLoadedClass(name); + if (c == null) { + try { + // Try to load from this class loader's JAR first + c = findClass(name); + } catch (final ClassNotFoundException e) { + // If not found, delegate to parent + c = super.loadClass(name, resolve); + } + } + if (resolve) { + resolveClass(c); + } + return c; + } + } + + @Override + public String toString() { + return "PluginClassLoader{urls=" + java.util.Arrays.toString(getURLs()) + "}"; + } +} diff --git a/server/src/main/java/com/arcadedb/server/plugin/PluginDescriptor.java b/server/src/main/java/com/arcadedb/server/plugin/PluginDescriptor.java new file mode 100644 index 0000000000..75fbe747fa --- /dev/null +++ b/server/src/main/java/com/arcadedb/server/plugin/PluginDescriptor.java @@ -0,0 +1,74 @@ +/* + * Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-FileCopyrightText: 2021-present Arcade Data Ltd (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.server.plugin; + +import com.arcadedb.server.ServerPlugin; + +import java.util.Objects; + +/** + * Descriptor for a plugin that provides metadata and lifecycle management. + * Each plugin is loaded in its own class loader for isolation. + * + * @author Luca Garulli (l.garulli@arcadedata.com) + */ +public class PluginDescriptor { + private final String pluginName; + private final ClassLoader classLoader; + private ServerPlugin pluginInstance; + private boolean started; + + public PluginDescriptor(final String pluginName, final ClassLoader classLoader) { + this.pluginName = Objects.requireNonNull(pluginName, "Plugin name cannot be null"); + this.classLoader = Objects.requireNonNull(classLoader, "Class loader cannot be null"); + this.started = false; + } + + public String getPluginName() { + return pluginName; + } + + public ClassLoader getClassLoader() { + return classLoader; + } + + public ServerPlugin getPluginInstance() { + return pluginInstance; + } + + public void setPluginInstance(final ServerPlugin pluginInstance) { + this.pluginInstance = pluginInstance; + } + + public boolean isStarted() { + return started; + } + + public void setStarted(final boolean started) { + this.started = started; + } + + @Override + public String toString() { + return "PluginDescriptor{" + + "pluginName='" + pluginName + '\'' + + ", started=" + started + + '}'; + } +} diff --git a/server/src/main/java/com/arcadedb/server/plugin/PluginManager.java b/server/src/main/java/com/arcadedb/server/plugin/PluginManager.java new file mode 100644 index 0000000000..66d0808dfd --- /dev/null +++ b/server/src/main/java/com/arcadedb/server/plugin/PluginManager.java @@ -0,0 +1,298 @@ +/* + * Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-FileCopyrightText: 2021-present Arcade Data Ltd (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.server.plugin; + +import com.arcadedb.ContextConfiguration; +import com.arcadedb.GlobalConfiguration; +import com.arcadedb.log.LogManager; +import com.arcadedb.server.ArcadeDBServer; +import com.arcadedb.server.ServerException; +import com.arcadedb.server.ServerPlugin; +import com.arcadedb.utility.CodeUtils; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; + +/** + * Manager for loading and managing plugins using isolated class loaders. + * Plugins are discovered from the lib/plugins directory using the ServiceLoader pattern. + * + * @author Luca Garulli (l.garulli@arcadedata.com) + */ +public class PluginManager { + private final ArcadeDBServer server; + private final ContextConfiguration configuration; + private final String pluginsDirectory; + private final Map plugins = new LinkedHashMap<>(); + private final Map classLoaderMap = new ConcurrentHashMap<>(); + private final Set configuredPlugins; + + public PluginManager(final ArcadeDBServer server, final ContextConfiguration configuration) { + this.server = server; + this.configuration = configuration; + this.pluginsDirectory = server.getRootPath() + File.separator + "lib" + File.separator + "plugins"; + configuredPlugins = getConfiguredPlugins(); + + } + + private Set getConfiguredPlugins() { + final String configuration = this.configuration.getValueAsString(GlobalConfiguration.SERVER_PLUGINS); + Set configuredPlugins = new HashSet<>(); + if (!configuration.isEmpty()) { + final String[] pluginEntries = configuration.split(","); + for (final String p : pluginEntries) { + final String[] pluginPair = p.split(":"); + + final String pluginName = pluginPair[0]; + configuredPlugins.add(pluginName); + final String pluginClass = pluginPair.length > 1 ? pluginPair[1] : pluginPair[0]; + configuredPlugins.add(pluginClass); + } + } + return configuredPlugins; + } + + private void discoverPluginsOnMainClassLoader() { + final ServiceLoader serviceLoader = ServiceLoader.load(ServerPlugin.class, getClass().getClassLoader()); + + for (ServerPlugin pluginInstance : serviceLoader) { + String name = pluginInstance.getClass().getSimpleName(); + if (configuredPlugins.contains(name) || configuredPlugins.contains(pluginInstance.getClass().getName())) { + // Register the plugin + final PluginDescriptor descriptor = new PluginDescriptor(name, getClass().getClassLoader()); + descriptor.setPluginInstance(pluginInstance); + plugins.put(name, descriptor); + + LogManager.instance().log(this, Level.INFO, "Discovered plugin on main class loader: %s", name); + } + } + } + + /** + * Discover and load plugins from the plugins directory. + * Each plugin JAR is loaded in its own isolated class loader. + */ + public void discoverPlugins() { + discoverPluginsOnMainClassLoader(); + + final File pluginsDir = new File(pluginsDirectory); + if (!pluginsDir.exists() || !pluginsDir.isDirectory()) { + LogManager.instance().log(this, Level.INFO, "Plugins directory not found: %s", pluginsDirectory); + return; + } + + final File[] pluginJars = pluginsDir.listFiles((dir, name) -> name.endsWith(".jar")); + if (pluginJars == null || pluginJars.length == 0) { + LogManager.instance().log(this, Level.INFO, "No plugin JARs found in: %s", pluginsDirectory); + return; + } + + for (final File pluginJar : pluginJars) { + try { + loadPlugin(pluginJar); + } catch (final Exception e) { + LogManager.instance().log(this, Level.SEVERE, "Failed to load plugin from: %s", e, pluginJar.getAbsolutePath()); + } + } + } + + /** + * Load a plugin from a JAR file using an isolated class loader. + */ + private void loadPlugin(final File pluginJar) throws Exception { + final String jarName = pluginJar.getName(); + final String pluginName = jarName.substring(0, jarName.lastIndexOf('.')); + + LogManager.instance().log(this, Level.FINE, "Loading plugin from: %s", pluginJar.getAbsolutePath()); + + // Create isolated class loader for this plugin + final PluginClassLoader classLoader = new PluginClassLoader(pluginName, pluginJar, getClass().getClassLoader()); + boolean registered = false; + + try { + // Use ServiceLoader to discover plugin implementations + final ServiceLoader serviceLoader = ServiceLoader.load(ServerPlugin.class, classLoader); + + // Load the first plugin implementation (typically only one per JAR) + for (ServerPlugin pluginInstance : serviceLoader) { + // Create plugin descriptor + final PluginDescriptor descriptor = new PluginDescriptor(pluginInstance.getName(), classLoader); + descriptor.setPluginInstance(pluginInstance); + + String name = pluginInstance.getName(); + LogManager.instance().log(this, Level.FINE, "Discovered plugin class: %s", name); + + if (plugins.containsKey(name)) { + LogManager.instance().log(this, Level.WARNING, "Plugin with name '%s' is already loaded, skipping duplicate from %s", + name, pluginJar.getName()); + break; // Exit loop - classloader will be closed in finally block + } + + if (configuredPlugins.contains(name) || configuredPlugins.contains(pluginName) || configuredPlugins.contains( + pluginInstance.getClass().getName())) { + // Register the plugin + plugins.put(name, descriptor); + classLoaderMap.put(classLoader, descriptor); + registered = true; + + LogManager.instance().log(this, Level.INFO, "Loaded plugin: %s from %s", name, pluginJar.getName()); + } else { + LogManager.instance().log(this, Level.INFO, "Skipping plugin: %s as not registered in configuration", name); + } + break; // Only load the first plugin from each JAR + } + } finally { + if (!registered) { + classLoader.close(); + } + } + } + + /** + * Start plugins based on their installation priority. + */ + public void startPlugins(final ServerPlugin.PluginInstallationPriority priority) { + for (final Map.Entry entry : plugins.entrySet()) { + final String pluginName = entry.getKey(); + final PluginDescriptor descriptor = entry.getValue(); + final ServerPlugin plugin = descriptor.getPluginInstance(); + + if (plugin == null || descriptor.isStarted()) { + continue; + } + + if (plugin.getInstallationPriority() != priority) { + continue; + } + + try { + // Set the context class loader to the plugin's class loader + final Thread currentThread = Thread.currentThread(); + final ClassLoader originalClassLoader = currentThread.getContextClassLoader(); + try { + currentThread.setContextClassLoader(descriptor.getClassLoader()); + + // Configure and start the plugin + plugin.configure(server, configuration); + plugin.startService(); + + descriptor.setStarted(true); + LogManager.instance().log(this, Level.INFO, "- %s plugin started", pluginName); + + } finally { + currentThread.setContextClassLoader(originalClassLoader); + } + } catch (final Exception e) { + throw new ServerException("Error starting plugin: " + pluginName + " (priority: " + priority + ")", e); + } + } + } + + /** + * Stop all plugins in reverse order of registration. + */ + public void stopPlugins() { + final List> pluginList = new ArrayList<>(plugins.entrySet()); + Collections.reverse(pluginList); + + for (final Map.Entry entry : pluginList) { + final String pluginName = entry.getKey(); + final PluginDescriptor descriptor = entry.getValue(); + final ServerPlugin plugin = descriptor.getPluginInstance(); + + if (plugin == null || !descriptor.isStarted()) { + continue; + } + + LogManager.instance().log(this, Level.INFO, "- Stop %s plugin", pluginName); + + final Thread currentThread = Thread.currentThread(); + final ClassLoader originalClassLoader = currentThread.getContextClassLoader(); + try { + currentThread.setContextClassLoader(descriptor.getClassLoader()); + CodeUtils.executeIgnoringExceptions(plugin::stopService, + "Error stopping plugin: " + pluginName, false); + descriptor.setStarted(false); + } finally { + currentThread.setContextClassLoader(originalClassLoader); + } + } + + // Close class loaders + for (final PluginDescriptor descriptor : plugins.values()) { + final ClassLoader classLoader = descriptor.getClassLoader(); + if (classLoader instanceof PluginClassLoader) { + try { + ((PluginClassLoader) classLoader).close(); + } catch (final IOException e) { + LogManager.instance().log(this, Level.WARNING, "Error closing class loader for plugin: %s", + e, descriptor.getPluginName()); + } + } + } + + plugins.clear(); + classLoaderMap.clear(); + } + + /** + * Get all loaded plugins. + */ + public Collection getPlugins() { + final List result = new ArrayList<>(); + for (final PluginDescriptor descriptor : plugins.values()) { + if (descriptor.getPluginInstance() != null) { + result.add(descriptor.getPluginInstance()); + } + } + return Collections.unmodifiableCollection(result); + } + + /** + * Get the number of loaded plugins. + */ + public int getPluginCount() { + return plugins.size(); + } + + /** + * Get plugin names. + */ + public Set getPluginNames() { + return Collections.unmodifiableSet(plugins.keySet()); + } + + /** + * Get plugin descriptor by name. + */ + public PluginDescriptor getPluginDescriptor(final String pluginName) { + return plugins.get(pluginName); + } +} diff --git a/server/src/main/java/com/arcadedb/server/plugin/package-info.java b/server/src/main/java/com/arcadedb/server/plugin/package-info.java new file mode 100644 index 0000000000..2611a801ab --- /dev/null +++ b/server/src/main/java/com/arcadedb/server/plugin/package-info.java @@ -0,0 +1,106 @@ +/* + * Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-FileCopyrightText: 2021-present Arcade Data Ltd (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Plugin architecture for ArcadeDB with isolated class loaders. + *

+ * This package provides infrastructure for loading and managing plugins in isolated class loaders, + * allowing each plugin to have its own set of dependencies without conflicts. + * + *

Architecture

+ *

+ * The plugin system consists of three main components: + *

    + *
  • {@link com.arcadedb.server.plugin.PluginManager} - Discovers and manages plugin lifecycle
  • + *
  • {@link com.arcadedb.server.plugin.PluginClassLoader} - Provides isolated class loading
  • + *
  • {@link com.arcadedb.server.plugin.PluginDescriptor} - Holds plugin metadata and state
  • + *
+ * + *

Plugin Discovery

+ *

+ * Plugins are discovered using the Java ServiceLoader pattern. Each plugin JAR must: + *

    + *
  1. Implement {@link com.arcadedb.server.ServerPlugin} interface
  2. + *
  3. Provide a META-INF/services/com.arcadedb.server.ServerPlugin file with the implementation class name
  4. + *
  5. Be placed in the {@code lib/plugins/} directory
  6. + *
+ * + *

Class Loading Strategy

+ *

+ * The {@link com.arcadedb.server.plugin.PluginClassLoader} uses a hybrid delegation model: + *

    + *
  • Server API classes (com.arcadedb.*) are loaded from the parent class loader (shared)
  • + *
  • Plugin-specific classes are loaded from the plugin's JAR first (isolated)
  • + *
  • Other classes fall back to parent class loader if not found in plugin JAR
  • + *
+ * + *

Plugin Lifecycle

+ *

+ * Plugins follow this lifecycle: + *

    + *
  1. Discovery - PluginManager scans lib/plugins/ directory
  2. + *
  3. Loading - Each plugin JAR gets its own PluginClassLoader
  4. + *
  5. Instantiation - ServiceLoader creates plugin instances
  6. + *
  7. Configuration - {@link com.arcadedb.server.ServerPlugin#configure} is called
  8. + *
  9. Starting - {@link com.arcadedb.server.ServerPlugin#startService} is called
  10. + *
  11. Running - Plugin provides its functionality
  12. + *
  13. Stopping - {@link com.arcadedb.server.ServerPlugin#stopService} is called
  14. + *
  15. Cleanup - ClassLoaders are closed
  16. + *
+ * + *

Creating a Plugin

+ *

+ * To create a new plugin: + *

{@code
+ * public class MyPlugin implements ServerPlugin {
+ *   @Override
+ *   public void configure(ArcadeDBServer server, ContextConfiguration config) {
+ *     // Initialize configuration
+ *   }
+ *
+ *   @Override
+ *   public void startService() {
+ *     // Start plugin services
+ *   }
+ *
+ *   @Override
+ *   public void stopService() {
+ *     // Stop plugin services
+ *   }
+ * }
+ * }
+ * + *

+ * Create {@code src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin}: + *

+ * com.example.MyPlugin
+ * 
+ * + *

Thread Safety

+ *

+ * The plugin system manages the thread context class loader during plugin operations to ensure + * proper class loading context. Plugin implementations should be thread-safe if they handle + * concurrent requests. + * + * @author Luca Garulli (l.garulli@arcadedata.com) + * @see com.arcadedb.server.ServerPlugin + * @see com.arcadedb.server.plugin.PluginManager + * @see com.arcadedb.server.plugin.PluginClassLoader + */ +package com.arcadedb.server.plugin; diff --git a/server/src/test/java/com/arcadedb/server/plugin/PluginManagerTest.java b/server/src/test/java/com/arcadedb/server/plugin/PluginManagerTest.java new file mode 100644 index 0000000000..2d6488e790 --- /dev/null +++ b/server/src/test/java/com/arcadedb/server/plugin/PluginManagerTest.java @@ -0,0 +1,437 @@ +/* + * Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-FileCopyrightText: 2021-present Arcade Data Ltd (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.server.plugin; + +import com.arcadedb.ContextConfiguration; +import com.arcadedb.GlobalConfiguration; +import com.arcadedb.server.ArcadeDBServer; +import com.arcadedb.server.ServerException; +import com.arcadedb.server.ServerPlugin; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test for PluginManager to verify plugin discovery and loading with isolated class loaders. + * + * @author Luca Garulli (l.garulli@arcadedata.com) + */ +public class PluginManagerTest { + private ArcadeDBServer server; + private PluginManager pluginManager; + + @TempDir + Path tempDir; + + @BeforeEach + public void setup() { + final ContextConfiguration configuration = new ContextConfiguration(); + configuration.setValue(GlobalConfiguration.SERVER_ROOT_PATH, tempDir.toString()); + configuration.setValue(GlobalConfiguration.SERVER_DATABASE_DIRECTORY, tempDir.resolve("databases").toString()); + configuration.setValue(GlobalConfiguration.SERVER_PLUGINS, + TestPlugin1.class.getSimpleName() + "," + + TestPlugin2.class.getSimpleName() + "," + + LifecycleTestPlugin.class.getSimpleName() + "," + + AfterHttpPlugin.class.getSimpleName() + "," + + FailingPlugin.class.getSimpleName() + "," + + OrderTestPlugin1.class.getSimpleName() + "," + + OrderTestPlugin2.class.getSimpleName() + "," + + OrderTestPlugin3.class.getSimpleName() + "," + + BeforeHttpPlugin.class.getSimpleName()); + + server = new ArcadeDBServer(configuration); + pluginManager = new PluginManager(server, configuration); + } + + @AfterEach + public void teardown() { + if (pluginManager != null) { + pluginManager.stopPlugins(); + } + if (server != null && server.isStarted()) { + server.stop(); + } + } + + @Test + public void testPluginManagerCreation() { + assertNotNull(pluginManager); + assertEquals(0, pluginManager.getPluginCount()); + } + + @Test + public void testDiscoverPluginsWithNoDirectory() { + // Should handle missing plugins directory gracefully + pluginManager.discoverPlugins(); + assertEquals(0, pluginManager.getPluginCount()); + } + + @Test + public void testGetPluginNames() { + final Collection names = pluginManager.getPluginNames(); + assertNotNull(names); + assertTrue(names.isEmpty()); + } + + @Test + public void testGetPlugins() { + final Collection plugins = pluginManager.getPlugins(); + assertNotNull(plugins); + assertTrue(plugins.isEmpty()); + } + + @Test + public void testStopPluginsWhenEmpty() { + // Should handle stopping with no plugins loaded + assertDoesNotThrow(() -> pluginManager.stopPlugins()); + } + + @Test + public void testDiscoverPluginsWithEmptyDirectory() throws IOException { + // Create empty plugins directory + final Path pluginsDir = tempDir.resolve("lib/plugins"); + Files.createDirectories(pluginsDir); + + pluginManager.discoverPlugins(); + assertEquals(0, pluginManager.getPluginCount()); + } + + @Test + public void testLoadPluginWithMetaInfServices() throws Exception { + // Create a test plugin JAR with proper META-INF/services + final Path pluginsDir = tempDir.resolve("lib/plugins"); + Files.createDirectories(pluginsDir); + + final File pluginJar = createTestPluginJar(pluginsDir, "test-plugin", TestPlugin1.class); + + pluginManager.discoverPlugins(); + + assertEquals(1, pluginManager.getPluginCount()); + assertTrue(pluginManager.getPluginNames().contains(TestPlugin1.class.getSimpleName())); + + final Collection plugins = pluginManager.getPlugins(); + assertEquals(1, plugins.size()); + } + + @Test + public void testLoadMultiplePlugins() throws Exception { + final Path pluginsDir = tempDir.resolve("lib/plugins"); + Files.createDirectories(pluginsDir); + + createTestPluginJar(pluginsDir, "plugin1", TestPlugin1.class); + createTestPluginJar(pluginsDir, "plugin2", TestPlugin2.class); + + pluginManager.discoverPlugins(); + + assertEquals(2, pluginManager.getPluginCount()); + final Set names = pluginManager.getPluginNames(); + assertTrue(names.contains(TestPlugin1.class.getSimpleName())); + assertTrue(names.contains(TestPlugin2.class.getSimpleName())); + } + + @Test + public void testPluginLifecycle() throws Exception { + final Path pluginsDir = tempDir.resolve("lib/plugins"); + Files.createDirectories(pluginsDir); + + createTestPluginJar(pluginsDir, "lifecycle-plugin", LifecycleTestPlugin.class); + + pluginManager.discoverPlugins(); + assertEquals(1, pluginManager.getPluginCount()); + + // Start the plugin + pluginManager.startPlugins(ServerPlugin.PluginInstallationPriority.BEFORE_HTTP_ON); + + // Verify plugin was configured and started + final PluginDescriptor descriptor = pluginManager.getPluginDescriptor(LifecycleTestPlugin.class.getSimpleName()); + assertNotNull(descriptor); + assertTrue(descriptor.isStarted()); + assertTrue(descriptor.getPluginInstance() instanceof LifecycleTestPlugin); + + final LifecycleTestPlugin plugin = (LifecycleTestPlugin) descriptor.getPluginInstance(); + assertTrue(plugin.configured.get()); + assertTrue(plugin.started.get()); + assertFalse(plugin.stopped.get()); + + // Stop the plugin + pluginManager.stopPlugins(); + assertTrue(plugin.stopped.get()); + assertFalse(descriptor.isStarted()); + } + + @Test + public void testPluginStartOrderByPriority() throws Exception { + final Path pluginsDir = tempDir.resolve("lib/plugins"); + Files.createDirectories(pluginsDir); + + createTestPluginJar(pluginsDir, "before-plugin", BeforeHttpPlugin.class); + createTestPluginJar(pluginsDir, "after-plugin", AfterHttpPlugin.class); + + pluginManager.discoverPlugins(); + assertEquals(2, pluginManager.getPluginCount()); + + // Start BEFORE_HTTP_ON plugins + pluginManager.startPlugins(ServerPlugin.PluginInstallationPriority.BEFORE_HTTP_ON); + + PluginDescriptor beforeDesc = pluginManager.getPluginDescriptor(BeforeHttpPlugin.class.getSimpleName()); + PluginDescriptor afterDesc = pluginManager.getPluginDescriptor(AfterHttpPlugin.class.getSimpleName()); + + assertTrue(beforeDesc.isStarted()); + assertFalse(afterDesc.isStarted()); + + // Start AFTER_HTTP_ON plugins + pluginManager.startPlugins(ServerPlugin.PluginInstallationPriority.AFTER_HTTP_ON); + + assertTrue(beforeDesc.isStarted()); + assertTrue(afterDesc.isStarted()); + } + + @Test + public void testPluginWithoutMetaInfServices() throws Exception { + final Path pluginsDir = tempDir.resolve("lib/plugins"); + Files.createDirectories(pluginsDir); + + // Create JAR without META-INF/services + final File pluginJar = pluginsDir.resolve("invalid-plugin.jar").toFile(); + try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(pluginJar))) { + // Just create an empty JAR + jos.putNextEntry(new JarEntry("dummy.txt")); + jos.write("test".getBytes()); + jos.closeEntry(); + } + + pluginManager.discoverPlugins(); + + // Plugin should not be loaded due to missing META-INF/services + assertEquals(0, pluginManager.getPluginCount()); + } + + @Test + public void testPluginStartException() throws Exception { + final Path pluginsDir = tempDir.resolve("lib/plugins"); + Files.createDirectories(pluginsDir); + + createTestPluginJar(pluginsDir, "failing-plugin", FailingPlugin.class); + + pluginManager.discoverPlugins(); + assertEquals(1, pluginManager.getPluginCount()); + + // Starting the plugin should throw exception + assertThrows(ServerException.class, () -> + pluginManager.startPlugins(ServerPlugin.PluginInstallationPriority.BEFORE_HTTP_ON)); + } + + @Test + public void testGetPluginDescriptor() throws Exception { + final Path pluginsDir = tempDir.resolve("lib/plugins"); + Files.createDirectories(pluginsDir); + + createTestPluginJar(pluginsDir, "test-plugin", TestPlugin1.class); + + pluginManager.discoverPlugins(); + + final PluginDescriptor descriptor = pluginManager.getPluginDescriptor(TestPlugin1.class.getSimpleName()); + assertNotNull(descriptor); + assertEquals(TestPlugin1.class.getSimpleName(), descriptor.getPluginName()); + assertNotNull(descriptor.getClassLoader()); + assertNotNull(descriptor.getPluginInstance()); + assertFalse(descriptor.isStarted()); + } + + @Test + public void testClassLoaderIsolation() throws Exception { + final Path pluginsDir = tempDir.resolve("lib/plugins"); + Files.createDirectories(pluginsDir); + + createTestPluginJar(pluginsDir, "plugin1", TestPlugin1.class); + createTestPluginJar(pluginsDir, "plugin2", TestPlugin2.class); + + pluginManager.discoverPlugins(); + + final PluginDescriptor desc1 = pluginManager.getPluginDescriptor(TestPlugin1.class.getSimpleName()); + final PluginDescriptor desc2 = pluginManager.getPluginDescriptor(TestPlugin2.class.getSimpleName()); + + // Each plugin should have its own class loader + assertNotNull(desc1.getClassLoader()); + assertNotNull(desc2.getClassLoader()); + assertNotSame(desc1.getClassLoader(), desc2.getClassLoader()); + + // Both should be PluginClassLoader instances + assertTrue(desc1.getClassLoader() instanceof PluginClassLoader); + assertTrue(desc2.getClassLoader() instanceof PluginClassLoader); + } + + /** + * Helper method to create a test plugin JAR with proper META-INF/services + */ + private File createTestPluginJar(final Path pluginsDir, + final String pluginName, + final Class pluginClass) + throws Exception { + final File jarFile = pluginsDir.resolve(pluginName + ".jar").toFile(); + + try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarFile))) { + // Add the plugin class + final String classFileName = pluginClass.getName().replace('.', '/') + ".class"; + jos.putNextEntry(new JarEntry(classFileName)); + + // Load class bytes from current classloader + try (InputStream is = getClass().getClassLoader().getResourceAsStream(classFileName)) { + if (is != null) { + is.transferTo(jos); + } + } + jos.closeEntry(); + + // Add META-INF/services/com.arcadedb.server.ServerPlugin + jos.putNextEntry(new JarEntry("META-INF/services/com.arcadedb.server.ServerPlugin")); + jos.write(pluginClass.getName().getBytes()); + jos.closeEntry(); + } + + return jarFile; + } + + // Test plugin implementations + public static class TestPlugin1 implements ServerPlugin { + @Override + public void startService() { + } + } + + public static class TestPlugin2 implements ServerPlugin { + @Override + public void startService() { + } + } + + public static class LifecycleTestPlugin implements ServerPlugin { + public final AtomicBoolean configured = new AtomicBoolean(false); + public final AtomicBoolean started = new AtomicBoolean(false); + public final AtomicBoolean stopped = new AtomicBoolean(false); + + @Override + public void configure(ArcadeDBServer arcadeDBServer, ContextConfiguration configuration) { + configured.set(true); + } + + @Override + public void startService() { + started.set(true); + } + + @Override + public void stopService() { + stopped.set(true); + } + } + + public static class BeforeHttpPlugin implements ServerPlugin { + @Override + public void startService() { + } + + @Override + public PluginInstallationPriority getInstallationPriority() { + return PluginInstallationPriority.BEFORE_HTTP_ON; + } + } + + public static class AfterHttpPlugin implements ServerPlugin { + @Override + public void startService() { + } + + @Override + public PluginInstallationPriority getInstallationPriority() { + return PluginInstallationPriority.AFTER_HTTP_ON; + } + } + + public static class FailingPlugin implements ServerPlugin { + @Override + public void startService() { + throw new RuntimeException("Plugin failed to start"); + } + } + + public static class OrderTestPlugin1 implements ServerPlugin { + public static final AtomicInteger stopCounter = new AtomicInteger(0); + public static final AtomicInteger stopOrder = new AtomicInteger(0); + + @Override + public void startService() { + } + + @Override + public void stopService() { + stopOrder.set(stopCounter.incrementAndGet()); + } + } + + public static class OrderTestPlugin2 implements ServerPlugin { + public static final AtomicInteger stopCounter = new AtomicInteger(0); + public static final AtomicInteger stopOrder = new AtomicInteger(0); + + @Override + public void startService() { + } + + @Override + public void stopService() { + stopOrder.set(stopCounter.incrementAndGet()); + } + } + + public static class OrderTestPlugin3 implements ServerPlugin { + public static final AtomicInteger stopCounter = new AtomicInteger(0); + public static final AtomicInteger stopOrder = new AtomicInteger(0); + + @Override + public void startService() { + } + + @Override + public void stopService() { + stopOrder.set(stopCounter.incrementAndGet()); + } + } +}