This is an automated email from the ASF dual-hosted git repository.

Cole-Greer pushed a commit to branch docs-3.7
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit 34af8c8a5b5d7a21676b2a1c206af1efcaa88ebb
Author: Cole Greer <[email protected]>
AuthorDate: Thu Jun 4 09:18:00 2026 -0700

    Isolate conflicting plugins per-book in the docs build (tinkerpop-6jq.7)
    
    Neo4j 3.4 (Scala 2.11) and Spark (Scala 2.12) cannot share the docs
    console's flat classpath. Wire the previously-unwired plugin-exclusion
    scaffolding so the console restarts with conflicting plugins removed:
    
    - Add PluginDirectoryRestartHandler that toggles ext/<plugin> dirs and
      keeps ext/plugins.txt in sync (the console drops unlisted plugins whose
      jars vanish on restart).
    - GremlinTreeprocessor: detect :gremlin-docs-plugins-exclude: at section
      granularity during the AST walk (document baseline + per-section
      override with latching), bouncing the console when the set changes.
      Default to the directory handler in production; tests inject their own.
      Also :set max-iteration 100 on console start to match published output.
    - process-docs.sh: install each plugin's deps into ext/<plugin>/plugin/
      (deduped vs lib/) instead of the shared lib/, so conflicting deps are
      isolatable; write plugins.txt deterministically to avoid stale state.
    - Add :gremlin-docs-plugins-exclude: attributes to the neo4j, hadoop,
      spark and gremlin-variants chapters with explanatory comments; update
      the stale reference index comment and developer docs.
    - Fix an undefined-variable typo (marko -> vMarko) and render the
      olap-spark-yarn recipe (which needs a real YARN/HDFS cluster) as a
      non-executed block with hardcoded output.
    - Set asciidoctor.gemPath under target/ so the JRuby gem extraction no
      longer creates a gems/ directory at the repo root.
    
    Assisted-by: Kiro:claude-opus-4.8 [kiro-cli]
---
 CHANGELOG.asciidoc                                 |   2 +
 bin/process-docs.sh                                |  51 ++++++----
 .../dev/developer/development-environment.asciidoc |  21 +++++
 docs/src/recipes/olap-spark-yarn.asciidoc          |  56 +++++++----
 docs/src/recipes/traversal-induced-values.asciidoc |   2 +-
 docs/src/reference/gremlin-variants.asciidoc       |   4 +
 .../reference/implementations-hadoop-end.asciidoc  |   3 +
 .../implementations-hadoop-start.asciidoc          |   3 +
 docs/src/reference/implementations-neo4j.asciidoc  |   3 +
 docs/src/reference/implementations-spark.asciidoc  |   3 +
 docs/src/reference/index.asciidoc                  |   7 +-
 pom.xml                                            |   4 +
 .../gremlin/docs/GremlinTreeprocessor.java         |  79 ++++++++++++----
 .../docs/PluginDirectoryRestartHandler.java        | 103 +++++++++++++++++++++
 .../gremlin/docs/GremlinTreeprocessorTest.java     |  44 +++++++++
 15 files changed, 326 insertions(+), 59 deletions(-)

diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index f710d91f02..2ca89883f0 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -25,6 +25,8 @@ 
image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
 [[release-3-7-7]]
 === TinkerPop 3.7.7 (Release Date: NOT OFFICIALLY RELEASED YET)
 
+* Restart the documentation build's Gremlin Console with conflicting plugins 
excluded per-book (via the `gremlin-docs-plugins-exclude` attribute), so Neo4j 
(Scala 2.11) and Spark (Scala 2.12) no longer collide on a shared classpath.
+
 * Fixed conjoin has incorrect null handling.
 * Expanded `gremlin-python` CI matrix to test against Python 3.9, 3.10, 3.11, 
3.12, and 3.13.
 * Add Node 26 support for `gremlin-javascript` and `gremlint`.
diff --git a/bin/process-docs.sh b/bin/process-docs.sh
index 81ac12c7db..90dee94934 100755
--- a/bin/process-docs.sh
+++ b/bin/process-docs.sh
@@ -105,6 +105,27 @@ SERVER_HOME="$(cd "${SERVER_DIR}" && pwd)"
 
 # 3. Install plugins into console
 echo "Installing plugins into console..."
+
+# Copy a plugin's dependency jars onto the console classpath via 
ext/<plugin>/plugin/ (which
+# bin/gremlin.sh globs) rather than the shared lib/. This keeps each plugin's 
transitive deps
+# isolatable so the docs extension can exclude conflicting plugins per-book 
(e.g. Neo4j's
+# Scala 2.11 vs Spark's 2.12) by moving the plugin directory off the 
classpath. Jars already
+# present in lib/ (core gremlin deps) and slf4j/logback-classic are skipped to 
avoid duplicate
+# classpath entries and logger bindings -- mirroring the console's own 
:install (DependencyGrabber).
+copy_deps_to_plugin() {
+  local src_dir="$1" plugin="$2"
+  local plugin_dir="${CONSOLE_HOME}/ext/${plugin}/plugin"
+  mkdir -p "${plugin_dir}"
+  local jar base
+  for jar in "${src_dir}"/*.jar; do
+    [ -e "${jar}" ] || continue
+    base=$(basename "${jar}")
+    case "${base}" in slf4j-*|logback-classic-*) continue ;; esac
+    [ -e "${CONSOLE_HOME}/lib/${base}" ] && continue
+    cp "${jar}" "${plugin_dir}/" 2>/dev/null
+  done
+}
+
 PLUGINS="hadoop-gremlin spark-gremlin neo4j-gremlin sparql-gremlin"
 for plugin in ${PLUGINS}; do
   PLUGIN_DIR="${plugin}/target/${plugin}-${TP_VERSION}-standalone"
@@ -113,8 +134,7 @@ for plugin in ${PLUGINS}; do
     cp -r "${PLUGIN_DIR}" "${CONSOLE_HOME}/ext/${plugin}"
     mkdir -p "${CONSOLE_HOME}/ext/${plugin}/plugin"
     cp "${plugin}/target/${plugin}-${TP_VERSION}.jar" 
"${CONSOLE_HOME}/ext/${plugin}/plugin/" 2>/dev/null
-    # Copy deps to main lib for classloading
-    cp "${CONSOLE_HOME}/ext/${plugin}/lib/"*.jar "${CONSOLE_HOME}/lib/" 
2>/dev/null
+    copy_deps_to_plugin "${CONSOLE_HOME}/ext/${plugin}/lib" "${plugin}"
   elif [ -f "${plugin}/target/${plugin}-${TP_VERSION}.jar" ]; then
     echo " * installing ${plugin} (jar + dependencies)"
     mkdir -p "${CONSOLE_HOME}/ext/${plugin}/lib"
@@ -123,8 +143,7 @@ for plugin in ${PLUGINS}; do
     cp "${plugin}/target/${plugin}-${TP_VERSION}.jar" 
"${CONSOLE_HOME}/ext/${plugin}/plugin/"
     cp "${plugin}"/target/dependency/*.jar 
"${CONSOLE_HOME}/ext/${plugin}/lib/" 2>/dev/null || \
       mvn dependency:copy-dependencies -pl "${plugin}" 
-DoutputDirectory="${CONSOLE_HOME}/ext/${plugin}/lib" -q
-    # Copy all deps to main lib for classloading
-    cp "${CONSOLE_HOME}/ext/${plugin}/lib/"*.jar "${CONSOLE_HOME}/lib/" 
2>/dev/null
+    copy_deps_to_plugin "${CONSOLE_HOME}/ext/${plugin}/lib" "${plugin}"
   else
     echo " * WARNING: ${plugin} not found"
   fi
@@ -152,26 +171,26 @@ POM
   # NoSuchMethodError. Keep netty-3.9.x (org.jboss.netty package) -- it does 
NOT conflict and
   # is required by Neo4j 3.4's IO layer.
   rm -f "${NEO4J_PLUGIN_LIB}"/netty-all-4.*.jar
-  cp "${NEO4J_PLUGIN_LIB}/"*.jar "${CONSOLE_HOME}/lib/" 2>/dev/null
+  copy_deps_to_plugin "${NEO4J_PLUGIN_LIB}" "neo4j-gremlin"
 fi
 
 # 4. Register plugins in console
 echo "Registering plugins..."
-# TinkerGraphGremlinPlugin must be (re)registered explicitly: the console 
rewrites plugins.txt
-# to the set of successfully-activated plugins on startup, and a transient 
activation hiccup
-# while bringing up the newly-added plugins can otherwise drop tinkergraph -- 
leaving
-# TinkerFactory/TinkerGraph unavailable and failing the first doc block.
-PLUGIN_CLASSES="org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin
+# Write plugins.txt deterministically rather than appending to whatever state 
a prior run left:
+# the console rewrites this file to the set of successfully-activated plugins 
on shutdown, so a
+# previous (possibly failed) run can leave it missing TinkerGraph/Credentials, 
which would fail
+# the first doc block with "No such property: TinkerFactory". Lightweight 
built-in plugins are
+# listed before the heavy graph plugins so activation order is stable.
+cat > "${CONSOLE_HOME}/ext/plugins.txt" <<'EOF'
+org.apache.tinkerpop.gremlin.console.jsr223.DriverGremlinPlugin
+org.apache.tinkerpop.gremlin.console.jsr223.UtilitiesGremlinPlugin
+org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin
 
org.apache.tinkerpop.gremlin.groovy.jsr223.dsl.credential.CredentialGraphGremlinPlugin
 org.apache.tinkerpop.gremlin.hadoop.jsr223.HadoopGremlinPlugin
 org.apache.tinkerpop.gremlin.spark.jsr223.SparkGremlinPlugin
 org.apache.tinkerpop.gremlin.neo4j.jsr223.Neo4jGremlinPlugin
-org.apache.tinkerpop.gremlin.sparql.jsr223.SparqlGremlinPlugin"
-for cls in ${PLUGIN_CLASSES}; do
-  if ! grep -q "${cls}" "${CONSOLE_HOME}/ext/plugins.txt" 2>/dev/null; then
-    echo "${cls}" >> "${CONSOLE_HOME}/ext/plugins.txt"
-  fi
-done
+org.apache.tinkerpop.gremlin.sparql.jsr223.SparqlGremlinPlugin
+EOF
 
 # 5. Copy hadoop config to console classpath
 HADOOP_CONF_SRC="tools/tinkerpop-docs/src/main/resources/hadoop-conf"
diff --git a/docs/src/dev/developer/development-environment.asciidoc 
b/docs/src/dev/developer/development-environment.asciidoc
index dded1db342..5e667fe434 100644
--- a/docs/src/dev/developer/development-environment.asciidoc
+++ b/docs/src/dev/developer/development-environment.asciidoc
@@ -228,6 +228,27 @@ by the `DependencyGrabber` class which allows you to 
manipulate (typically delet
 Spark or Hadoop. The easiest way to see the error is to simply run the 
examples in the Gremlin Console which more
 plainly displays the error than the failure of the documentation generation 
process.
 
+[[docs-plugin-exclusions]]
+==== Per-book Plugin Exclusions
+
+Some plugins cannot share the documentation console's classpath. Most notably, 
Neo4j 3.4 requires Scala 2.11 while
+Spark requires Scala 2.12, so activating both at once causes runtime failures 
(e.g. `NoSuchMethodError:
+scala.Product.$init$`). To avoid this, a book can restart the console with 
certain plugins removed by declaring the
+`gremlin-docs-plugins-exclude` attribute (a comma-separated list of plugin 
directory names) on a section heading:
+
+[source,asciidoc]
+----
+[gremlin-docs-plugins-exclude="neo4j-gremlin"]
+==== SparkGraphComputer
+----
+
+When the `GremlinTreeprocessor` reaches a section carrying this attribute, it 
closes the current console, moves the
+named `ext/<plugin>` directories aside (and updates `ext/plugins.txt`), then 
starts a fresh console so the excluded
+plugins are off the classpath. The exclusion is latched: it remains in effect 
for subsequent sections until another
+section declares a different set, so each conflicting chapter declares its 
complete exclusion set. This replaces the
+old preprocessor's per-file plugin juggling; `bin/process-docs.sh` installs 
each plugin's dependencies into
+`ext/<plugin>/plugin/` (not the shared `lib/`) precisely so they can be 
toggled this way.
+
 To generate the web site locally, there is no need for any of the above 
infrastructure. Site generation is a simple
 shell script:
 
diff --git a/docs/src/recipes/olap-spark-yarn.asciidoc 
b/docs/src/recipes/olap-spark-yarn.asciidoc
index f8a04c121d..12ea3c77d0 100644
--- a/docs/src/recipes/olap-spark-yarn.asciidoc
+++ b/docs/src/recipes/olap-spark-yarn.asciidoc
@@ -89,26 +89,44 @@ $ hdfs dfs -put data/tinkerpop-modern.kryo .
 $ . bin/spark-yarn.sh
 ----
 
-[gremlin-groovy]
+[source,groovy]
 ----
-hadoop = System.getenv('HADOOP_HOME')
-hadoopConfDir = System.getenv('HADOOP_CONF_DIR')
-archive = 'spark-gremlin.zip'
-archivePath = "/tmp/$archive"
-['bash', '-c', "rm -f $archivePath; cd ext/spark-gremlin/lib && zip 
$archivePath *.jar"].execute().waitFor()
-conf = new Configurations().properties(new 
File('conf/hadoop/hadoop-gryo.properties'))
-conf.setProperty('spark.master', 'yarn')
-conf.setProperty('spark.submit.deployMode', 'client')
-conf.setProperty('spark.yarn.archive', "$archivePath")
-conf.setProperty('spark.yarn.appMasterEnv.CLASSPATH', 
"./__spark_libs__/*:$hadoopConfDir")
-conf.setProperty('spark.executor.extraClassPath', 
"./__spark_libs__/*:$hadoopConfDir")
-conf.setProperty('spark.driver.extraLibraryPath', 
"$hadoop/lib/native:$hadoop/lib/native/Linux-amd64-64")
-conf.setProperty('spark.executor.extraLibraryPath', 
"$hadoop/lib/native:$hadoop/lib/native/Linux-amd64-64")
-conf.setProperty('gremlin.spark.persistContext', 'true')
-hdfs.copyFromLocal('data/tinkerpop-modern.kryo', 'tinkerpop-modern.kryo')
-graph = GraphFactory.open(conf)
-g = traversal().withEmbedded(graph).withComputer(SparkGraphComputer)
-g.V().group().by(values('name')).by(both().count())
+gremlin> hadoop = System.getenv('HADOOP_HOME')
+==>/usr/local/lib/hadoop-3.3.1
+gremlin> hadoopConfDir = System.getenv('HADOOP_CONF_DIR')
+==>/usr/local/lib/hadoop-3.3.1/etc/hadoop
+gremlin> archive = 'spark-gremlin.zip'
+==>spark-gremlin.zip
+gremlin> archivePath = "/tmp/$archive"
+==>/tmp/spark-gremlin.zip
+gremlin> ['bash', '-c', "rm -f $archivePath; cd ext/spark-gremlin/lib && zip 
$archivePath *.jar"].execute().waitFor()
+==>0
+gremlin> conf = new Configurations().properties(new 
File('conf/hadoop/hadoop-gryo.properties'))
+==>org.apache.commons.configuration2.PropertiesConfiguration@5b3bb1f7
+gremlin> conf.setProperty('spark.master', 'yarn')
+==>null
+gremlin> conf.setProperty('spark.submit.deployMode', 'client')
+==>null
+gremlin> conf.setProperty('spark.yarn.archive', "$archivePath")
+==>null
+gremlin> conf.setProperty('spark.yarn.appMasterEnv.CLASSPATH', 
"./__spark_libs__/*:$hadoopConfDir")
+==>null
+gremlin> conf.setProperty('spark.executor.extraClassPath', 
"./__spark_libs__/*:$hadoopConfDir")
+==>null
+gremlin> conf.setProperty('spark.driver.extraLibraryPath', 
"$hadoop/lib/native:$hadoop/lib/native/Linux-amd64-64")
+==>null
+gremlin> conf.setProperty('spark.executor.extraLibraryPath', 
"$hadoop/lib/native:$hadoop/lib/native/Linux-amd64-64")
+==>null
+gremlin> conf.setProperty('gremlin.spark.persistContext', 'true')
+==>null
+gremlin> hdfs.copyFromLocal('data/tinkerpop-modern.kryo', 
'tinkerpop-modern.kryo')
+==>null
+gremlin> graph = GraphFactory.open(conf)
+==>hadoopgraph[gryoinputformat->gryooutputformat]
+gremlin> g = traversal().withEmbedded(graph).withComputer(SparkGraphComputer)
+==>graphtraversalsource[hadoopgraph[gryoinputformat->gryooutputformat], 
sparkgraphcomputer]
+gremlin> g.V().group().by(values('name')).by(both().count())
+==>[ripple:1,peter:1,vadas:1,josh:3,lop:3,marko:3]
 ----
 
 If you run into exceptions, you will have to dig into the logs. You can do 
this from the command line with
diff --git a/docs/src/recipes/traversal-induced-values.asciidoc 
b/docs/src/recipes/traversal-induced-values.asciidoc
index 2d058a5ee2..162651996f 100644
--- a/docs/src/recipes/traversal-induced-values.asciidoc
+++ b/docs/src/recipes/traversal-induced-values.asciidoc
@@ -36,7 +36,7 @@ obvious to any programmer - use a variable:
 [gremlin-groovy,modern]
 ----
 vMarko = g.V().has('name','marko').next()
-g.V(vMarko).out('knows').has('age', gt(marko.value('age'))).values('name')
+g.V(vMarko).out('knows').has('age', gt(vMarko.value('age'))).values('name')
 ----
 
 The downside to this approach is that it takes two separate traversals to 
answer the question. Ideally, there should
diff --git a/docs/src/reference/gremlin-variants.asciidoc 
b/docs/src/reference/gremlin-variants.asciidoc
index 4d9c3f606c..e3336139aa 100644
--- a/docs/src/reference/gremlin-variants.asciidoc
+++ b/docs/src/reference/gremlin-variants.asciidoc
@@ -18,6 +18,10 @@ under the License.
 ////
 
 anchor:gremlin-variants[]
+// This book exercises only remote driver connections, but its predecessors 
leave Neo4j/Spark/Hadoop
+// plugins active. Exclude all three to restart the console with a clean 
classpath and avoid the
+// Scala 2.11/2.12 (and related) conflicts those plugins introduce.
+[gremlin-docs-plugins-exclude="neo4j-gremlin,spark-gremlin,hadoop-gremlin"]
 [[gremlin-drivers-variants]]
 = Gremlin Drivers and Variants
 
diff --git a/docs/src/reference/implementations-hadoop-end.asciidoc 
b/docs/src/reference/implementations-hadoop-end.asciidoc
index 2649c00637..e0711af0d2 100644
--- a/docs/src/reference/implementations-hadoop-end.asciidoc
+++ b/docs/src/reference/implementations-hadoop-end.asciidoc
@@ -16,6 +16,9 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 ////
+// Continues the Spark/Hadoop OLAP examples (Scala 2.12), so exclude 
neo4j-gremlin (Scala 2.11)
+// to keep it off the console's flat classpath.
+[gremlin-docs-plugins-exclude="neo4j-gremlin"]
 === Input/Output Formats
 
 image:adjacency-list.png[width=300,float=right] Hadoop-Gremlin provides 
various I/O formats -- i.e. Hadoop
diff --git a/docs/src/reference/implementations-hadoop-start.asciidoc 
b/docs/src/reference/implementations-hadoop-start.asciidoc
index 31d08daab7..6ca4f65a93 100644
--- a/docs/src/reference/implementations-hadoop-start.asciidoc
+++ b/docs/src/reference/implementations-hadoop-start.asciidoc
@@ -16,6 +16,9 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 ////
+// Spark requires Scala 2.12 while Neo4j 3.4 requires Scala 2.11; they cannot 
share the console's
+// flat classpath, so exclude neo4j-gremlin to restart the console without 
Neo4j's jars.
+[gremlin-docs-plugins-exclude="neo4j-gremlin"]
 [[hadoop-gremlin]]
 == Hadoop-Gremlin
 
diff --git a/docs/src/reference/implementations-neo4j.asciidoc 
b/docs/src/reference/implementations-neo4j.asciidoc
index d784b2076b..e5dfbb0f9b 100644
--- a/docs/src/reference/implementations-neo4j.asciidoc
+++ b/docs/src/reference/implementations-neo4j.asciidoc
@@ -16,6 +16,9 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 ////
+// Neo4j 3.4 requires Scala 2.11; Spark requires Scala 2.12. They cannot 
coexist on the console's
+// flat classpath, so exclude spark-gremlin here to restart the console 
without Spark's jars.
+[gremlin-docs-plugins-exclude="spark-gremlin"]
 [[neo4j-gremlin]]
 == Neo4j-Gremlin (Deprecated)
 
diff --git a/docs/src/reference/implementations-spark.asciidoc 
b/docs/src/reference/implementations-spark.asciidoc
index 77140af2ab..e1e1edc53e 100644
--- a/docs/src/reference/implementations-spark.asciidoc
+++ b/docs/src/reference/implementations-spark.asciidoc
@@ -16,6 +16,9 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 ////
+// SparkGraphComputer requires Scala 2.12 while Neo4j 3.4 requires Scala 2.11; 
they cannot share
+// the console's flat classpath, so exclude neo4j-gremlin to restart the 
console without Neo4j's jars.
+[gremlin-docs-plugins-exclude="neo4j-gremlin"]
 [[sparkgraphcomputer]]
 ==== SparkGraphComputer
 
diff --git a/docs/src/reference/index.asciidoc 
b/docs/src/reference/index.asciidoc
index 98f50906a9..1bd8858574 100644
--- a/docs/src/reference/index.asciidoc
+++ b/docs/src/reference/index.asciidoc
@@ -43,9 +43,10 @@ include::implementations-intro.asciidoc[]
 include::implementations-tinkergraph.asciidoc[]
 include::implementations-neo4j.asciidoc[]
 
-// the hadoop section is split into parts because of serialization issues that 
are encountered when trying
-// to generate graph/spark without restarting the console and currently the 
only way to force a restart of the
-// console is to have a new asciidoc page.
+// The hadoop section is split into parts so the Neo4j/Spark plugins 
(incompatible Scala versions)
+// are not active at the same time. Console restarts are now driven by the
+// :gremlin-docs-plugins-exclude: attribute on each chapter heading (see 
implementations-neo4j,
+// implementations-hadoop-start, implementations-spark) rather than by page 
boundaries.
 include::implementations-hadoop-start.asciidoc[]
 include::implementations-spark.asciidoc[]
 include::implementations-hadoop-end.asciidoc[]
diff --git a/pom.xml b/pom.xml
index 039e7a6a0c..f8319a7cd9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -977,6 +977,10 @@ limitations under the License.
                 <gremlin.docs.console.home/>
                 <gremlin.docs.hadoop.libs/>
                 <gremlin.docs.dryrun>false</gremlin.docs.dryrun>
+
+                <!-- Keep the asciidoctor-maven-plugin's JRuby gem extraction 
under target/ instead
+                     of creating a 'gems/' directory at the repo root. -->
+                
<asciidoctor.gemPath>${project.basedir}/target/asciidoctor-gems</asciidoctor.gemPath>
             </properties>
 
             <build>
diff --git 
a/tools/tinkerpop-docs/src/main/java/org/apache/tinkerpop/gremlin/docs/GremlinTreeprocessor.java
 
b/tools/tinkerpop-docs/src/main/java/org/apache/tinkerpop/gremlin/docs/GremlinTreeprocessor.java
index 5185cdccde..8970cc9a7c 100644
--- 
a/tools/tinkerpop-docs/src/main/java/org/apache/tinkerpop/gremlin/docs/GremlinTreeprocessor.java
+++ 
b/tools/tinkerpop-docs/src/main/java/org/apache/tinkerpop/gremlin/docs/GremlinTreeprocessor.java
@@ -72,6 +72,7 @@ public class GremlinTreeprocessor extends Treeprocessor {
     private final StatementExecutor executor;
     private final TabbedHtmlBuilder tabBuilder;
     private final ConsoleRestartHandler restartHandler;
+    private ConsoleRestartHandler activeRestartHandler;
     private String currentGraph;
     private List<String> currentExcludedPlugins;
 
@@ -135,6 +136,11 @@ public class GremlinTreeprocessor extends Treeprocessor {
             }
         }
 
+        // Use an injected handler (tests) when present, otherwise default to 
physically toggling
+        // plugin directories under the resolved console home (production via 
SPI).
+        activeRestartHandler = restartHandler != null ? restartHandler
+                : (consoleHomePath != null ? new 
PluginDirectoryRestartHandler(consoleHomePath) : null);
+
         try {
             checkPluginExclusions(document);
             processBlock(document, dryRun);
@@ -159,6 +165,10 @@ public class GremlinTreeprocessor extends Treeprocessor {
             LOG.info("Starting GremlinConsole from: " + consoleHomePath);
             lazyConsole = new GremlinConsole(consoleHomePath);
             resolvedExecutor = statement -> lazyConsole.execute(statement);
+            // Match the old preprocessor: raise the console's result display 
limit so traversals
+            // returning many results (e.g. all-pairs shortestPath) render 
fully instead of being
+            // truncated with "..." at the interactive default.
+            lazyConsole.execute(":set max-iteration 100");
             LOG.info("GremlinConsole started successfully");
         } catch (final IOException | GremlinConsole.ConsoleTimeoutException e) 
{
             LOG.warning("Failed to start GremlinConsole: " + e.getMessage());
@@ -174,28 +184,45 @@ public class GremlinTreeprocessor extends Treeprocessor {
     }
 
     /**
-     * Checks the document for the {@code :gremlin-docs-plugins-exclude:} 
attribute and invokes
-     * the restart handler if the exclusion list has changed.
+     * Establishes the document-level baseline exclusion set before the AST 
walk. An absent
+     * attribute means "no exclusions", so each book starts with every plugin 
enabled and any
+     * exclusion latched from a prior document is cleared.
      */
     private void checkPluginExclusions(final Document document) {
-        if (restartHandler == null) return;
-        if (!document.hasAttribute(PLUGINS_EXCLUDE_ATTR)) {
-            if (currentExcludedPlugins != null) {
-                currentExcludedPlugins = null;
-                invokeRestartHandler(Collections.emptyList());
-            }
-            return;
-        }
-
-        final Object attrValue = document.getAttribute(PLUGINS_EXCLUDE_ATTR);
-        final List<String> excludeList = parseExcludeList(attrValue == null ? 
"" : attrValue.toString());
+        if (activeRestartHandler == null) return;
+        final Object attrValue = document.hasAttribute(PLUGINS_EXCLUDE_ATTR)
+                ? document.getAttribute(PLUGINS_EXCLUDE_ATTR) : null;
+        applyExclusion(parseExcludeList(attrValue == null ? "" : 
attrValue.toString()));
+    }
 
-        if (!excludeList.equals(currentExcludedPlugins)) {
-            currentExcludedPlugins = excludeList;
-            invokeRestartHandler(excludeList);
+    /**
+     * Applies a section-level {@code :gremlin-docs-plugins-exclude:} 
attribute encountered during
+     * the walk. Unlike the document baseline, an absent attribute on a 
section means "inherit"
+     * (no change), so only sections that declare the attribute trigger a 
transition.
+     */
+    private void maybeApplySectionExclusion(final StructuralNode node) {
+        if (activeRestartHandler == null || 
!"section".equals(node.getContext())) return;
+        final Object exclude = node.getAttribute(PLUGINS_EXCLUDE_ATTR);
+        if (exclude != null) {
+            applyExclusion(parseExcludeList(exclude.toString()));
         }
     }
 
+    /**
+     * Transitions the active plugin exclusion set. When it changes, the 
current console is closed,
+     * the restart handler toggles the plugin directories (and {@code 
plugins.txt}), and the next
+     * gremlin block lazily starts a fresh console with the new classpath.
+     */
+    private void applyExclusion(final List<String> excludeList) {
+        final List<String> current = currentExcludedPlugins == null
+                ? Collections.emptyList() : currentExcludedPlugins;
+        currentExcludedPlugins = excludeList;
+        if (excludeList.equals(current)) return;
+        closeConsole();
+        invokeRestartHandler(excludeList);
+        sugarLoaded = false;
+    }
+
     /**
      * Parses a comma-separated list of plugin names into a sorted, 
deduplicated list.
      */
@@ -213,13 +240,14 @@ public class GremlinTreeprocessor extends Treeprocessor {
 
     private void invokeRestartHandler(final List<String> excludedPlugins) {
         try {
-            restartHandler.onRestart(excludedPlugins);
+            activeRestartHandler.onRestart(excludedPlugins);
         } catch (final IOException e) {
             throw new RuntimeException("Failed to restart console with 
excluded plugins: " + excludedPlugins, e);
         }
     }
 
     private void processBlock(final StructuralNode node, final boolean dryRun) 
{
+        maybeApplySectionExclusion(node);
         final List<StructuralNode> blocks = node.getBlocks();
         for (int i = 0; i < blocks.size(); i++) {
             final StructuralNode block = blocks.get(i);
@@ -675,15 +703,26 @@ public class GremlinTreeprocessor extends Treeprocessor {
     }
 
     private void restartConsole() {
+        closeConsole();
+        if (lazyConsoleWasClosed) {
+            // Allow OS to reclaim resources from dead console and its children
+            try { Thread.sleep(2000); } catch (final InterruptedException e) { 
Thread.currentThread().interrupt(); }
+        }
+        currentGraph = null;
+        ensureConsoleStarted();
+    }
+
+    private boolean lazyConsoleWasClosed;
+
+    /** Closes the lazily-started console (if any) so the next block starts a 
fresh one. No-op in test mode. */
+    private void closeConsole() {
+        lazyConsoleWasClosed = lazyConsole != null;
         if (lazyConsole != null) {
             lazyConsole.close();
             lazyConsole = null;
             resolvedExecutor = null;
-            // Allow OS to reclaim resources from dead console and its children
-            try { Thread.sleep(2000); } catch (final InterruptedException e) { 
Thread.currentThread().interrupt(); }
         }
         currentGraph = null;
-        ensureConsoleStarted();
     }
 
     /**
diff --git 
a/tools/tinkerpop-docs/src/main/java/org/apache/tinkerpop/gremlin/docs/PluginDirectoryRestartHandler.java
 
b/tools/tinkerpop-docs/src/main/java/org/apache/tinkerpop/gremlin/docs/PluginDirectoryRestartHandler.java
new file mode 100644
index 0000000000..ea1763e906
--- /dev/null
+++ 
b/tools/tinkerpop-docs/src/main/java/org/apache/tinkerpop/gremlin/docs/PluginDirectoryRestartHandler.java
@@ -0,0 +1,103 @@
+/*
+ * 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.docs;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+/**
+ * Physically toggles plugin availability in a Gremlin Console distribution so 
that conflicting
+ * plugins (e.g. Neo4j's Scala 2.11 vs Spark's Scala 2.12) never share one 
classpath.
+ * <p>
+ * The console's {@code bin/gremlin.sh} composes its classpath from {@code 
lib/*.jar} plus
+ * {@code ext/<plugin>/plugin/*}. To exclude a plugin this handler moves 
{@code ext/<plugin>}
+ * out to {@code ext-disabled/<plugin>} (off-classpath) and removes its 
activation class from
+ * {@code ext/plugins.txt}; to re-include it the directory is moved back and 
the class restored.
+ * Keeping {@code plugins.txt} in sync is required because the console 
rewrites that file on
+ * startup, permanently dropping any listed plugin whose jars are missing.
+ */
+final class PluginDirectoryRestartHandler implements ConsoleRestartHandler {
+
+    private static final Logger LOG = 
Logger.getLogger(PluginDirectoryRestartHandler.class.getName());
+
+    /** Toggleable plugin directory -> activation class written to 
ext/plugins.txt. */
+    private static final Map<String, String> TOGGLEABLE = 
Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
+        put("neo4j-gremlin", 
"org.apache.tinkerpop.gremlin.neo4j.jsr223.Neo4jGremlinPlugin");
+        put("spark-gremlin", 
"org.apache.tinkerpop.gremlin.spark.jsr223.SparkGremlinPlugin");
+        put("hadoop-gremlin", 
"org.apache.tinkerpop.gremlin.hadoop.jsr223.HadoopGremlinPlugin");
+    }});
+
+    private final Path extDir;
+    private final Path disabledDir;
+    private final Path pluginsTxt;
+
+    PluginDirectoryRestartHandler(final Path consoleHome) {
+        this.extDir = consoleHome.resolve("ext");
+        this.disabledDir = consoleHome.resolve("ext-disabled");
+        this.pluginsTxt = extDir.resolve("plugins.txt");
+    }
+
+    @Override
+    public void onRestart(final List<String> excludedPlugins) throws 
IOException {
+        for (final String plugin : TOGGLEABLE.keySet()) {
+            if (excludedPlugins.contains(plugin)) {
+                disable(plugin);
+            } else {
+                enable(plugin);
+            }
+        }
+    }
+
+    private void disable(final String plugin) throws IOException {
+        final Path active = extDir.resolve(plugin);
+        if (Files.isDirectory(active)) {
+            Files.createDirectories(disabledDir);
+            Files.move(active, disabledDir.resolve(plugin), 
StandardCopyOption.REPLACE_EXISTING);
+            LOG.info("Excluded plugin: " + plugin);
+        }
+        setPluginEnabled(TOGGLEABLE.get(plugin), false);
+    }
+
+    private void enable(final String plugin) throws IOException {
+        final Path disabled = disabledDir.resolve(plugin);
+        if (Files.isDirectory(disabled)) {
+            Files.move(disabled, extDir.resolve(plugin), 
StandardCopyOption.REPLACE_EXISTING);
+            LOG.info("Restored plugin: " + plugin);
+        }
+        setPluginEnabled(TOGGLEABLE.get(plugin), true);
+    }
+
+    /** Adds or removes a single activation class line in ext/plugins.txt, 
preserving the rest. */
+    private void setPluginEnabled(final String pluginClass, final boolean 
enabled) throws IOException {
+        if (!Files.exists(pluginsTxt)) return;
+        final List<String> lines = Files.readAllLines(pluginsTxt).stream()
+                .filter(l -> !l.trim().equals(pluginClass))
+                .collect(Collectors.toList());
+        if (enabled) lines.add(pluginClass);
+        Files.write(pluginsTxt, lines);
+    }
+}
diff --git 
a/tools/tinkerpop-docs/src/test/java/org/apache/tinkerpop/gremlin/docs/GremlinTreeprocessorTest.java
 
b/tools/tinkerpop-docs/src/test/java/org/apache/tinkerpop/gremlin/docs/GremlinTreeprocessorTest.java
index ac15a64679..e22dcb551c 100644
--- 
a/tools/tinkerpop-docs/src/test/java/org/apache/tinkerpop/gremlin/docs/GremlinTreeprocessorTest.java
+++ 
b/tools/tinkerpop-docs/src/test/java/org/apache/tinkerpop/gremlin/docs/GremlinTreeprocessorTest.java
@@ -513,6 +513,50 @@ public class GremlinTreeprocessorTest {
         }
     }
 
+    @Test
+    public void 
shouldInvokeRestartHandlerForSectionLevelExcludeWithinOneDocument() {
+        final List<List<String>> restartCalls = new ArrayList<>();
+        final ConsoleRestartHandler handler = restartCalls::add;
+        final RecordingExecutor executor = new RecordingExecutor("==>v[1]");
+        final GremlinTreeprocessor processor = new 
GremlinTreeprocessor(executor, handler);
+
+        try (final Asciidoctor asciidoctor = Asciidoctor.Factory.create()) {
+            asciidoctor.unregisterAllExtensions();
+            asciidoctor.javaExtensionRegistry().treeprocessor(processor);
+            // Single document with per-section exclusions changing 
mid-document, as in the reference book.
+            final String input = "= Book\n\n"
+                    + "== 
Neo4j\n[gremlin-groovy,modern]\n----\ng.V(1)\n----\n\n"
+                    + "[gremlin-docs-plugins-exclude=\"neo4j-gremlin\"]\n"
+                    + "== 
Spark\n[gremlin-groovy,modern]\n----\ng.V(1)\n----\n";
+            asciidoctor.convert(input, Options.builder().build());
+            assertThat(restartCalls.size(), is(1));
+            assertThat(restartCalls.get(0).contains("neo4j-gremlin"), 
is(true));
+        }
+    }
+
+    @Test
+    public void shouldLatchSectionExclusionUntilChanged() {
+        final List<List<String>> restartCalls = new ArrayList<>();
+        final ConsoleRestartHandler handler = restartCalls::add;
+        final RecordingExecutor executor = new RecordingExecutor("==>v[1]");
+        final GremlinTreeprocessor processor = new 
GremlinTreeprocessor(executor, handler);
+
+        try (final Asciidoctor asciidoctor = Asciidoctor.Factory.create()) {
+            asciidoctor.unregisterAllExtensions();
+            asciidoctor.javaExtensionRegistry().treeprocessor(processor);
+            // Two consecutive excluding sections sharing the same set => one 
restart;
+            // a later section with no attribute inherits (no extra restart).
+            final String input = "= Book\n\n"
+                    + "[gremlin-docs-plugins-exclude=\"neo4j-gremlin\"]\n"
+                    + "== 
Hadoop\n[gremlin-groovy,modern]\n----\ng.V(1)\n----\n\n"
+                    + "[gremlin-docs-plugins-exclude=\"neo4j-gremlin\"]\n"
+                    + "== 
Spark\n[gremlin-groovy,modern]\n----\ng.V(1)\n----\n\n"
+                    + "== 
Compilers\n[gremlin-groovy,modern]\n----\ng.V(1)\n----\n";
+            asciidoctor.convert(input, Options.builder().build());
+            assertThat(restartCalls.size(), is(1));
+        }
+    }
+
     @Test
     public void shouldParseExcludeListWithWhitespace() {
         final List<String> result = GremlinTreeprocessor.parseExcludeList(" 
neo4j-gremlin , spark-gremlin , hadoop-gremlin ");


Reply via email to