diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 2fec4921cdb..7378830bd95 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -27,6 +27,14 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima * Added Gremlator, a single page web application, that translates Gremlin into various programming languages like Javascript and Python. * Removed `uuid` dependency from `gremlin-javascript` in favor of the built-in `globalThis.crypto.randomUUID()`. +* Added declarative `traversalSources` configuration to Gremlin Server YAML for creating `TraversalSource` instances with optional strategy configuration via `gremlinExpression`. +* Added Java-based `lifecycleHooks` configuration to Gremlin Server YAML, replacing Groovy init script `LifeCycleHook` creation. +* Added `TinkerFactoryDataLoader` `LifeCycleHook` implementation for loading sample datasets without Groovy. +* Added auto-creation of `TraversalSource` bindings from `graphs` configuration (`graph` maps to `g`, others to `g_`). +* Added `GraphManager` to `LifeCycleHook.Context` for Java-based hooks to access configured graphs. +* Deprecated Groovy-based `LifeCycleHook` and `TraversalSource` creation via init scripts in favor of YAML configuration. +* Updated all default Gremlin Server configs to remove Groovy dependency from initialization. +* Added script engine allowlist to Gremlin Server - the `scriptEngines` YAML configuration now restricts which engines can serve requests; `gremlin-lang` is always available. [[release-4-0-0-beta-2]] === TinkerPop 4.0.0-beta.2 (April 1, 2026) diff --git a/docker/gremlin-server/gremlin-server-integration-krb5.yaml b/docker/gremlin-server/gremlin-server-integration-krb5.yaml index 6dfd94dfc50..974c94a8cc5 100644 --- a/docker/gremlin-server/gremlin-server-integration-krb5.yaml +++ b/docker/gremlin-server/gremlin-server-integration-krb5.yaml @@ -27,14 +27,21 @@ graphs: { sink: conf/tinkergraph-empty.properties, tx: conf/tinkertransactiongraph-empty.properties } -scriptEngines: { - gremlin-lang : {}, - gremlin-groovy: { - plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.groovy.jsr223.GroovyCompilerGremlinPlugin: {expectedCompilationTime: 30000}, - org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/generate-all.groovy]}}}} +traversalSources: { + gclassic: {graph: classic}, + gmodern: {graph: modern}, + g: {graph: graph}, + gcrew: {graph: crew}, + ggraph: {graph: graph}, + ggrateful: {graph: grateful}, + gsink: {graph: sink}, + gtx: {graph: tx}} +lifecycleHooks: + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: classic, dataset: classic}} + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: modern, dataset: modern}} + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: crew, dataset: crew}} + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: grateful, dataset: grateful}} + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: sink, dataset: sink}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV2, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV2] }} diff --git a/docker/gremlin-server/gremlin-server-integration-secure.yaml b/docker/gremlin-server/gremlin-server-integration-secure.yaml index 6528551cb9b..9dfa96903fb 100644 --- a/docker/gremlin-server/gremlin-server-integration-secure.yaml +++ b/docker/gremlin-server/gremlin-server-integration-secure.yaml @@ -27,14 +27,21 @@ graphs: { sink: conf/tinkergraph-empty.properties, tx: conf/tinkertransactiongraph-empty.properties } -scriptEngines: { - gremlin-lang : {}, - gremlin-groovy: { - plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.groovy.jsr223.GroovyCompilerGremlinPlugin: {expectedCompilationTime: 30000}, - org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/generate-all.groovy]}}}} +traversalSources: { + gclassic: {graph: classic}, + gmodern: {graph: modern}, + g: {graph: graph}, + gcrew: {graph: crew}, + ggraph: {graph: graph}, + ggrateful: {graph: grateful}, + gsink: {graph: sink}, + gtx: {graph: tx}} +lifecycleHooks: + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: classic, dataset: classic}} + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: modern, dataset: modern}} + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: crew, dataset: crew}} + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: grateful, dataset: grateful}} + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: sink, dataset: sink}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV4, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/json - { className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV4 } # application/vnd.graphbinary-v1.0 diff --git a/docker/gremlin-server/gremlin-server-integration.yaml b/docker/gremlin-server/gremlin-server-integration.yaml index 8f5d2f74f83..e5e0141b680 100644 --- a/docker/gremlin-server/gremlin-server-integration.yaml +++ b/docker/gremlin-server/gremlin-server-integration.yaml @@ -29,14 +29,29 @@ graphs: { sink: conf/tinkergraph-service.properties, tx: conf/tinkertransactiongraph-service.properties } +traversalSources: { + gclassic: {graph: classic}, + gmodern: {graph: modern}, + g: {graph: graph}, + gcrew: {graph: crew}, + ggraph: {graph: graph}, + ggrateful: {graph: grateful}, + gsink: {graph: sink}, + gtx: {graph: tx}, + gimmutable: {graph: immutable}} +lifecycleHooks: + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: classic, dataset: classic}} + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: modern, dataset: modern}} + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: crew, dataset: crew}} + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: grateful, dataset: grateful}} + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: sink, dataset: sink}} scriptEngines: { gremlin-lang : {}, gremlin-groovy: { plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, org.apache.tinkerpop.gremlin.groovy.jsr223.GroovyCompilerGremlinPlugin: {expectedCompilationTime: 30000}, - org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/generate-all.groovy]}}}} + org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}}}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV4, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/json - { className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV4 } # application/vnd.graphbinary-v1.0 diff --git a/docs/src/reference/gremlin-applications.asciidoc b/docs/src/reference/gremlin-applications.asciidoc index 8039afff907..cbf32f24889 100644 --- a/docs/src/reference/gremlin-applications.asciidoc +++ b/docs/src/reference/gremlin-applications.asciidoc @@ -444,22 +444,22 @@ $ bin/gremlin-server.sh conf/gremlin-server-modern.yaml (o o) -----oOOo-(4)-oOOo----- -[INFO] GremlinServer - Configuring Gremlin Server from conf/gremlin-server-modern.yaml -[INFO] MetricManager - Configured Metrics Slf4jReporter configured with interval=180000ms and loggerName=org.apache.tinkerpop.gremlin.server.Settings$Slf4jReporterMetrics -[INFO] DefaultGraphManager - Graph [graph] was successfully configured via [conf/tinkergraph-empty.properties]. -[INFO] ServerGremlinExecutor - Initialized Gremlin thread pool. Threads in pool named with pattern gremlin-* -[INFO] ServerGremlinExecutor - Initialized GremlinExecutor and preparing GremlinScriptEngines instances. -[INFO] ServerGremlinExecutor - Initialized gremlin-groovy GremlinScriptEngine and registered metrics -[INFO] ServerGremlinExecutor - A GraphTraversalSource is now bound to [g] with graphtraversalsource[tinkergraph[vertices:0 edges:0], standard] -[INFO] GremlinServer - Executing start up LifeCycleHook -[INFO] Logger$info - Loading 'modern' graph data. -[INFO] GremlinServer - idleConnectionTimeout was set to 0 which resolves to 0 seconds when configuring this value - this feature will be disabled -[INFO] GremlinServer - keepAliveInterval was set to 0 which resolves to 0 seconds when configuring this value - this feature will be disabled -[INFO] AbstractChannelizer - Configured application/vnd.gremlin-v4.0+json with org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV4 -[INFO] AbstractChannelizer - Configured application/json with org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV4 -[INFO] AbstractChannelizer - Configured application/vnd.graphbinary-v4.0 with org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV4 -[INFO] GremlinServer$1 - Gremlin Server configured with worker thread pool of 1, gremlin pool of 4 and boss thread pool of 1. -[INFO] GremlinServer$1 - Channel started at port 8182. +[INFO] o.a.t.g.s.GremlinServer - Configuring Gremlin Server from conf/gremlin-server-modern.yaml +[INFO] o.a.t.g.s.u.MetricManager - Configured Metrics Slf4jReporter configured with interval=180000ms and loggerName=org.apache.tinkerpop.gremlin.server.Settings$Slf4jReporterMetrics +[INFO] o.a.t.g.s.u.DefaultGraphManager - Graph [graph] was successfully configured via [conf/tinkergraph-empty.properties]. +[INFO] o.a.t.g.s.u.ServerGremlinExecutor - Initialized Gremlin thread pool. Threads in pool named with pattern gremlin-* +[INFO] o.a.t.g.s.u.ServerGremlinExecutor - Initialized GremlinExecutor and preparing GremlinScriptEngines instances. +[INFO] o.a.t.g.s.u.ServerGremlinExecutor - Initialized gremlin-lang GremlinScriptEngine and registered metrics +[INFO] o.a.t.g.s.u.ServerGremlinExecutor - A GraphTraversalSource is now auto-bound to [g] for graph [graph] +[INFO] o.a.t.g.s.u.ServerGremlinExecutor - Instantiated LifeCycleHook: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader +[INFO] o.a.t.g.s.h.TransactionManager - TransactionManager initialized with timeout=600000ms, maxTransactions=1000 +[INFO] o.a.t.g.s.GremlinServer - Executing start up LifeCycleHook +[INFO] o.a.t.g.s.u.TinkerFactoryDataLoader - TinkerFactoryDataLoader loaded [modern] dataset into graph [graph] +[INFO] o.a.t.g.s.AbstractChannelizer - Configured application/vnd.gremlin-v4.0+json with org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV4 +[INFO] o.a.t.g.s.AbstractChannelizer - Configured application/json with org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV4 +[INFO] o.a.t.g.s.AbstractChannelizer - Configured application/vnd.graphbinary-v4.0 with org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV4 +[INFO] o.a.t.g.s.GremlinServer - Gremlin Server configured with worker thread pool of 1, gremlin pool of 10 and boss thread pool of 1. +[INFO] o.a.t.g.s.GremlinServer - Channel started at port 8182. ---- Gremlin Server is configured by the provided link:http://www.yaml.org/[YAML] file `conf/gremlin-server-modern.yaml`. @@ -469,31 +469,93 @@ That file tells Gremlin Server many things such as: * Thread pool sizes * Where to report metrics gathered by the server * The serializers to make available -* The Gremlin `ScriptEngine` instances to expose and external dependencies to inject into them * `Graph` instances to expose +* `TraversalSource` bindings (auto-created or explicitly declared) +* `LifeCycleHook` implementations for startup/shutdown logic The log messages that printed above show a number of things, but most importantly, there is a `Graph` instance named `graph` that is exposed in Gremlin Server. This graph is an in-memory TinkerGraph and was empty at the start of the -server. An initialization script at `scripts/generate-modern.groovy` was executed during startup. Its contents are -as follows: +server. A `TinkerFactoryDataLoader` lifecycle hook loaded the "modern" dataset into it during startup, and a +`TraversalSource` named `g` was auto-created from the `graph` entry. -[source,groovy] +[[server-auto-traversal-sources]] +==== Auto-Created TraversalSources + +When Gremlin Server starts, it automatically creates a `TraversalSource` for each graph in the `graphs` configuration +that does not have an explicit entry in the `traversalSources` section. The naming convention is: + +* A graph named `graph` gets a `TraversalSource` named `g` +* All other graphs get `g_` (e.g. a graph named `modern` gets `g_modern`) + +This means a minimal configuration like the following is fully functional: + +[source,yaml] +---- +graphs: { + graph: conf/tinkergraph-empty.properties} +---- + +[[server-traversal-sources]] +==== Declarative TraversalSources + +For more control, the `traversalSources` YAML section allows explicit `TraversalSource` creation with optional +strategy configuration via a Gremlin expression: + +[source,yaml] +---- +traversalSources: { + g: {graph: graph}, + gReadOnly: {graph: graph, gremlinExpression: "g.withStrategies(ReadOnlyStrategy)", language: "gremlin-lang"}} +---- + +Each entry supports: + +* `graph` (required) — references a key in the `graphs` section +* `gremlinExpression` (optional) — a Gremlin expression evaluated with a base traversal source bound as `g`; the + result becomes the final `TraversalSource` +* `language` (optional) — which script engine to use for expression evaluation; defaults to `gremlin-lang`, or the + sole configured engine if only one is present + +Graphs with explicit `traversalSources` entries are excluded from auto-creation. + +[[server-lifecycle-hooks]] +==== LifeCycleHooks + +The `lifecycleHooks` YAML section configures Java-based `LifeCycleHook` implementations that execute during server +startup and shutdown: + +[source,yaml] ---- -include::{basedir}/gremlin-server/scripts/generate-modern.groovy[] +lifecycleHooks: + - className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader + config: {graph: graph, dataset: modern} ---- -The script above initializes a `Map` and assigns two key/values to it. The first, assigned to "hook", defines a -`LifeCycleHook` for Gremlin Server. The "hook" provides a way to tie script code into the Gremlin Server startup and -shutdown sequences. The `LifeCycleHook` has two methods that can be implemented: `onStartUp` and `onShutDown`. -These events are called once at Gremlin Server start and once at Gremlin Server stop. This is an important point -because code outside of the "hook" is executed for each `ScriptEngine` creation (multiple may be created when -"sessions" are enabled) and therefore the `LifeCycleHook` provides a way to ensure that a script is only executed a -single time. In this case, the startup hook loads the "modern" graph into the empty TinkerGraph instance, preparing -it for use. The second key/value pair assigned to the `Map`, named "g", defines a `TraversalSource` from the `Graph` -bound to the "graph" variable in the YAML configuration file. This variable `g`, as well as any other variable -assigned to the `Map`, will be made available as variables for future remote script executions. In more general -terms, any key/value pairs assigned to a `Map` returned from the initialization script will become variables that -are global to all requests. In addition, any functions that are defined will be cached for future use. +Each entry specifies: + +* `className` (required) — the fully qualified class name of a `LifeCycleHook` implementation +* `config` (optional) — a `Map` of key/value pairs passed to the hook's `init(Map)` method + +The built-in `TinkerFactoryDataLoader` loads TinkerFactory sample datasets into a graph. It accepts two config +parameters: `graph` (the name of the graph as defined in `graphs`) and `dataset` (one of `modern`, `classic`, `crew`, +`grateful`, `sink`, or `airroutes`). + +Custom `LifeCycleHook` implementations must have a no-arg constructor and implement the `onStartUp(Context)` and +`onShutDown(Context)` methods. The `Context` provides access to a `Logger` and the `GraphManager`. + +IMPORTANT: Creating `LifeCycleHook` and `TraversalSource` instances via Groovy init scripts is deprecated. Use the +`lifecycleHooks` and `traversalSources` YAML sections instead. Existing Groovy scripts continue to work but will +produce a deprecation warning at startup. + +Both the deprecated Groovy init scripts and the new YAML configuration can coexist in the same configuration file to +allow for progressive migration. When both are present, the server processes them in the following order: + +. Groovy init scripts run first, creating any `TraversalSource` and `LifeCycleHook` bindings. +. Declarative `traversalSources` from YAML are processed next. If a YAML entry uses the same binding name as one + created by an init script, the YAML-configured `TraversalSource` overwrites the script-created one. +. Implicitly-created `TraversalSource` bindings are added for any graphs not yet covered by either mechanism. +. Declarative `lifecycleHooks` from YAML are instantiated. At startup, YAML-configured hooks execute before any + hooks created by init scripts. WARNING: Transactions on graphs in initialization scripts are not closed automatically after the script finishes executing. It is up to the script to properly commit or rollback transactions in the script itself. @@ -842,8 +904,15 @@ The following table describes the various YAML configuration options that Gremli |scriptEngines |A `Map` of `ScriptEngine` implementations to expose through Gremlin Server, where the key is the name given by the `ScriptEngine` implementation. The key must match the name exactly for the `ScriptEngine` to be constructed. The value paired with this key is itself a `Map` of configuration for that `ScriptEngine`. If this value is not set, it will default to "gremlin-lang". |_gremlin-lang_ |scriptEngines..imports |A comma separated list of classes/packages to make available to the `ScriptEngine`. |_none_ |scriptEngines..staticImports |A comma separated list of "static" imports to make available to the `ScriptEngine`. |_none_ -|scriptEngines..scripts |A comma separated list of script files to execute on `ScriptEngine` initialization. `Graph` and `TraversalSource` instance references produced from scripts will be stored globally in Gremlin Server, therefore it is possible to use initialization scripts to add Traversal Strategies or create entirely new `Graph` instances all together. Instantiating a `LifeCycleHook` in a script provides a way to execute scripts when Gremlin Server starts and stops.|_none_ +|scriptEngines..scripts |A comma separated list of script files to execute on `ScriptEngine` initialization. Deprecated — use `traversalSources` and `lifecycleHooks` instead.|_none_ |scriptEngines..config |A `Map` of configuration settings for the `ScriptEngine`. These settings are dependent on the `ScriptEngine` implementation being used. |_none_ +|traversalSources |A `Map` of `TraversalSource` configurations keyed by binding name. Each entry specifies a `graph` reference and optionally a `query` to configure strategies. See <>. |_none (auto-created from graphs)_ +|traversalSources..graph |The name of the graph (as defined in `graphs`) to create the traversal source from. |_none_ +|traversalSources..query |An optional Gremlin query evaluated with a base traversal source bound as `g`. The result becomes the final `TraversalSource`. |_none_ +|traversalSources..language |The script engine language to use for evaluating `query`. Falls back to the sole configured engine if only one is present, or `gremlin-lang` otherwise. |_auto-detected_ +|lifecycleHooks |A `List` of Java-based `LifeCycleHook` implementations to instantiate and execute during server startup and shutdown. See <>. |_none_ +|lifecycleHooks[X].className |The fully qualified class name of the `LifeCycleHook` implementation. |_none_ +|lifecycleHooks[X].config |A `Map` of configuration passed to the hook's `init(Map)` method. |_none_ |evaluationTimeout |The amount of time in milliseconds before a request evaluation and iteration of result times out. This feature can be turned off by setting the value to `0`. |30000 |serializers |A `List` of `Map` settings, where each `Map` represents a `MessageSerializer` implementation to use along with its configuration. If this value is not set, then Gremlin Server will configure with GraphSON and GraphBinary but will not register any `ioRegistries` for configured graphs. |_empty_ |serializers[X].className |The full class name of the `MessageSerializer` implementation. |_none_ @@ -1278,22 +1347,19 @@ consult the documentation of the graph you are using to determine what authoriza Gremlin Server supports three mechanisms to configure authorization: -. With the `ScriptFileGremlinPlugin` a groovy script is configured that instantiates the `GraphTraversalSources` that -can be accessed by client requests. Using the `withStrategies()` gremlin -link:https://tinkerpop.apache.org/docs/x.y.z/reference/#start-steps[start step], one can apply so-called -link:https://tinkerpop.apache.org/docs/x.y.z/reference/#traversalstrategy[TraversalStrategy instances] to these -`GraphTraversalSource` instances, some of which can serve for authorization purposes (`ReadOnlyStrategy`, +. With the `traversalSources` YAML configuration, one can declare `TraversalSource` instances with +link:https://tinkerpop.apache.org/docs/x.y.z/reference/#traversalstrategy[TraversalStrategy instances] applied via +a Gremlin expression, some of which can serve for authorization purposes (`ReadOnlyStrategy`, `LambdaRestrictionStrategy`, `VertexProgramRestrictionStrategy`, `SubgraphStrategy`, `PartitionStrategy`, `EdgeLabelVerificationStrategy`), provided that users are not allowed to remove or modify these `TraversalStrategy` -instances afterwards. The `ScriptFileGremlinPlugin` is found in the yaml configuration file for Gremlin Server: -+ +instances afterwards. For example: + [source,yaml] ---- -scriptEngines: { - gremlin-groovy: { - plugins: { - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/empty-sample.groovy]}}}} +traversalSources: { + g: {graph: graph, gremlinExpression: "g.withStrategies(ReadOnlyStrategy)"}} ---- + . Administrators can configure an authorizer class, an implementation of the `Authorizer` interface. An authorizer receives a request before it is executed and it can decide to pass or deny the request, based on the information it has available on the requesting user or can seek externally. @@ -1314,12 +1380,13 @@ gives an overview. [width="95%",cols="5,2,2,4",options="header"] |========================================================= |Type (mechanism) |GraphTraversalSources |Groups |Bytecode analysis -|Implicit (init script) | all accessible |one |`withStrategies()` +|Implicit (YAML config or init script) | all accessible |one |`withStrategies()` |Passive (pass/deny) | selected access |few |hybrid |Active (inject) |selected access |many |hybrid |========================================================= -With implicit authorization (only adding restricting `TraversalStrategy` instances in the initialization script of +With implicit authorization (configuring restricting `TraversalStrategy` instances via the `traversalSources` YAML +section or in a Groovy initialization script of Gremlin Server) all authenticated users can access all hosted `GraphTraversalSources` and all face the same restrictions. One would need separate Gremlin Server instances for each authorization policy and apply an authenticator that restricts access to a group of users (that is, supports in authorization). @@ -1330,7 +1397,8 @@ the most flexible and can support an almost unlimited number of authorization po implement. In particular, applying the `SubgraphStrategy` requires knowledge about the schema of the graph. The passive authorization solution perhaps provides a middle ground to start implementing authorization. This -solution assumes that the `SubgraphStrategy` is applied in the Gremlin Server initialization script, because compliance +solution assumes that the `SubgraphStrategy` is applied in the Gremlin Server `traversalSources` configuration (or +initialization script), because compliance with a subgraph restriction can only be determined during the actual execution of the gremlin traversal. Note that the same graph can be reused with different `SubgraphStrategies`. Now, authorization policies can be defined in terms of accessible `GraphTraversalSources` and the authorizer can simply match the requested access to a `GraphTraversalSource` @@ -1587,7 +1655,14 @@ groupsandbox: [usersandbox, marko] [[script-execution]] -==== Protecting Script Execution +==== Protecting Groovy Script Execution + +NOTE: TinkerPop currently provides two `GremlinScriptEngine` implementations, the grammar-based `GremlinLangScriptEngine`, +and the Groovy-based `GremlinGroovyScriptEngine`. `GremlinLangScriptEngine` is the default implementation and +recommended for most usages. This engine evaluates scripts through our gremlin grammar and parser, and thus isn't prone +to the same arbitrary script evaluation risks as the `GremlinGroovyScriptEngine`. The following section details the +process of hardening the `GremlinGroovyScriptEngine` if necessary. The simplest and safest recommendation is to avoid +enabling `GremlinGroovyScriptEngine` in applications where hardening is warranted. It is important to remember that Gremlin Server exposes `GremlinScriptEngine` instances that allows for remote execution of arbitrary code on the server. Obviously, this situation can represent a security risk or, more minimally, provide @@ -1982,11 +2057,12 @@ Another option, `all`, can be used to indicate that all properties should be ret In some cases it can be inconvenient to load Elements with properties due to large data size or for compatibility reasons. That can be solved by utilizing `ReferenceElementStrategy` when creating the out-of-the-box `GraphTraversalSource`. As the name suggests, this means that elements will be detached by reference and will therefore not have properties -included. The relevant configuration from the Gremlin Server initialization script looks like this: +included. The relevant configuration from the Gremlin Server YAML looks like this: -[source,groovy] +[source,yaml] ---- -globals << [g : traversal().with(graph).withStrategies(ReferenceElementStrategy)] +traversalSources: { + g: {graph: graph, gremlinExpression: "g.withStrategies(ReferenceElementStrategy)"}} ---- This configuration is global to Gremlin Server and therefore all methods of connection will always return elements @@ -2233,7 +2309,7 @@ NOTE: This plugin is typically only useful to the Gremlin Console and is enabled The Server Plugin for remoting with the Gremlin Console should not be confused with a plugin of similar name that is used by the server. `GremlinServerGremlinPlugin` is typically only configured in Gremlin Server and provides a number -of imports that are required for writing <>. +of imports that are required for writing Groovy initialization scripts (if Groovy is enabled). [[spark-plugin]] === Spark Plugin diff --git a/docs/src/upgrade/release-4.x.x.asciidoc b/docs/src/upgrade/release-4.x.x.asciidoc index 08e019de330..b189eb0f7cf 100644 --- a/docs/src/upgrade/release-4.x.x.asciidoc +++ b/docs/src/upgrade/release-4.x.x.asciidoc @@ -51,6 +51,130 @@ deserialization in GraphBinary is unchanged. Applications that depend on the `uu `gremlin-javascript` brought it in as a transitive dependency should add it directly to their own `package.json` if they still need it. +==== Gremlin Server Initialization Without Groovy + +Previous versions of Gremlin Server relied on the Groovy script engine for basic server initialization; binding +traversal sources, loading data, and running lifecycle hooks all required Groovy init scripts. This meant the Groovy +script engine was active by default, even for users who only needed to send standard Gremlin queries. An active +scripting engine capable of executing arbitrary code introduces a security surface that is difficult to harden and +easy to misconfigure. + +As of this release, Groovy is completely disabled by default throughout the entire server lifecycle. Server +initialization is handled through new declarative YAML configuration, and standard Gremlin queries are processed by the +`gremlin-lang` engine. Users who explicitly need Groovy script execution can still opt in, but it is no longer +required for any standard usage. + +===== Simplified Server Configuration + +The most basic server setup, initializing a basic graph with a traversal source, previously required a Groovy init +script just to create the `g` binding. Now, any graph defined in the `graphs` section automatically gets a +`TraversalSource` according to these rules: + +* A graph named `graph` is implicitly bound to `g` +* All others are bound to `g_` (e.g. `modern` gets `g_modern`) + +A fully functional minimal configuration is now simply: + +[source,yaml] +---- +graphs: { + graph: conf/tinkergraph-empty.properties} +---- + +===== Strategy Configuration via `traversalSources` + +For cases that require custom traversalSource naming or strategies on a `TraversalSource` (e.g. `ReadOnlyStrategy`), the +`traversalSources` YAML section provides explicit control without scripting: + +[source,yaml] +---- +traversalSources: { + g: {graph: graph}, + gReadOnly: {graph: graph, gremlinExpression: "g.withStrategies(ReadOnlyStrategy)", language: "gremlin-lang"}} +---- + +Each entry specifies: + +- `graph` (required): references a key in the `graphs` section +- `gremlinExpression` (optional): a Gremlin expression evaluated with a base traversal source bound as `g` +- `language` (optional): which script engine to use for the expression (defaults to `gremlin-lang`, or the sole + configured engine if only one is present) + +Graphs with explicit `traversalSources` entries are excluded from the implicitly defined traversal sources described in +the previous section. + +===== Java-Based Lifecycle Hooks + +For startup and shutdown logic that goes beyond declarative configuration such as loading sample data, initializing +caches, or custom setup, Java-based `LifeCycleHook` implementations can now replace Groovy init scripts: + +[source,yaml] +---- +lifecycleHooks: + - className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader + config: {graph: graph, dataset: modern} +---- + +Each entry specifies a `className` implementing `LifeCycleHook` and an optional `config` map passed to the hook's +`init()` method. The built-in `TinkerFactoryDataLoader` supports datasets: `airroutes`, `modern`, `classic`, `crew`, +`grateful`, and `sink`. + +===== Migrating from Groovy Init Scripts + +Creating `TraversalSource` and `LifeCycleHook` instances via Groovy init scripts is now deprecated. Existing scripts +continue to work when `GremlinGroovyScriptEngine` is explicitly configured, but a deprecation warning is logged at +startup. Support for Groovy script initialization and customization may be dropped in a future release. + +*Before (Groovy init script):* + +[source,groovy] +---- +def globals = [:] +globals << [hook : [ + onStartUp: { ctx -> + org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerFactory.generateModern(graph) + } +] as LifeCycleHook] +globals << [g : traversal().withEmbedded(graph)] +---- + +*After (YAML only):* + +[source,yaml] +---- +graphs: { + graph: conf/tinkergraph-empty.properties} +lifecycleHooks: + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: graph, dataset: modern}} +---- + +The `g` binding is implicitly created from the `graph` entry. No `scriptEngines` section is needed. + +===== Script Engine Allowlist + +The `scriptEngines` configuration in the Gremlin Server YAML now acts as an allowlist. Only engines explicitly listed in +the configuration will accept requests. The `gremlin-lang` engine is always available regardless of configuration. + +Previously, any `GremlinScriptEngineFactory` discovered via Java's `ServiceLoader` mechanism on the classpath could be +used by clients, even if it was not listed in the server's `scriptEngines` configuration. This meant that +`gremlin-groovy` was always available as long as its JAR was on the classpath, regardless of the server configuration. + +With this change, a request specifying a `language` that is not in the `scriptEngines` configuration will receive a +`400 Bad Request` response. Providers who rely on `gremlin-groovy` or any other script engine must explicitly include it +in their `scriptEngines` configuration: + +[source,yaml] +---- +scriptEngines: { + gremlin-lang: {}, + gremlin-groovy: { + plugins: { + org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}}}} +---- + +See: link:https://issues.apache.org/jira/browse/TINKERPOP-2720[TINKERPOP-2720], +link:https://issues.apache.org/jira/browse/TINKERPOP-3107[TINKERPOP-3107] + == TinkerPop 4.0.0-beta.2 *Release Date: April 1, 2026* diff --git a/gremlin-console/src/test/java/org/apache/tinkerpop/gremlin/console/jsr223/AbstractGremlinServerIntegrationTest.java b/gremlin-console/src/test/java/org/apache/tinkerpop/gremlin/console/jsr223/AbstractGremlinServerIntegrationTest.java index f9fc1465516..393373b89ab 100644 --- a/gremlin-console/src/test/java/org/apache/tinkerpop/gremlin/console/jsr223/AbstractGremlinServerIntegrationTest.java +++ b/gremlin-console/src/test/java/org/apache/tinkerpop/gremlin/console/jsr223/AbstractGremlinServerIntegrationTest.java @@ -25,7 +25,6 @@ import java.io.InputStream; import java.nio.file.Paths; -import java.util.Collections; /** * Starts and stops an instance for each executed test. @@ -51,10 +50,6 @@ public void setUp() throws Exception { final Settings overridenSettings = overrideSettings(settings); final String prop = Paths.get(AbstractGremlinServerIntegrationTest.class.getResource("tinkergraph-empty.properties").toURI()).toString(); overridenSettings.graphs.put("graph", prop); - final String script = Paths.get(AbstractGremlinServerIntegrationTest.class.getResource("generate.groovy").toURI()).toString(); - overridenSettings.scriptEngines.get("gremlin-groovy").plugins - .get("org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin") - .put("files", Collections.singletonList(script)); this.server = new GremlinServer(overridenSettings); diff --git a/gremlin-console/src/test/resources/org/apache/tinkerpop/gremlin/console/jsr223/gremlin-server-integration.yaml b/gremlin-console/src/test/resources/org/apache/tinkerpop/gremlin/console/jsr223/gremlin-server-integration.yaml index 4bb6dcf7549..cb16c1fa8fa 100644 --- a/gremlin-console/src/test/resources/org/apache/tinkerpop/gremlin/console/jsr223/gremlin-server-integration.yaml +++ b/gremlin-console/src/test/resources/org/apache/tinkerpop/gremlin/console/jsr223/gremlin-server-integration.yaml @@ -19,12 +19,10 @@ host: localhost port: 45940 evaluationTimeout: 30000 channelizer: org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer -scriptEngines: { - gremlin-groovy: { - plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {}}}} +graphs: { + graph: conf/tinkergraph-empty.properties} +lifecycleHooks: + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: graph, dataset: modern}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV4} # application/vnd.graphbinary-v4.0 metrics: { diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/CachedGremlinScriptEngineManager.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/CachedGremlinScriptEngineManager.java index 8b7c1c344a2..f34f6518ec1 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/CachedGremlinScriptEngineManager.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/CachedGremlinScriptEngineManager.java @@ -18,6 +18,7 @@ */ package org.apache.tinkerpop.gremlin.jsr223; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** @@ -47,6 +48,15 @@ public CachedGremlinScriptEngineManager(final ClassLoader loader) { super(loader); } + /** + * Creates a cached manager with an allowlist of engine names. + * + * @see DefaultGremlinScriptEngineManager#DefaultGremlinScriptEngineManager(Set) + */ + public CachedGremlinScriptEngineManager(final Set allowedEngines) { + super(allowedEngines); + } + /** * Gets a {@link GremlinScriptEngine} from cache or creates a new one from the {@link GremlinScriptEngineFactory}. *

@@ -55,6 +65,7 @@ public CachedGremlinScriptEngineManager(final ClassLoader loader) { @Override public GremlinScriptEngine getEngineByName(final String shortName) { final GremlinScriptEngine engine = cache.computeIfAbsent(shortName, super::getEngineByName); + if (engine == null) return null; registerLookUpInfo(engine, shortName); return engine; } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/DefaultGremlinScriptEngineManager.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/DefaultGremlinScriptEngineManager.java index 01a305fb50c..048e39cd9d5 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/DefaultGremlinScriptEngineManager.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/DefaultGremlinScriptEngineManager.java @@ -35,6 +35,7 @@ import java.util.Optional; import java.util.ServiceConfigurationError; import java.util.ServiceLoader; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -99,11 +100,19 @@ public class DefaultGremlinScriptEngineManager implements GremlinScriptEngineMan */ private List plugins = new ArrayList<>(); + /** + * Optional set of engine names that are allowed to be resolved by this manager. When {@code null}, all + * SPI-discovered engines are available (the default). When set, {@link #getEngineByName(String)} will + * return {@code null} for any engine name not in this set. + */ + private final Set allowedEngines; + /** * The effect of calling this constructor is the same as calling * {@code DefaultGremlinScriptEngineManager(Thread.currentThread().getContextClassLoader())}. */ public DefaultGremlinScriptEngineManager() { + this.allowedEngines = null; final ClassLoader ctxtLoader = Thread.currentThread().getContextClassLoader(); initEngines(ctxtLoader); } @@ -115,9 +124,23 @@ public DefaultGremlinScriptEngineManager() { * (installed extensions) are loaded. */ public DefaultGremlinScriptEngineManager(final ClassLoader loader) { + this.allowedEngines = null; initEngines(loader); } + /** + * Creates a manager with an allowlist of engine names. Only engines whose names appear in + * {@code allowedEngines} will be returned by {@link #getEngineByName(String)}. Engines discovered + * via SPI but not in the allowlist will be rejected. Pass {@code null} to allow all engines. + * + * @param allowedEngines the set of permitted engine names, or {@code null} for no restriction + */ + public DefaultGremlinScriptEngineManager(final Set allowedEngines) { + this.allowedEngines = allowedEngines; + final ClassLoader ctxtLoader = Thread.currentThread().getContextClassLoader(); + initEngines(ctxtLoader); + } + @Override public List getCustomizers(final String scriptEngineName) { final List pluginCustomizers = plugins.stream().flatMap(plugin -> { @@ -193,6 +216,11 @@ public Object get(final String key) { @Override public GremlinScriptEngine getEngineByName(final String shortName) { if (null == shortName) throw new NullPointerException(); + + if (allowedEngines != null && !allowedEngines.contains(shortName)) { + return null; + } + //look for registered name first Object obj; if (null != (obj = nameAssociations.get(shortName))) { diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/jsr223/SingleScriptEngineManagerTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/jsr223/SingleScriptEngineManagerTest.java index 6f0e179926e..5d521405d41 100644 --- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/jsr223/SingleScriptEngineManagerTest.java +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/jsr223/SingleScriptEngineManagerTest.java @@ -20,6 +20,7 @@ import org.junit.Test; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; /** @@ -37,8 +38,8 @@ public void shouldGetSameInstance() { assertSame(mgr, SingleGremlinScriptEngineManager.instance()); } - @Test(expected = IllegalArgumentException.class) + @Test public void shouldNotGetGremlinScriptEngineAsItIsNotRegistered() { - mgr.getEngineByName("gremlin-groovy"); + assertNull(mgr.getEngineByName("gremlin-groovy")); } } diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/MessagesTests.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/MessagesTests.cs index 082610cd43b..7880607db24 100644 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/MessagesTests.cs +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/MessagesTests.cs @@ -69,8 +69,7 @@ public async Task ShouldThrowForUnsupportedLanguage() var thrownException = await Assert.ThrowsAsync(() => gremlinClient.SubmitAsync(requestMsg)); - Assert.Contains("not an available GremlinScript", thrownException.Message); - Assert.Contains(unknownLanguage, thrownException.Message); + Assert.Contains("Script engine [unknown] is not available.", thrownException.Message); } } } diff --git a/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/engine/GremlinExecutor.java b/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/engine/GremlinExecutor.java index 57387cd9424..5562e8cc269 100644 --- a/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/engine/GremlinExecutor.java +++ b/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/engine/GremlinExecutor.java @@ -44,6 +44,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -103,7 +104,7 @@ private GremlinExecutor(final Builder builder, final boolean suppliedExecutor, this.evaluationTimeout = builder.evaluationTimeout; this.globalBindings = builder.globalBindings; - this.gremlinScriptEngineManager = new CachedGremlinScriptEngineManager(); + this.gremlinScriptEngineManager = new CachedGremlinScriptEngineManager(builder.allowedEngineNames); initializeGremlinScriptEngineManager(); this.suppliedExecutor = suppliedExecutor; @@ -502,6 +503,7 @@ public final static class Builder { private BiConsumer afterFailure = (b, e) -> { }; private Bindings globalBindings = new ConcurrentBindings(); + private Set allowedEngineNames = null; private Builder() { } @@ -517,6 +519,16 @@ public Builder addPlugins(final String engineName, final Map allowedEngineNames) { + this.allowedEngineNames = allowedEngineNames; + return this; + } + /** * Bindings to apply to every script evaluated. Note that the entries of the supplied {@code Bindings} object * will be copied into a newly created {@link ConcurrentBindings} object diff --git a/gremlin-server/conf/gremlin-server-airroutes.yaml b/gremlin-server/conf/gremlin-server-airroutes.yaml new file mode 100644 index 00000000000..c4c4529f8fd --- /dev/null +++ b/gremlin-server/conf/gremlin-server-airroutes.yaml @@ -0,0 +1,43 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +host: localhost +port: 8182 +evaluationTimeout: 30000 +channelizer: org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer +graphs: { + graph: conf/tinkergraph-empty.properties} +lifecycleHooks: + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: graph, dataset: airroutes}} +serializers: + - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV4, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/json + - { className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV4} # application/vnd.graphbinary-v4.0 +metrics: { + slf4jReporter: {enabled: true, interval: 180000}} +strictTransactionManagement: false +idleConnectionTimeout: 0 +keepAliveInterval: 0 +maxInitialLineLength: 4096 +maxHeaderSize: 8192 +maxChunkSize: 8192 +maxRequestContentLength: 10485760 +maxAccumulationBufferComponents: 1024 +resultIterationBatchSize: 64 +writeBufferLowWaterMark: 32768 +writeBufferHighWaterMark: 65536 +ssl: { + enabled: false} diff --git a/gremlin-server/conf/gremlin-server-classic.yaml b/gremlin-server/conf/gremlin-server-classic.yaml index 37b51f53808..b421c3f2763 100644 --- a/gremlin-server/conf/gremlin-server-classic.yaml +++ b/gremlin-server/conf/gremlin-server-classic.yaml @@ -20,13 +20,8 @@ port: 8182 evaluationTimeout: 30000 graphs: { graph: conf/tinkergraph-empty.properties} -scriptEngines: { - gremlin-lang: {}, - gremlin-groovy: { - plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/generate-classic.groovy]}}}} +lifecycleHooks: + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: graph, dataset: classic}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/json - { className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1 } # application/vnd.graphbinary-v1.0 diff --git a/gremlin-server/conf/gremlin-server-modern-readonly.yaml b/gremlin-server/conf/gremlin-server-modern-readonly.yaml index 3c6ad637b74..9dc7f3a53fe 100644 --- a/gremlin-server/conf/gremlin-server-modern-readonly.yaml +++ b/gremlin-server/conf/gremlin-server-modern-readonly.yaml @@ -20,13 +20,10 @@ port: 8182 evaluationTimeout: 30000 graphs: { graph: conf/tinkergraph-empty.properties} -scriptEngines: { - gremlin-lang: {}, - gremlin-groovy: { - plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/generate-modern-readonly.groovy]}}}} +traversalSources: { + g: {graph: graph, gremlinExpression: "g.withStrategies(ReadOnlyStrategy)"}} +lifecycleHooks: + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: graph, dataset: modern}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/json - { className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1 } # application/vnd.graphbinary-v1.0 diff --git a/gremlin-server/conf/gremlin-server-modern.yaml b/gremlin-server/conf/gremlin-server-modern.yaml index 4551023d804..4a1fe9803ec 100644 --- a/gremlin-server/conf/gremlin-server-modern.yaml +++ b/gremlin-server/conf/gremlin-server-modern.yaml @@ -21,14 +21,8 @@ evaluationTimeout: 30000 channelizer: org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer graphs: { graph: conf/tinkergraph-empty.properties} -scriptEngines: { - gremlin-lang: {}, - gremlin-groovy: { - plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.groovy.jsr223.GroovyCompilerGremlinPlugin: {enableThreadInterrupt: true}, - org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/generate-modern.groovy]}}}} +lifecycleHooks: + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: graph, dataset: modern}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV4, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/json - { className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV4} # application/vnd.graphbinary-v4.0 diff --git a/gremlin-server/conf/gremlin-server-rest-modern.yaml b/gremlin-server/conf/gremlin-server-rest-modern.yaml index 905a9ca002b..3b66b58177d 100644 --- a/gremlin-server/conf/gremlin-server-rest-modern.yaml +++ b/gremlin-server/conf/gremlin-server-rest-modern.yaml @@ -21,13 +21,8 @@ evaluationTimeout: 30000 channelizer: org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer graphs: { graph: conf/tinkergraph-empty.properties} -scriptEngines: { - gremlin-lang: {}, - gremlin-groovy: { - plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/generate-modern.groovy]}}}} +lifecycleHooks: + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: graph, dataset: modern}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV3, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/vnd.gremlin-v3.0+json;types=false - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/vnd.gremlin-v3.0+json diff --git a/gremlin-server/conf/gremlin-server-rest-secure.yaml b/gremlin-server/conf/gremlin-server-rest-secure.yaml index 8a62795351f..642033b31d1 100644 --- a/gremlin-server/conf/gremlin-server-rest-secure.yaml +++ b/gremlin-server/conf/gremlin-server-rest-secure.yaml @@ -29,14 +29,6 @@ evaluationTimeout: 30000 channelizer: org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer graphs: { graph: conf/tinkergraph-empty.properties} -scriptEngines: { - gremlin-lang: {}, - gremlin-groovy: { - plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.groovy.jsr223.GroovyCompilerGremlinPlugin: {enableThreadInterrupt: true, timedInterrupt: 10000, compilation: COMPILE_STATIC, extensions: org.apache.tinkerpop.gremlin.groovy.jsr223.customizer.SimpleSandboxExtension}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/empty-sample-secure.groovy]}}}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/json metrics: { diff --git a/gremlin-server/conf/gremlin-server-secure.yaml b/gremlin-server/conf/gremlin-server-secure.yaml index ddc57c0d85a..644b275f702 100644 --- a/gremlin-server/conf/gremlin-server-secure.yaml +++ b/gremlin-server/conf/gremlin-server-secure.yaml @@ -29,14 +29,6 @@ evaluationTimeout: 30000 channelizer: org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer graphs: { graph: conf/tinkergraph-empty.properties} -scriptEngines: { - gremlin-lang: {}, - gremlin-groovy: { - plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.groovy.jsr223.GroovyCompilerGremlinPlugin: {enableThreadInterrupt: true, timedInterrupt: 10000, compilation: COMPILE_STATIC, extensions: org.apache.tinkerpop.gremlin.groovy.jsr223.customizer.SimpleSandboxExtension}, - org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/empty-sample-secure.groovy]}}}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/json - { className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1 } # application/vnd.graphbinary-v1.0 diff --git a/gremlin-server/conf/gremlin-server-transaction.yaml b/gremlin-server/conf/gremlin-server-transaction.yaml index e4cc804ac23..6b61f40a255 100644 --- a/gremlin-server/conf/gremlin-server-transaction.yaml +++ b/gremlin-server/conf/gremlin-server-transaction.yaml @@ -21,13 +21,6 @@ evaluationTimeout: 30000 channelizer: org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer graphs: { graph: conf/tinkertransactiongraph-empty.properties} -scriptEngines: { - gremlin-lang: {}, - gremlin-groovy: { - plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/empty-sample.groovy]}}}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/json - { className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1 } # application/vnd.graphbinary-v1.0 diff --git a/gremlin-server/conf/gremlin-server.yaml b/gremlin-server/conf/gremlin-server.yaml index 286f7f18ea8..163d807849b 100644 --- a/gremlin-server/conf/gremlin-server.yaml +++ b/gremlin-server/conf/gremlin-server.yaml @@ -21,13 +21,6 @@ evaluationTimeout: 30000 channelizer: org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer graphs: { graph: conf/tinkergraph-empty.properties} -scriptEngines: { - gremlin-lang: {}, - gremlin-groovy: { - plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/empty-sample.groovy]}}}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV4, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/json - { className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV4 } # application/vnd.graphbinary-v1.0 diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/GremlinServer.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/GremlinServer.java index 16d8a2d045d..a0b68e5e3ab 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/GremlinServer.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/GremlinServer.java @@ -159,7 +159,7 @@ public synchronized CompletableFuture start() throws Exce serverGremlinExecutor.getHooks().forEach(hook -> { logger.info("Executing start up {}", LifeCycleHook.class.getSimpleName()); try { - hook.onStartUp(new LifeCycleHook.Context(logger)); + hook.onStartUp(new LifeCycleHook.Context(logger, serverGremlinExecutor.getGraphManager())); } catch (UnsupportedOperationException uoe) { // if the user doesn't implement onStartUp the scriptengine will throw // this exception. it can safely be ignored. @@ -269,7 +269,7 @@ public synchronized CompletableFuture stop() { serverGremlinExecutor.getHooks().forEach(hook -> { logger.info("Executing shutdown {}", LifeCycleHook.class.getSimpleName()); try { - hook.onShutDown(new LifeCycleHook.Context(logger)); + hook.onShutDown(new LifeCycleHook.Context(logger, serverGremlinExecutor.getGraphManager())); } catch (UnsupportedOperationException | UndeclaredThrowableException uoe) { // if the user doesn't implement onShutDown the scriptengine will throw // this exception. it can safely be ignored. diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Settings.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Settings.java index 3489bd511e2..0586aee3e8a 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Settings.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Settings.java @@ -244,6 +244,18 @@ public Settings() { */ public Map scriptEngines; + /** + * {@link Map} of {@link TraversalSource} configurations keyed by the binding name. Each entry specifies a + * graph reference and optionally a Gremlin query to configure strategies. + */ + public Map traversalSources = new LinkedHashMap<>(); + + /** + * List of {@link LifeCycleHook} implementations to instantiate via reflection and execute during server + * initialization and shutdown. + */ + public List lifecycleHooks = new ArrayList<>(); + /** * List of {@link MessageSerializer} to configure. If no serializers are specified then default serializers for * the most current versions of "application/json" and "application/vnd.gremlin-v1.0+gryo" are applied. @@ -300,6 +312,8 @@ protected static Constructor createDefaultYamlConstructor() { settingsDescription.addPropertyParameters("graphs", String.class, String.class); settingsDescription.addPropertyParameters("scriptEngines", String.class, ScriptEngineSettings.class); settingsDescription.addPropertyParameters("serializers", SerializerSettings.class); + settingsDescription.addPropertyParameters("traversalSources", String.class, TraversalSourceSettings.class); + settingsDescription.addPropertyParameters("lifecycleHooks", LifeCycleHookSettings.class); constructor.addTypeDescription(settingsDescription); final TypeDescription serializerSettingsDescription = new TypeDescription(SerializerSettings.class); @@ -314,6 +328,13 @@ protected static Constructor createDefaultYamlConstructor() { scriptEngineSettingsDescription.addPropertyParameters("plugins", String.class, Object.class); constructor.addTypeDescription(scriptEngineSettingsDescription); + final TypeDescription traversalSourceSettingsDescription = new TypeDescription(TraversalSourceSettings.class); + constructor.addTypeDescription(traversalSourceSettingsDescription); + + final TypeDescription lifeCycleHookSettingsDescription = new TypeDescription(LifeCycleHookSettings.class); + lifeCycleHookSettingsDescription.addPropertyParameters("config", String.class, Object.class); + constructor.addTypeDescription(lifeCycleHookSettingsDescription); + final TypeDescription sslSettings = new TypeDescription(SslSettings.class); constructor.addTypeDescription(sslSettings); @@ -393,6 +414,43 @@ public static class ScriptEngineSettings { public Map> plugins = new LinkedHashMap<>(); } + /** + * Settings for a declarative {@link TraversalSource} entry in the server YAML. + */ + public static class TraversalSourceSettings { + /** + * The name of the graph (as defined in {@code graphs}) to create the traversal source from. + */ + public String graph; + + /** + * An optional Gremlin expression evaluated with a base traversal source bound as {@code g}. + * The result of the expression becomes the final {@link TraversalSource}. + */ + public String gremlinExpression = null; + + /** + * The script engine language to use for evaluating {@link #gremlinExpression}. If not specified, + * resolution falls back to the single configured engine or {@code gremlin-lang}. + */ + public String language = null; + } + + /** + * Settings for a Java-based {@link LifeCycleHook} configured in the server YAML. + */ + public static class LifeCycleHookSettings { + /** + * The fully qualified class name of the {@link LifeCycleHook} implementation. + */ + public String className; + + /** + * Optional configuration passed to {@link LifeCycleHook#init(java.util.Map)}. + */ + public Map config = null; + } + /** * Settings for the {@link MessageSerializer} implementations. */ diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java index fa34d4d762c..1ed0e6fb317 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java @@ -392,6 +392,15 @@ private void iterateScriptEvalResult(final Context context, MessageSerializer final String language = args.containsKey(Tokens.ARGS_LANGUAGE) ? (String) args.get(Tokens.ARGS_LANGUAGE) : "gremlin-lang"; final GremlinScriptEngine scriptEngine = gremlinExecutor.getScriptEngineManager().getEngineByName(language); + if (scriptEngine == null) { + if (!settings.scriptEngines.containsKey(language) && !language.equals("gremlin-lang")) { + logger.warn("Request for script engine [{}] could not be fulfilled - not configured in the server's scriptEngines setting", language); + } else { + logger.warn("Request for script engine [{}] could not be fulfilled - configured but failed to load (check classpath for the engine's implementation)", language); + } + throw new ProcessingException(GremlinError.scriptEngineNotAvailable(language)); + } + final Bindings mergedBindings = mergeBindingsFromRequest(context, new SimpleBindings(graphManager.getAsBindings())); final Object result = scriptEngine.eval(message.getGremlin(), mergedBindings); diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/GremlinError.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/GremlinError.java index 3014438abdd..42cc67bffa8 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/GremlinError.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/GremlinError.java @@ -232,4 +232,15 @@ public static GremlinError transactionNotSupported(final UnsupportedOperationExc public static GremlinError transactionUnableToStart(final String message) { return new GremlinError(HttpResponseStatus.INTERNAL_SERVER_ERROR, message, "TransactionException"); } + + /** + * Creates an error for when a requested script engine is not available on the server. + * + * @param language the requested script engine name + * @return A GremlinError with appropriate message and status code + */ + public static GremlinError scriptEngineNotAvailable(final String language) { + final String message = String.format("Script engine [%s] is not available.", language); + return new GremlinError(HttpResponseStatus.BAD_REQUEST, message, "InvalidRequestException"); + } } diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/LifeCycleHook.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/LifeCycleHook.java index 95884d8d7f3..80696686b84 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/LifeCycleHook.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/LifeCycleHook.java @@ -18,8 +18,11 @@ */ package org.apache.tinkerpop.gremlin.server.util; +import org.apache.tinkerpop.gremlin.server.GraphManager; import org.slf4j.Logger; +import java.util.Map; + /** * Provides a method in which users can hook into the startup and shutdown lifecycle of Gremlin Server. Creating * an instance of this interface in a Gremlin Server initialization script enables the ability to get a callback @@ -29,6 +32,15 @@ */ public interface LifeCycleHook { + /** + * Called once after instantiation to pass configuration from the {@code lifecycleHooks} YAML section. + * Implementations that require configuration should override this method. The default implementation + * is a no-op so that existing hooks are not forced to implement it. + * + * @param config the key/value pairs from the {@code config} block in the YAML entry + */ + public default void init(final Map config) {} + /** * Called when the server starts up. The graph collection will have been initialized at this point * and all initialization scripts will have been executed when this callback is called. @@ -45,13 +57,30 @@ public interface LifeCycleHook { */ public static class Context { private final Logger logger; + private final GraphManager graphManager; + /** + * @deprecated As of release 4.0.0, replaced by {@link #Context(Logger, GraphManager)}. + */ + @Deprecated public Context(final Logger logger) { + this(logger, null); + } + + public Context(final Logger logger, final GraphManager graphManager) { this.logger = logger; + this.graphManager = graphManager; } public Logger getLogger() { return logger; } + + /** + * Gets the {@link GraphManager} which provides access to all configured graphs and traversal sources. + */ + public GraphManager getGraphManager() { + return graphManager; + } } } diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/ServerGremlinExecutor.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/ServerGremlinExecutor.java index dce5fb931a5..dd4b89bbd60 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/ServerGremlinExecutor.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/ServerGremlinExecutor.java @@ -36,9 +36,12 @@ import javax.script.SimpleBindings; import java.lang.reflect.Constructor; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; @@ -135,6 +138,10 @@ public ServerGremlinExecutor(final Settings settings, final ExecutorService grem logger.info("Initialized Gremlin thread pool. Threads in pool named with pattern gremlin-*"); + // build the allowlist of script engines from the server config - gremlin-lang is always available + final Set allowedEngines = new HashSet<>(settings.scriptEngines.keySet()); + allowedEngines.add("gremlin-lang"); + final GremlinExecutor.Builder gremlinExecutorBuilder = GremlinExecutor.build() .evaluationTimeout(settings.getEvaluationTimeout()) .afterFailure((b, e) -> this.graphManager.rollbackAll()) @@ -142,7 +149,8 @@ public ServerGremlinExecutor(final Settings settings, final ExecutorService grem .afterTimeout((b, e) -> this.graphManager.rollbackAll()) .globalBindings(this.graphManager.getAsBindings()) .executorService(this.gremlinExecutorService) - .scheduledExecutorService(this.scheduledExecutorService); + .scheduledExecutorService(this.scheduledExecutorService) + .allowedEngineNames(allowedEngines); settings.scriptEngines.forEach((k, v) -> { // use plugins if they are present @@ -186,20 +194,108 @@ public ServerGremlinExecutor(final Settings settings, final ExecutorService grem .forEach(kv -> this.graphManager.putGraph(kv.getKey(), (Graph) kv.getValue())); // script engine init may have constructed the TraversalSource bindings - store them in Graphs object - gremlinExecutor.getScriptEngineManager().getBindings().entrySet().stream() - .filter(kv -> kv.getValue() instanceof TraversalSource) - .forEach(kv -> { - logger.info("A {} is now bound to [{}] with {}", kv.getValue().getClass().getSimpleName(), kv.getKey(), kv.getValue()); - this.graphManager.putTraversalSource(kv.getKey(), (TraversalSource) kv.getValue()); - }); + final boolean hasScriptTraversalSources = gremlinExecutor.getScriptEngineManager().getBindings().entrySet().stream() + .anyMatch(kv -> kv.getValue() instanceof TraversalSource); + if (hasScriptTraversalSources) { + logger.warn("TraversalSource instances were created via script engine initialization scripts. " + + "This approach is deprecated - use the 'traversalSources' YAML configuration instead."); + gremlinExecutor.getScriptEngineManager().getBindings().entrySet().stream() + .filter(kv -> kv.getValue() instanceof TraversalSource) + .forEach(kv -> { + logger.info("A {} is now bound to [{}] with {}", kv.getValue().getClass().getSimpleName(), kv.getKey(), kv.getValue()); + this.graphManager.putTraversalSource(kv.getKey(), (TraversalSource) kv.getValue()); + }); + } // determine if the initialization scripts introduced LifeCycleHook objects - if so we need to gather them // up for execution - hooks = gremlinExecutor.getScriptEngineManager().getBindings().entrySet().stream() + final boolean hasScriptHooks = gremlinExecutor.getScriptEngineManager().getBindings().entrySet().stream() + .anyMatch(kv -> kv.getValue() instanceof LifeCycleHook); + if (hasScriptHooks) { + logger.warn("LifeCycleHook instances were created via script engine initialization scripts. " + + "This approach is deprecated - use the 'lifecycleHooks' YAML configuration instead."); + } + final List scriptHooks = gremlinExecutor.getScriptEngineManager().getBindings().entrySet().stream() .filter(kv -> kv.getValue() instanceof LifeCycleHook) .map(kv -> (LifeCycleHook) kv.getValue()) .collect(Collectors.toList()); + // process declarative traversalSources from YAML config - track which graphs have explicit entries + final Set graphsWithExplicitTraversalSources = new HashSet<>(); + if (settings.traversalSources != null && !settings.traversalSources.isEmpty()) { + settings.traversalSources.forEach((tsName, tsSettings) -> { + final Graph graph = this.graphManager.getGraph(tsSettings.graph); + if (null == graph) { + logger.warn("Could not create TraversalSource [{}] - graph [{}] not found in graphs configuration", + tsName, tsSettings.graph); + return; + } + + graphsWithExplicitTraversalSources.add(tsSettings.graph); + + if (tsSettings.gremlinExpression != null && !tsSettings.gremlinExpression.isEmpty()) { + // resolve which script engine to use for the expression + final String language = resolveLanguage(tsSettings.language); + try { + // bind a base traversal source as 'g' for the expression to operate on + final SimpleBindings bindings = new SimpleBindings(); + bindings.put("g", graph.traversal()); + final TraversalSource ts = (TraversalSource) gremlinExecutor.eval( + tsSettings.gremlinExpression, language, bindings).join(); + this.graphManager.putTraversalSource(tsName, ts); + logger.info("A {} is now bound to [{}] via gremlinExpression", ts.getClass().getSimpleName(), tsName); + } catch (Exception ex) { + logger.warn(String.format("Could not create TraversalSource [%s] from gremlinExpression - %s", + tsName, ex.getMessage()), ex); + } + } else { + final TraversalSource ts = graph.traversal(); + this.graphManager.putTraversalSource(tsName, ts); + logger.info("A {} is now bound to [{}]", ts.getClass().getSimpleName(), tsName); + } + }); + } + + // auto-create TraversalSources for graphs that don't have explicit traversalSources entries and + // were not already bound by script engine initialization + for (final String graphName : this.graphManager.getGraphNames()) { + if (graphsWithExplicitTraversalSources.contains(graphName)) + continue; + + final String tsName = graphName.equals("graph") ? "g" : "g_" + graphName; + if (this.graphManager.getTraversalSource(tsName) != null) + continue; + + final Graph graph = this.graphManager.getGraph(graphName); + final TraversalSource ts = graph.traversal(); + this.graphManager.putTraversalSource(tsName, ts); + logger.info("A {} is now auto-bound to [{}] for graph [{}]", + ts.getClass().getSimpleName(), tsName, graphName); + } + + // instantiate Java-based lifecycle hooks from YAML config + final List yamlHooks = new ArrayList<>(); + if (settings.lifecycleHooks != null) { + for (final Settings.LifeCycleHookSettings hookSettings : settings.lifecycleHooks) { + try { + final Class clazz = Class.forName(hookSettings.className); + final LifeCycleHook hook = (LifeCycleHook) clazz.getDeclaredConstructor().newInstance(); + if (hookSettings.config != null) { + hook.init(hookSettings.config); + } + yamlHooks.add(hook); + logger.info("Instantiated LifeCycleHook: {}", hookSettings.className); + } catch (Exception ex) { + logger.error(String.format("Could not instantiate LifeCycleHook %s", hookSettings.className), ex); + } + } + } + + // combine YAML-configured hooks with any script-produced hooks (YAML hooks execute first) + final List allHooks = new ArrayList<>(yamlHooks); + allHooks.addAll(scriptHooks); + hooks = allHooks; + transactionManager = new TransactionManager( scheduledExecutorService, graphManager, @@ -214,6 +310,21 @@ private void registerMetrics(final String engineName) { MetricManager.INSTANCE.registerGremlinScriptEngineMetrics(engine, engineName, "sessionless", "class-cache"); } + /** + * Resolves the script engine language to use for evaluating a traversal source gremlinExpression. If an explicit language + * is provided, it is used directly. Otherwise, if exactly one script engine is configured, that engine is used. + * Falls back to {@code gremlin-lang}. + */ + private String resolveLanguage(final String explicitLanguage) { + if (explicitLanguage != null && !explicitLanguage.isEmpty()) + return explicitLanguage; + + if (settings.scriptEngines.size() == 1) + return settings.scriptEngines.keySet().iterator().next(); + + return "gremlin-lang"; + } + public void addHostOption(final String key, final Object value) { hostOptions.put(key, value); } diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/TinkerFactoryDataLoader.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/TinkerFactoryDataLoader.java new file mode 100644 index 00000000000..a211d814263 --- /dev/null +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/TinkerFactoryDataLoader.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.tinkerpop.gremlin.server.util; + +import org.apache.tinkerpop.gremlin.structure.Graph; +import org.apache.tinkerpop.gremlin.tinkergraph.structure.AbstractTinkerGraph; +import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerFactory; + +import java.util.Map; + +/** + * A {@link LifeCycleHook} that loads TinkerFactory sample datasets into a graph during server startup. + * Replaces the Groovy init scripts (e.g. generate-modern.groovy) for default server configurations. + * + *

Configuration parameters (via {@code config} in YAML): + *

    + *
  • {@code graph} — name of the graph (as defined in {@code graphs:} config) to load data into
  • + *
  • {@code dataset} — which dataset to load: modern, classic, crew, grateful, sink, airroutes
  • + *
+ * + *

Example YAML usage: + *

+ * lifecycleHooks:
+ *   - className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader
+ *     config: {graph: graph, dataset: modern}
+ * 
+ */ +public class TinkerFactoryDataLoader implements LifeCycleHook { + + private String graphName; + private String dataset; + + @Override + public void init(final Map config) { + graphName = (String) config.get("graph"); + dataset = (String) config.get("dataset"); + if (graphName == null || dataset == null) { + throw new IllegalArgumentException("TinkerFactoryDataLoader requires 'graph' and 'dataset' config parameters"); + } + } + + @Override + public void onStartUp(final Context c) { + final Graph graph = c.getGraphManager().getGraph(graphName); + if (null == graph) { + c.getLogger().warn("TinkerFactoryDataLoader could not find graph [{}]", graphName); + return; + } + if (!(graph instanceof AbstractTinkerGraph)) { + c.getLogger().warn("TinkerFactoryDataLoader requires an AbstractTinkerGraph but [{}] is {}", + graphName, graph.getClass().getName()); + return; + } + + final AbstractTinkerGraph tinkerGraph = (AbstractTinkerGraph) graph; + switch (dataset) { + case "modern": + TinkerFactory.generateModern(tinkerGraph); + break; + case "classic": + TinkerFactory.generateClassic(tinkerGraph); + break; + case "crew": + TinkerFactory.generateTheCrew(tinkerGraph); + break; + case "grateful": + TinkerFactory.generateGratefulDead(tinkerGraph); + break; + case "sink": + TinkerFactory.generateKitchenSink(tinkerGraph); + break; + case "airroutes": + TinkerFactory.generateAirRoutes(tinkerGraph); + break; + default: + c.getLogger().warn("TinkerFactoryDataLoader unknown dataset [{}]", dataset); + return; + } + c.getLogger().info("TinkerFactoryDataLoader loaded [{}] dataset into graph [{}]", dataset, graphName); + } + + @Override + public void onShutDown(final Context c) { + // nothing to do on shutdown + } +} diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverIntegrateTest.java index c4cfa266199..33babe76864 100644 --- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverIntegrateTest.java +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverIntegrateTest.java @@ -119,16 +119,13 @@ public Settings overrideSettings(final Settings settings) { settings.channelizer = HttpChannelizer.class.getName(); break; case "shouldAliasTraversalSourceVariables": - try { - final String p = Storage.toPath(TestHelper.generateTempFileFromResource( - GremlinDriverIntegrateTest.class, - "generate-shouldRebindTraversalSourceVariables.groovy", "")); - final Map m = new HashMap<>(); - m.put("files", Collections.singletonList(p)); - settings.scriptEngines.get("gremlin-groovy").plugins.put(ScriptFileGremlinPlugin.class.getName(), m); - } catch (Exception ex) { - throw new RuntimeException(ex); - } + final Settings.TraversalSourceSettings readOnlyG = new Settings.TraversalSourceSettings(); + readOnlyG.graph = "graph"; + readOnlyG.gremlinExpression = "g.withStrategies(ReadOnlyStrategy)"; + settings.traversalSources.put("g", readOnlyG); + final Settings.TraversalSourceSettings writableG1 = new Settings.TraversalSourceSettings(); + writableG1.graph = "graph"; + settings.traversalSources.put("g1", writableG1); break; case "shouldFailWithBadClientSideSerialization": // add custom gryo config for Color diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerConfigIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerConfigIntegrateTest.java new file mode 100644 index 00000000000..835aad5732d --- /dev/null +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerConfigIntegrateTest.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.tinkerpop.gremlin.server; + +import org.apache.tinkerpop.gremlin.driver.Client; +import org.apache.tinkerpop.gremlin.driver.Cluster; +import org.apache.tinkerpop.gremlin.driver.Result; +import org.apache.tinkerpop.gremlin.tinkergraph.structure.AbstractTinkerGraph; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +/** + * Validates that each shipped server config can start successfully and serve a basic query. + * Configs requiring SSL or authentication infrastructure are excluded. + */ +@RunWith(Parameterized.class) +public class GremlinServerConfigIntegrateTest { + + private static final Logger logger = LoggerFactory.getLogger(GremlinServerConfigIntegrateTest.class); + + @Parameterized.Parameter + public String configName; + + @Parameterized.Parameters(name = "{0}") + public static Collection configs() { + return Arrays.asList( + "gremlin-server.yaml", + "gremlin-server-min.yaml", + "gremlin-server-modern.yaml", + "gremlin-server-classic.yaml", + "gremlin-server-airroutes.yaml", + "gremlin-server-modern-readonly.yaml", + "gremlin-server-rest-modern.yaml", + "gremlin-server-transaction.yaml" + ); + } + + @Test + public void shouldStartAndServeQuery() throws Exception { + final File confDir = new File(System.getProperty("build.dir"), "../conf"); + final Settings settings = Settings.read(new FileInputStream(new File(confDir, configName))); + + settings.serializers = Collections.emptyList(); + ServerTestHelper.rewritePathsInGremlinServerSettings(settings); + + final GremlinServer server = new GremlinServer(settings); + try { + server.start().join(); + logger.info("Started server with config: {}", configName); + + final Cluster cluster = Cluster.build("localhost").port(settings.port).create(); + final Client client = cluster.connect(); + try { + final List results = client.submit("g.inject(1)").all().get(); + assertThat(results.size(), is(1)); + assertThat(results.get(0).getInt(), is(1)); + } finally { + client.close(); + cluster.close(); + } + } finally { + server.getServerGremlinExecutor().getGraphManager().getAsBindings().values().stream() + .filter(g -> g instanceof AbstractTinkerGraph) + .forEach(g -> ((AbstractTinkerGraph) g).clear()); + server.stop().join(); + } + } +} diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java index 9f56cb2f1c2..7ff9cbc7caf 100644 --- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java @@ -177,6 +177,9 @@ public Settings overrideSettings(final Settings settings) { case "shouldTimeOutRemoteTraversal": settings.evaluationTimeout = 500; break; + case "ensureScriptEngineDefaultsToGremlinLang": + settings.scriptEngines = new HashMap<>(); + break; case "shouldBlowTheWorkQueueSize": settings.gremlinPool = 1; settings.maxWorkQueueSize = 1; @@ -1088,4 +1091,68 @@ public void shouldParseFloatLiteralWithoutSuffixDependingOnLanguage() { assertEquals(new BigDecimal("1.0"), client.submit("g.inject(1.0)", gremlinGroovy).one().getObject()); assertEquals(new BigDecimal("-123.456"), client.submit("g.inject(-123.456)", gremlinGroovy).one().getObject()); } + + @Test + public void ensureScriptEngineDefaultsToGremlinLang() { + final Cluster cluster = TestClientFactory.open(); + final Client client = cluster.connect(); + + try { + // should handle traversal with no explicit language + final RequestOptions withAlias = RequestOptions.build().addG("gmodern").create(); + assertEquals(6L, client.submit("g.V().count()", withAlias).one().getLong()); + + // should handle script with explicit gremlin-lang + final RequestOptions langOptions = RequestOptions.build().language("gremlin-lang").addG("gmodern").create(); + assertEquals(6L, client.submit("g.V().count()", langOptions).one().getLong()); + + // should reject non-Gremlin expressions + try { + client.submit("2+2", langOptions).all().get(); + fail("Should have failed for non-Gremlin expression"); + } catch (Exception ex) { + final ResponseException re = (ResponseException) ex.getCause(); + assertEquals(HttpResponseStatus.BAD_REQUEST, re.getResponseStatusCode()); + assertEquals("MalformedQueryException", re.getRemoteException()); + } + + // in gremlin-groovy '1g' is a valid BigDecimal but in gremlin-lang it should be '1m' + try { + client.submit("g.inject(1g)", langOptions).all().get(); + fail("Should have failed for Groovy-specific syntax"); + } catch (Exception ex) { + final ResponseException re = (ResponseException) ex.getCause(); + assertEquals(HttpResponseStatus.BAD_REQUEST, re.getResponseStatusCode()); + assertEquals("MalformedQueryException", re.getRemoteException()); + } + + // gremlin-lang BigDecimal syntax should work + assertEquals(BigDecimal.ONE, client.submit("g.inject(1m)", langOptions).one().getObject()); + + // explicit gremlin-groovy should fail when engine is not configured + try { + client.submit("2+2", groovyRequestOptions).all().get(); + fail("Should have failed for unavailable gremlin-groovy engine"); + } catch (Exception ex) { + final ResponseException re = (ResponseException) ex.getCause(); + assertEquals(HttpResponseStatus.BAD_REQUEST, re.getResponseStatusCode()); + assertEquals("InvalidRequestException", re.getRemoteException()); + assertEquals("Script engine [gremlin-groovy] is not available.", re.getMessage()); + } + + // completely unknown engine should also fail + try { + final RequestOptions unknownEngine = RequestOptions.build().language("gremlin-foo").create(); + client.submit("g.V()", unknownEngine).all().get(); + fail("Should have failed for unknown script engine"); + } catch (Exception ex) { + final ResponseException re = (ResponseException) ex.getCause(); + assertEquals(HttpResponseStatus.BAD_REQUEST, re.getResponseStatusCode()); + assertEquals("InvalidRequestException", re.getRemoteException()); + assertEquals("Script engine [gremlin-foo] is not available.", re.getMessage()); + } + } finally { + cluster.close(); + } + } } diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/ServerTestHelper.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/ServerTestHelper.java index 0d642ffd7fd..1a3dc91f920 100644 --- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/ServerTestHelper.java +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/ServerTestHelper.java @@ -38,24 +38,25 @@ public class ServerTestHelper { * If an overriden path is determined to be absolute then the path is not re-written. */ public static void rewritePathsInGremlinServerSettings(final Settings overridenSettings) { - final Map> plugins; - final Map scriptFileGremlinPlugin; final File homeDir; homeDir = new File( getBuildDir(), "../src/test/scripts" ); - plugins = overridenSettings.scriptEngines.get("gremlin-groovy").plugins; - scriptFileGremlinPlugin = plugins.get(ScriptFileGremlinPlugin.class.getName()); + final Settings.ScriptEngineSettings groovyEngine = overridenSettings.scriptEngines.get("gremlin-groovy"); + if (groovyEngine != null) { + final Map> plugins = groovyEngine.plugins; + final Map scriptFileGremlinPlugin = plugins.get(ScriptFileGremlinPlugin.class.getName()); - if (scriptFileGremlinPlugin != null) { - scriptFileGremlinPlugin - .put("files", - ((List) scriptFileGremlinPlugin.get("files")).stream() - .map(s -> new File(s)) - .map(f -> f.isAbsolute() ? f - : new File(relocateFile( homeDir, f))) - .map(f -> Storage.toPath(f)) - .collect(Collectors.toList())); + if (scriptFileGremlinPlugin != null) { + scriptFileGremlinPlugin + .put("files", + ((List) scriptFileGremlinPlugin.get("files")).stream() + .map(s -> new File(s)) + .map(f -> f.isAbsolute() ? f + : new File(relocateFile( homeDir, f))) + .map(f -> Storage.toPath(f)) + .collect(Collectors.toList())); + } } overridenSettings.graphs = overridenSettings.graphs.entrySet().stream() diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/SettingsTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/SettingsTest.java index d399a5ad657..50ff8d9c5e9 100644 --- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/SettingsTest.java +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/SettingsTest.java @@ -22,12 +22,24 @@ import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.Constructor; +import java.io.File; +import java.io.FileInputStream; import java.io.InputStream; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertEquals; public class SettingsTest { + private static InputStream getMinimalConfigStream() throws Exception { + final File confDir = new File(System.getProperty("build.dir"), "../conf"); + return new FileInputStream(new File(confDir, "gremlin-server-min.yaml")); + } + private static class CustomSettings extends Settings { public String customValue = "localhost"; @@ -56,4 +68,77 @@ public void defaultCustomValuesAreHandledCorrectly() throws Exception { assertEquals("localhost", settings.customValue); } + + @Test + public void scriptEnginesDefaultsToGremlinLangWhenAbsentFromYaml() throws Exception { + final InputStream stream = getMinimalConfigStream(); + final Settings settings = Settings.read(stream); + + assertThat(settings.scriptEngines, is(notNullValue())); + assertThat(settings.scriptEngines.size(), is(1)); + assertThat(settings.scriptEngines, hasKey("gremlin-lang")); + } + + @Test + public void scriptEnginesPopulatedWhenPresentInYaml() throws Exception { + final InputStream stream = SettingsTest.class.getResourceAsStream("gremlin-server-integration.yaml"); + final Settings settings = Settings.read(stream); + + assertThat(settings.scriptEngines, hasKey("gremlin-groovy")); + assertThat(settings.scriptEngines, hasKey("gremlin-lang")); + } + + @Test + public void traversalSourcesParsedFromYaml() throws Exception { + final InputStream stream = SettingsTest.class.getResourceAsStream("gremlin-server-with-traversal-sources.yaml"); + final Settings settings = Settings.read(stream); + + assertThat(settings.traversalSources.size(), is(2)); + assertThat(settings.traversalSources, hasKey("g")); + assertThat(settings.traversalSources, hasKey("gReadOnly")); + + final Settings.TraversalSourceSettings gSettings = settings.traversalSources.get("g"); + assertThat(gSettings.graph, is("graph")); + assertThat(gSettings.gremlinExpression, is(nullValue())); + assertThat(gSettings.language, is(nullValue())); + + final Settings.TraversalSourceSettings roSettings = settings.traversalSources.get("gReadOnly"); + assertThat(roSettings.graph, is("graph")); + assertThat(roSettings.gremlinExpression, is("g.withStrategies(ReadOnlyStrategy)")); + assertThat(roSettings.language, is("gremlin-groovy")); + } + + @Test + public void traversalSourcesDefaultsToEmptyMapWhenAbsentFromYaml() throws Exception { + final InputStream stream = getMinimalConfigStream(); + final Settings settings = Settings.read(stream); + + assertThat(settings.traversalSources, is(notNullValue())); + assertThat(settings.traversalSources.isEmpty(), is(true)); + } + + @Test + public void lifecycleHooksParsedFromYaml() throws Exception { + final InputStream stream = SettingsTest.class.getResourceAsStream("gremlin-server-with-traversal-sources.yaml"); + final Settings settings = Settings.read(stream); + + assertThat(settings.lifecycleHooks.size(), is(2)); + + final Settings.LifeCycleHookSettings first = settings.lifecycleHooks.get(0); + assertThat(first.className, is("org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader")); + assertThat(first.config, hasKey("graph")); + assertThat(first.config.get("dataset"), is("modern")); + + final Settings.LifeCycleHookSettings second = settings.lifecycleHooks.get(1); + assertThat(second.config.get("dataset"), is("classic")); + } + + @Test + public void lifecycleHooksDefaultsToEmptyListWhenAbsentFromYaml() throws Exception { + final InputStream stream = getMinimalConfigStream(); + final Settings settings = Settings.read(stream); + + assertThat(settings.lifecycleHooks, is(notNullValue())); + assertThat(settings.lifecycleHooks.isEmpty(), is(true)); + } } diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/ServerGremlinExecutorTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/ServerGremlinExecutorTest.java new file mode 100644 index 00000000000..489d4a6a5fd --- /dev/null +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/ServerGremlinExecutorTest.java @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.tinkerpop.gremlin.server.util; + +import org.apache.tinkerpop.gremlin.server.Settings; +import org.junit.After; +import org.junit.Test; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class ServerGremlinExecutorTest { + + private static final String TINKERGRAPH_PROPERTIES = + new java.io.File(System.getProperty("build.dir"), "../src/test/scripts/tinkergraph-empty.properties") + .getAbsolutePath(); + + private ServerGremlinExecutor serverGremlinExecutor; + + @After + public void tearDown() { + if (serverGremlinExecutor != null) { + serverGremlinExecutor.getGremlinExecutorService().shutdownNow(); + serverGremlinExecutor.getGraphManager().getGraphNames().forEach(name -> { + try { + serverGremlinExecutor.getGraphManager().getGraph(name).close(); + } catch (Exception ignored) { + } + }); + } + } + + private Settings baseSettings() { + final Settings settings = new Settings(); + settings.graphs.put("graph", TINKERGRAPH_PROPERTIES); + settings.gremlinPool = 1; + return settings; + } + + @Test + public void shouldAutoCreateTraversalSourceForSingleGraph() { + serverGremlinExecutor = new ServerGremlinExecutor(baseSettings(), null, null); + + assertThat(serverGremlinExecutor.getGraphManager().getTraversalSource("g"), is(notNullValue())); + } + + @Test + public void shouldAutoCreateTraversalSourceWithPrefixForNonDefaultGraph() { + final Settings settings = baseSettings(); + settings.graphs.put("myGraph", TINKERGRAPH_PROPERTIES); + serverGremlinExecutor = new ServerGremlinExecutor(settings, null, null); + + assertThat(serverGremlinExecutor.getGraphManager().getTraversalSource("g"), is(notNullValue())); + assertThat(serverGremlinExecutor.getGraphManager().getTraversalSource("g_myGraph"), is(notNullValue())); + } + + @Test + public void shouldNotAutoCreateTraversalSourceWhenExplicitEntryExists() { + final Settings settings = baseSettings(); + final Settings.TraversalSourceSettings tsSettings = new Settings.TraversalSourceSettings(); + tsSettings.graph = "graph"; + settings.traversalSources.put("g", tsSettings); + serverGremlinExecutor = new ServerGremlinExecutor(settings, null, null); + + assertThat(serverGremlinExecutor.getGraphManager().getTraversalSource("g"), is(notNullValue())); + assertThat(serverGremlinExecutor.getGraphManager().getTraversalSource("g_graph"), is(nullValue())); + } + + @Test + public void shouldInstantiateLifecycleHooksFromYaml() { + final Settings settings = baseSettings(); + final Settings.LifeCycleHookSettings hook1 = new Settings.LifeCycleHookSettings(); + hook1.className = TinkerFactoryDataLoader.class.getName(); + hook1.config = new LinkedHashMap<>(); + hook1.config.put("graph", "graph"); + hook1.config.put("dataset", "modern"); + final Settings.LifeCycleHookSettings hook2 = new Settings.LifeCycleHookSettings(); + hook2.className = TinkerFactoryDataLoader.class.getName(); + hook2.config = new LinkedHashMap<>(); + hook2.config.put("graph", "graph"); + hook2.config.put("dataset", "classic"); + final List hooks = new ArrayList<>(); + hooks.add(hook1); + hooks.add(hook2); + settings.lifecycleHooks = hooks; + serverGremlinExecutor = new ServerGremlinExecutor(settings, null, null); + + assertThat(serverGremlinExecutor.getHooks().size(), is(2)); + assertThat(serverGremlinExecutor.getHooks().get(0) instanceof TinkerFactoryDataLoader, is(true)); + assertThat(serverGremlinExecutor.getHooks().get(1) instanceof TinkerFactoryDataLoader, is(true)); + } + + @Test + public void shouldHaveEmptyHooksWhenNoneConfigured() { + serverGremlinExecutor = new ServerGremlinExecutor(baseSettings(), null, null); + + assertThat(serverGremlinExecutor.getHooks().isEmpty(), is(true)); + } + + @Test + public void resolveLanguageShouldReturnExplicitLanguage() throws Exception { + serverGremlinExecutor = new ServerGremlinExecutor(baseSettings(), null, null); + + final Method resolveLanguage = ServerGremlinExecutor.class.getDeclaredMethod("resolveLanguage", String.class); + resolveLanguage.setAccessible(true); + + assertThat(resolveLanguage.invoke(serverGremlinExecutor, "gremlin-groovy"), is("gremlin-groovy")); + } + + @Test + public void resolveLanguageShouldFallBackToGremlinLangWhenNoExplicitLanguage() throws Exception { + serverGremlinExecutor = new ServerGremlinExecutor(baseSettings(), null, null); + + final Method resolveLanguage = ServerGremlinExecutor.class.getDeclaredMethod("resolveLanguage", String.class); + resolveLanguage.setAccessible(true); + + assertThat(resolveLanguage.invoke(serverGremlinExecutor, (String) null), is("gremlin-lang")); + assertThat(resolveLanguage.invoke(serverGremlinExecutor, ""), is("gremlin-lang")); + } + + @Test + public void resolveLanguageShouldFallBackToGremlinLangWhenMultipleEngines() throws Exception { + final Settings settings = baseSettings(); + settings.scriptEngines.put("gremlin-groovy", new Settings.ScriptEngineSettings()); + serverGremlinExecutor = new ServerGremlinExecutor(settings, null, null); + + final Method resolveLanguage = ServerGremlinExecutor.class.getDeclaredMethod("resolveLanguage", String.class); + resolveLanguage.setAccessible(true); + + assertThat(resolveLanguage.invoke(serverGremlinExecutor, (String) null), is("gremlin-lang")); + } + + @Test + public void resolveLanguageShouldUseSoleConfiguredEngine() throws Exception { + final Settings settings = baseSettings(); + settings.scriptEngines.clear(); + settings.scriptEngines.put("gremlin-groovy", new Settings.ScriptEngineSettings()); + serverGremlinExecutor = new ServerGremlinExecutor(settings, null, null); + + final Method resolveLanguage = ServerGremlinExecutor.class.getDeclaredMethod("resolveLanguage", String.class); + resolveLanguage.setAccessible(true); + + assertThat(resolveLanguage.invoke(serverGremlinExecutor, (String) null), is("gremlin-groovy")); + } +} diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/TinkerFactoryDataLoaderTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/TinkerFactoryDataLoaderTest.java new file mode 100644 index 00000000000..0700c48be3b --- /dev/null +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/TinkerFactoryDataLoaderTest.java @@ -0,0 +1,236 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.tinkerpop.gremlin.server.util; + +import org.apache.tinkerpop.gremlin.server.GraphManager; +import org.apache.tinkerpop.gremlin.structure.Graph; +import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph; +import org.junit.Test; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TinkerFactoryDataLoaderTest { + + private LifeCycleHook.Context createContext(final GraphManager graphManager) { + return new LifeCycleHook.Context(LoggerFactory.getLogger(TinkerFactoryDataLoaderTest.class), graphManager); + } + + @Test(expected = IllegalArgumentException.class) + public void initShouldThrowWhenGraphMissing() { + final TinkerFactoryDataLoader loader = new TinkerFactoryDataLoader(); + final Map config = new HashMap<>(); + config.put("dataset", "modern"); + loader.init(config); + } + + @Test(expected = IllegalArgumentException.class) + public void initShouldThrowWhenDatasetMissing() { + final TinkerFactoryDataLoader loader = new TinkerFactoryDataLoader(); + final Map config = new HashMap<>(); + config.put("graph", "graph"); + loader.init(config); + } + + @Test(expected = IllegalArgumentException.class) + public void initShouldThrowWhenConfigEmpty() { + final TinkerFactoryDataLoader loader = new TinkerFactoryDataLoader(); + loader.init(new HashMap<>()); + } + + @Test + public void onStartUpShouldHandleMissingGraph() { + final TinkerFactoryDataLoader loader = new TinkerFactoryDataLoader(); + final Map config = new HashMap<>(); + config.put("graph", "nonexistent"); + config.put("dataset", "modern"); + loader.init(config); + + final GraphManager gm = mock(GraphManager.class); + when(gm.getGraph("nonexistent")).thenReturn(null); + + // should not throw — just logs a warning + loader.onStartUp(createContext(gm)); + } + + @Test + public void onStartUpShouldHandleNonTinkerGraph() { + final TinkerFactoryDataLoader loader = new TinkerFactoryDataLoader(); + final Map config = new HashMap<>(); + config.put("graph", "graph"); + config.put("dataset", "modern"); + loader.init(config); + + final Graph mockGraph = mock(Graph.class); + final GraphManager gm = mock(GraphManager.class); + when(gm.getGraph("graph")).thenReturn(mockGraph); + + // should not throw — just logs a warning + loader.onStartUp(createContext(gm)); + } + + @Test + public void onStartUpShouldHandleUnknownDataset() { + final TinkerFactoryDataLoader loader = new TinkerFactoryDataLoader(); + final Map config = new HashMap<>(); + config.put("graph", "graph"); + config.put("dataset", "bogus"); + loader.init(config); + + final TinkerGraph graph = TinkerGraph.open(); + try { + final GraphManager gm = mock(GraphManager.class); + when(gm.getGraph("graph")).thenReturn(graph); + + // should not throw — just logs a warning + loader.onStartUp(createContext(gm)); + assertThat((int) graph.traversal().V().count().next().longValue(), is(0)); + } finally { + graph.close(); + } + } + + @Test + public void onStartUpShouldLoadModern() { + final TinkerFactoryDataLoader loader = new TinkerFactoryDataLoader(); + final Map config = new HashMap<>(); + config.put("graph", "graph"); + config.put("dataset", "modern"); + loader.init(config); + + final TinkerGraph graph = TinkerGraph.open(); + try { + final GraphManager gm = mock(GraphManager.class); + when(gm.getGraph("graph")).thenReturn(graph); + + loader.onStartUp(createContext(gm)); + assertThat((int) graph.traversal().V().count().next().longValue(), is(6)); + assertThat((int) graph.traversal().E().count().next().longValue(), is(6)); + } finally { + graph.close(); + } + } + + @Test + public void onStartUpShouldLoadClassic() { + final TinkerFactoryDataLoader loader = new TinkerFactoryDataLoader(); + final Map config = new HashMap<>(); + config.put("graph", "graph"); + config.put("dataset", "classic"); + loader.init(config); + + final TinkerGraph graph = TinkerGraph.open(); + try { + final GraphManager gm = mock(GraphManager.class); + when(gm.getGraph("graph")).thenReturn(graph); + + loader.onStartUp(createContext(gm)); + assertThat((int) graph.traversal().V().count().next().longValue(), is(6)); + } finally { + graph.close(); + } + } + + @Test + public void onStartUpShouldLoadCrew() { + final TinkerFactoryDataLoader loader = new TinkerFactoryDataLoader(); + final Map config = new HashMap<>(); + config.put("graph", "graph"); + config.put("dataset", "crew"); + loader.init(config); + + final TinkerGraph graph = TinkerGraph.open(); + try { + final GraphManager gm = mock(GraphManager.class); + when(gm.getGraph("graph")).thenReturn(graph); + + loader.onStartUp(createContext(gm)); + assertThat((int) graph.traversal().V().count().next().longValue(), greaterThan(0)); + } finally { + graph.close(); + } + } + + @Test + public void onStartUpShouldLoadGrateful() { + final TinkerFactoryDataLoader loader = new TinkerFactoryDataLoader(); + final Map config = new HashMap<>(); + config.put("graph", "graph"); + config.put("dataset", "grateful"); + loader.init(config); + + final TinkerGraph graph = TinkerGraph.open(); + try { + final GraphManager gm = mock(GraphManager.class); + when(gm.getGraph("graph")).thenReturn(graph); + + loader.onStartUp(createContext(gm)); + assertThat((int) graph.traversal().V().count().next().longValue(), greaterThan(0)); + } finally { + graph.close(); + } + } + + @Test + public void onStartUpShouldLoadSink() { + final TinkerFactoryDataLoader loader = new TinkerFactoryDataLoader(); + final Map config = new HashMap<>(); + config.put("graph", "graph"); + config.put("dataset", "sink"); + loader.init(config); + + final TinkerGraph graph = TinkerGraph.open(); + try { + final GraphManager gm = mock(GraphManager.class); + when(gm.getGraph("graph")).thenReturn(graph); + + loader.onStartUp(createContext(gm)); + assertThat((int) graph.traversal().V().count().next().longValue(), greaterThan(0)); + } finally { + graph.close(); + } + } + + @Test + public void onStartUpShouldLoadAirRoutes() { + final TinkerFactoryDataLoader loader = new TinkerFactoryDataLoader(); + final Map config = new HashMap<>(); + config.put("graph", "graph"); + config.put("dataset", "airroutes"); + loader.init(config); + + final TinkerGraph graph = TinkerGraph.open(); + try { + final GraphManager gm = mock(GraphManager.class); + when(gm.getGraph("graph")).thenReturn(graph); + + loader.onStartUp(createContext(gm)); + assertThat((int) graph.traversal().V().count().next().longValue(), greaterThan(0)); + } finally { + graph.close(); + } + } +} diff --git a/gremlin-server/src/test/resources/conf/remote-objects.yaml b/gremlin-server/src/test/resources/conf/remote-objects.yaml index a0f33187685..37e1caaa8da 100644 --- a/gremlin-server/src/test/resources/conf/remote-objects.yaml +++ b/gremlin-server/src/test/resources/conf/remote-objects.yaml @@ -23,8 +23,7 @@ # - docker/gremlin-server/conf/remote-objects.yaml # # Without such changes, the test docker server can't be started for independent -# testing with Gremlin Language Variants. Note this file's relation to -# gremlin-server/src/test/resources/scripts/test-server-start.groovy +# testing with Gremlin Language Variants. ############################################################################### hosts: [localhost] diff --git a/gremlin-server/src/test/resources/org/apache/tinkerpop/gremlin/server/gremlin-server-integration.yaml b/gremlin-server/src/test/resources/org/apache/tinkerpop/gremlin/server/gremlin-server-integration.yaml index 13a1c6ffdb9..6528be116db 100644 --- a/gremlin-server/src/test/resources/org/apache/tinkerpop/gremlin/server/gremlin-server-integration.yaml +++ b/gremlin-server/src/test/resources/org/apache/tinkerpop/gremlin/server/gremlin-server-integration.yaml @@ -25,8 +25,7 @@ # - docker/gremlin-server/gremlin-server-integration-secure.yaml # # Without such changes, the test docker server can't be started for independent -# testing with Gremlin Language Variants. Note this file's relation to -# gremlin-server/src/test/resources/scripts/test-server-start.groovy +# testing with Gremlin Language Variants. ############################################################################### host: 0.0.0.0 @@ -41,14 +40,30 @@ graphs: { sink: conf/tinkergraph-empty.properties, tx: conf/tinkertransactiongraph-empty.properties } +traversalSources: { + gclassic: {graph: classic}, + gmodern: {graph: modern}, + g: {graph: graph}, + gcrew: {graph: crew}, + ggraph: {graph: graph}, + ggrateful: {graph: grateful}, + gsink: {graph: sink}, + gtx: {graph: tx}} +lifecycleHooks: + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: classic, dataset: classic}} + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: modern, dataset: modern}} + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: crew, dataset: crew}} + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: grateful, dataset: grateful}} + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: sink, dataset: sink}} scriptEngines: { gremlin-lang : {}, + groovy-test: {}, gremlin-groovy: { plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, org.apache.tinkerpop.gremlin.groovy.jsr223.GroovyCompilerGremlinPlugin: {expectedCompilationTime: 30000}, org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/generate-all.groovy]}}}} + org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/init-functions.groovy]}}}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV4, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} diff --git a/gremlin-server/src/test/resources/org/apache/tinkerpop/gremlin/server/gremlin-server-with-traversal-sources.yaml b/gremlin-server/src/test/resources/org/apache/tinkerpop/gremlin/server/gremlin-server-with-traversal-sources.yaml new file mode 100644 index 00000000000..ae1ea0a3c34 --- /dev/null +++ b/gremlin-server/src/test/resources/org/apache/tinkerpop/gremlin/server/gremlin-server-with-traversal-sources.yaml @@ -0,0 +1,27 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +host: localhost +port: 8182 +graphs: { + graph: conf/tinkergraph-empty.properties} +traversalSources: { + g: {graph: graph}, + gReadOnly: {graph: graph, gremlinExpression: "g.withStrategies(ReadOnlyStrategy)", language: gremlin-groovy}} +lifecycleHooks: + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: graph, dataset: modern}} + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: graph, dataset: classic}} diff --git a/gremlin-server/src/test/scripts/generate-all.groovy b/gremlin-server/src/test/scripts/generate-all.groovy deleted file mode 100644 index 7f3706f2d82..00000000000 --- a/gremlin-server/src/test/scripts/generate-all.groovy +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -// An example of an initialization script that can be configured to run in Gremlin Server. -// Functions defined here will go into global cache and will not be removed from there -// unless there is a reset of the ScriptEngine. -def addItUp(x, y) { x + y } - -// an init script that returns a Map allows explicit setting of global bindings. -def globals = [:] - -// Generates the modern graph into an "empty" TinkerGraph via LifeCycleHook. -// Note that the name of the key in the "global" map is unimportant. -globals << [hook : [ - onStartUp: { ctx -> - // a wild bit of trickery here. the process tests use an INTEGER id manager when LoadGraphWith is used. this - // closure provides a way to to manually override the various id managers for TinkerGraph - the graph on which - // all of these remote tests are executed - so that the tests will pass nicely. an alternative might have been - // to have a special test TinkerGraph config for setting up the id manager properly, but based on how we do - // things now, that test config would have been mixed in with release artifacts and there would have been ugly - // exclusions to make packaging work properly. - allowSetOfIdManager = { graph, idManagerFieldName, idManager -> - java.lang.reflect.Field idManagerField = graph.class.getSuperclass().getDeclaredField(idManagerFieldName) - idManagerField.setAccessible(true) - idManagerField.set(graph, idManager) - } - - [classic, modern, crew, sink, grateful].each{ - allowSetOfIdManager(it, "vertexIdManager", TinkerGraph.DefaultIdManager.INTEGER) - allowSetOfIdManager(it, "edgeIdManager", TinkerGraph.DefaultIdManager.INTEGER) - allowSetOfIdManager(it, "vertexPropertyIdManager", TinkerGraph.DefaultIdManager.LONG) - } - TinkerFactory.generateClassic(classic) - TinkerFactory.generateModern(modern) - TinkerFactory.generateTheCrew(crew) - TinkerFactory.generateGratefulDead(grateful) - TinkerFactory.generateKitchenSink(sink) - } -] as LifeCycleHook] - -// add default TraversalSource instances for each graph instance -globals << [gclassic : traversal().withEmbedded(classic)] -globals << [gmodern : traversal().withEmbedded(modern)] -globals << [g : traversal().withEmbedded(graph)] -globals << [gcrew : traversal().withEmbedded(crew)] -globals << [ggraph : traversal().withEmbedded(graph)] -globals << [ggrateful : traversal().withEmbedded(grateful)] -globals << [gsink : traversal().withEmbedded(sink)] -globals << [gtx : traversal().withEmbedded(tx)] - -// dynamically detect existence of gimmutable as it is only used in gremlin-go testing suite -def dynamicGimmutable = context.getBindings(javax.script.ScriptContext.GLOBAL_SCOPE)["immutable"] -if (dynamicGimmutable != null) - globals << [gimmutable : traversal().withEmbedded(dynamicGimmutable)] - -globals diff --git a/gremlin-console/src/test/resources/org/apache/tinkerpop/gremlin/console/jsr223/generate.groovy b/gremlin-server/src/test/scripts/init-functions.groovy similarity index 57% rename from gremlin-console/src/test/resources/org/apache/tinkerpop/gremlin/console/jsr223/generate.groovy rename to gremlin-server/src/test/scripts/init-functions.groovy index 5c2b791dbc3..5c105446dc4 100644 --- a/gremlin-console/src/test/resources/org/apache/tinkerpop/gremlin/console/jsr223/generate.groovy +++ b/gremlin-server/src/test/scripts/init-functions.groovy @@ -17,17 +17,4 @@ * under the License. */ -// an init script that returns a Map allows explicit setting of global bindings. -def globals = [:] - -// Generates the modern graph into an "empty" TinkerGraph via LifeCycleHook. -// Note that the name of the key in the "global" map is unimportant. -globals << [hook : [ - onStartUp: { ctx -> - ctx.logger.info("Loading 'modern' graph data.") - org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerFactory.generateModern(graph) - } -] as LifeCycleHook] - -// define the default TraversalSource to bind queries to - this one will be named "g". -globals << [g : traversal().withEmbedded(graph)] \ No newline at end of file +def addItUp(x, y) { x + y } diff --git a/gremlin-server/src/test/scripts/test-server-start.groovy b/gremlin-server/src/test/scripts/test-server-start.groovy deleted file mode 100644 index c7e7f5e954f..00000000000 --- a/gremlin-server/src/test/scripts/test-server-start.groovy +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -import org.apache.tinkerpop.gremlin.server.GremlinServer -import org.apache.tinkerpop.gremlin.server.KdcFixture -import org.apache.tinkerpop.gremlin.server.Settings - -//////////////////////////////////////////////////////////////////////////////// -// IMPORTANT -//////////////////////////////////////////////////////////////////////////////// -// Changes to this file need to be appropriately replicated to -// -// - docker/gremlin-server/* -// - docker/gremlin-server.sh -// -// Without such changes, the test docker server can't be started for independent -// testing with Gremlin Language Variants. -//////////////////////////////////////////////////////////////////////////////// - -if (Boolean.parseBoolean(skipTests)) return - -log.info("Starting Gremlin Server instances for native testing of ${executionName}") - -def platformAgnosticBaseDirPath = "${tinkerpopRootDir}".replace("\\", "/") -def platformAgnosticGremlinServerDir = platformAgnosticBaseDirPath + "/gremlin-server" -def platformAgnosticSettingsFile = platformAgnosticGremlinServerDir + "/src/test/resources/org" + - "/apache/tinkerpop/gremlin/server/gremlin-server-integration.yaml" - -def settings = Settings.read("${platformAgnosticSettingsFile}") -settings.graphs.graph = platformAgnosticGremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties" -settings.graphs.classic = platformAgnosticGremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties" -settings.graphs.modern = platformAgnosticGremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties" -settings.graphs.crew = platformAgnosticGremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties" -settings.graphs.grateful = platformAgnosticGremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties" -settings.graphs.sink = platformAgnosticGremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties" -settings.graphs.tx = platformAgnosticGremlinServerDir + "/src/test/scripts/tinkertransactiongraph-empty.properties" -settings.scriptEngines["gremlin-groovy"].plugins["org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin"].files = [platformAgnosticGremlinServerDir + "/src/test/scripts/generate-all.groovy"] -settings.port = 45940 - -def server = new GremlinServer(settings) -server.start().join() - -project.setContextValue("gremlin.server", server) -log.info("Gremlin Server without authentication started on port 45940") - - -def securePropsFile = new File("${platformAgnosticBaseDirPath}/target/tinkergraph-credentials.properties") -if (!securePropsFile.exists()) { - securePropsFile.createNewFile() - securePropsFile << "gremlin.graph=org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph\n" - securePropsFile << "gremlin.tinkergraph.vertexIdManager=LONG\n" - securePropsFile << "gremlin.tinkergraph.graphLocation=${platformAgnosticGremlinServerDir}/data/credentials.kryo\n" - securePropsFile << "gremlin.tinkergraph.graphFormat=gryo" -} - -def settingsSecure = Settings.read("${platformAgnosticSettingsFile}") -settingsSecure.graphs.graph = platformAgnosticGremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties" -settingsSecure.graphs.classic = platformAgnosticGremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties" -settingsSecure.graphs.modern = platformAgnosticGremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties" -settingsSecure.graphs.crew = platformAgnosticGremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties" -settingsSecure.graphs.grateful = platformAgnosticGremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties" -settingsSecure.graphs.sink = platformAgnosticGremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties" -settingsSecure.scriptEngines["gremlin-groovy"].plugins["org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin"].files = [platformAgnosticGremlinServerDir + "/src/test/scripts/generate-all.groovy"] -settingsSecure.port = 45941 -settingsSecure.authentication.authenticator = "org.apache.tinkerpop.gremlin.server.auth.SimpleAuthenticator" -settingsSecure.authentication.config = [credentialsDb: platformAgnosticBaseDirPath + "/target/tinkergraph-credentials.properties"] -settingsSecure.ssl = new Settings.SslSettings() -settingsSecure.ssl.enabled = true -settingsSecure.ssl.sslEnabledProtocols = ["TLSv1.2"] -settingsSecure.ssl.keyStore = platformAgnosticGremlinServerDir + "/src/test/resources/server-key.jks" -settingsSecure.ssl.keyStorePassword = "changeit" - -def serverSecure = new GremlinServer(settingsSecure) -serverSecure.start().join() - -project.setContextValue("gremlin.server.secure", serverSecure) -log.info("Gremlin Server with password authentication started on port 45941") - - -def kdcServer = new KdcFixture(projectBaseDir) -kdcServer.setUp() - -project.setContextValue("gremlin.server.kdcserver", kdcServer) -log.info("KDC started with configuration ${projectBaseDir}/target/kdc/krb5.conf") - -def settingsKrb5 = Settings.read("${settingsFile}") -settingsKrb5.graphs.graph = gremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties" -settingsKrb5.graphs.classic = gremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties" -settingsKrb5.graphs.modern = gremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties" -settingsKrb5.graphs.crew = gremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties" -settingsKrb5.graphs.grateful = gremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties" -settingsKrb5.graphs.sink = gremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties" -settingsKrb5.scriptEngines["gremlin-groovy"].plugins["org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin"].files = [gremlinServerDir + "/src/test/scripts/generate-all.groovy"] -settingsKrb5.port = 45942 -settingsKrb5.authentication.authenticator = "org.apache.tinkerpop.gremlin.server.auth.Krb5Authenticator" -settingsKrb5.authentication.config = [ - "principal": kdcServer.serverPrincipal, - "keytab": kdcServer.serviceKeytabFile.getAbsolutePath()] - -def serverKrb5 = new GremlinServer(settingsKrb5) -serverKrb5.start().join() - -project.setContextValue("gremlin.server.krb5", serverKrb5) -log.info("Gremlin Server with kerberos authentication started on port 45942")