From a19e921c7da1656a24d8c7a80075faf3a1470efd Mon Sep 17 00:00:00 2001 From: duncdrum Date: Sun, 5 Jul 2026 15:08:31 +0200 Subject: [PATCH 01/11] [feature] Add enabled="no" to disable modules, triggers, jobs, and SAX parser features in conf.xml Co-Authored-By: Claude Sonnet 4.6 --- .../java/org/exist/util/Configuration.java | 31 +++++++++-- .../org/exist/util/ParametersExtractor.java | 4 ++ .../resources/org/exist/xmldb/allowAnyUri.xml | 1 - exist-distribution/src/main/config/conf.xml | 31 ++++------- .../resources-filtered/lazy-cache-conf.xml | 1 - .../non-lazy-cache-conf.xml | 1 - schema/conf.xsd | 51 ++++++++++++++++++- schema/generate-conf-fixture.xsl | 10 ++++ 8 files changed, 103 insertions(+), 27 deletions(-) diff --git a/exist-core/src/main/java/org/exist/util/Configuration.java b/exist-core/src/main/java/org/exist/util/Configuration.java index bbead109e73..519d23aff27 100644 --- a/exist-core/src/main/java/org/exist/util/Configuration.java +++ b/exist-core/src/main/java/org/exist/util/Configuration.java @@ -591,6 +591,12 @@ private void loadModuleClasses(final Element xquery, throw (new DatabaseConfigurationException("element 'module' requires an attribute 'uri'")); } + // enabled="no" disables the module without removing it from conf.xml + if ("no".equalsIgnoreCase(elem.getAttribute("enabled"))) { + LOG.debug("Module '{}' is disabled via enabled=\"no\", skipping", uri); + continue; + } + final String clazz = elem.getAttribute(BUILT_IN_MODULE_CLASS_ATTRIBUTE); final String source = elem.getAttribute(BUILT_IN_MODULE_SOURCE_ATTRIBUTE); // either class or source attribute must be present @@ -836,6 +842,12 @@ private void configureScheduler(final Element scheduler) { } private void addJobToList(final List jobList, final Element job) { + // enabled="no" disables the job without removing it from conf.xml + if ("no".equalsIgnoreCase(getConfigAttributeValue(job, "enabled"))) { + LOG.debug("Job '{}' is disabled via enabled=\"no\", skipping", getConfigAttributeValue(job, JOB_NAME_ATTRIBUTE)); + return; + } + //get the job type final String strJobType = getConfigAttributeValue(job, JOB_TYPE_ATTRIBUTE); @@ -1107,6 +1119,12 @@ private void configureStartup(final Element startup) throws DatabaseConfiguratio // Get element final Element trigger = (Element) nlTrigger.item(i); + // enabled="no" disables the trigger without removing it from conf.xml + if ("no".equalsIgnoreCase(trigger.getAttribute("enabled"))) { + LOG.debug("Startup trigger '{}' is disabled via enabled=\"no\", skipping", trigger.getAttribute("class")); + continue; + } + // Get @class final String startupTriggerClass = trigger.getAttribute("class"); @@ -1191,10 +1209,17 @@ private void configureIndexer(final Document doc, final Element indexer) throws return; } final NodeList module = ((Element) modules.item(0)).getElementsByTagName(IndexManager.CONFIGURATION_MODULE_ELEMENT_NAME); - final IndexModuleConfig[] modConfig = new IndexModuleConfig[module.getLength()]; + final List modConfigList = new ArrayList<>(); for (int i = 0; i < module.getLength(); i++) { final Element elem = (Element) module.item(i); + + // enabled="no" disables the index module without removing it from conf.xml + if ("no".equalsIgnoreCase(elem.getAttribute("enabled"))) { + LOG.debug("Index module '{}' is disabled via enabled=\"no\", skipping", elem.getAttribute(IndexManager.INDEXER_MODULES_ID_ATTRIBUTE)); + continue; + } + final String className = elem.getAttribute(IndexManager.INDEXER_MODULES_CLASS_ATTRIBUTE); final String id = elem.getAttribute(IndexManager.INDEXER_MODULES_ID_ATTRIBUTE); @@ -1206,9 +1231,9 @@ private void configureIndexer(final Document doc, final Element indexer) throws throw (new DatabaseConfigurationException("Required attribute id is missing for module")); } - modConfig[i] = new IndexModuleConfig(id, className, elem); + modConfigList.add(new IndexModuleConfig(id, className, elem)); } - setProperty(IndexManager.PROPERTY_INDEXER_MODULES, modConfig); + setProperty(IndexManager.PROPERTY_INDEXER_MODULES, modConfigList.toArray(new IndexModuleConfig[0])); } private void configureValidation(final Optional dbHome, final Element validation) { diff --git a/exist-core/src/main/java/org/exist/util/ParametersExtractor.java b/exist-core/src/main/java/org/exist/util/ParametersExtractor.java index 28f0cfa15e8..7fc78febab0 100644 --- a/exist-core/src/main/java/org/exist/util/ParametersExtractor.java +++ b/exist-core/src/main/java/org/exist/util/ParametersExtractor.java @@ -255,6 +255,10 @@ private static Properties parseProperties(final Node container, final String ele for (int i = 0; i < params.getLength(); i++) { final Element param = ((Element) params.item(i)); + if ("no".equals(param.getAttribute("enabled"))) { + continue; + } + final String name = param.getAttribute("name"); final String value = param.getAttribute("value"); diff --git a/exist-core/src/test/resources/org/exist/xmldb/allowAnyUri.xml b/exist-core/src/test/resources/org/exist/xmldb/allowAnyUri.xml index a938990b72a..ef6f4ded5fd 100644 --- a/exist-core/src/test/resources/org/exist/xmldb/allowAnyUri.xml +++ b/exist-core/src/test/resources/org/exist/xmldb/allowAnyUri.xml @@ -924,7 +924,6 @@ xmldb:xcollection() should return an empty sequence when an XML resource can not be retrieved. --> - - + - + @@ -547,9 +547,7 @@ - + @@ -571,9 +569,7 @@ distribution of elements in the database, which can be used by the query optimizer for additional optimizations. --> - + - - @@ -998,22 +993,18 @@ - - + + - + @@ -1053,13 +1044,13 @@ - + - + diff --git a/extensions/modules/cache/src/test/resources-filtered/lazy-cache-conf.xml b/extensions/modules/cache/src/test/resources-filtered/lazy-cache-conf.xml index af9663be608..0350bee319a 100644 --- a/extensions/modules/cache/src/test/resources-filtered/lazy-cache-conf.xml +++ b/extensions/modules/cache/src/test/resources-filtered/lazy-cache-conf.xml @@ -735,7 +735,6 @@ xmldb:xcollection() should return an empty sequence when an XML resource can not be retrieved. --> - - + version="2.3.0"> @@ -36,6 +36,13 @@ + + + + Optional human-readable description of the parameter's purpose. + + + @@ -64,6 +71,13 @@ + + + + Set to "no" to disable this trigger without removing it from the configuration. + + + @@ -637,6 +651,13 @@ + + + + Set to "no" to disable this index module without removing it from the configuration. + + + @@ -823,6 +844,13 @@ + + + + Set to "no" to disable this job without removing it from the configuration. + + + @@ -853,6 +881,13 @@ + + + + Set to "no" to disable this feature flag without removing it from the configuration. + + + @@ -907,6 +942,13 @@ + + + + Set to "no" to disable this feature flag without removing it from the configuration. + + + @@ -1214,6 +1256,13 @@ + + + + Set to "no" to disable this module without removing it from the configuration. + + + diff --git a/schema/generate-conf-fixture.xsl b/schema/generate-conf-fixture.xsl index 98b44a42248..f5e931d6a3d 100644 --- a/schema/generate-conf-fixture.xsl +++ b/schema/generate-conf-fixture.xsl @@ -125,6 +125,10 @@ + + + @@ -135,6 +139,9 @@ + + + @@ -146,9 +153,12 @@ Canonical's RestXq/AutoDeployment startup triggers assume a full webapp deployment, a RESTXQ registry, an autodeploy directory, neither present nor wanted in an isolated test fixture, so they're dropped by default; restored per-fixture via $extra-triggers if needed. + XQueryStartupTrigger is dropped too: it requires /db/system/autostart infrastructure not + present in test fixtures. Re-add via $extra-triggers when needed. --> + From 9ec6fd7ec96123932a449f511997e39e0b53a648 Mon Sep 17 00:00:00 2001 From: duncdrum Date: Sun, 5 Jul 2026 16:36:47 +0200 Subject: [PATCH 02/11] [feature] Complete @enabled coverage: startup triggers with enabled="yes", scheduler jobs as live enabled="no" entries, @enabled on and in conf.xsd; bump schema to 2.5.0 Co-Authored-By: Claude Sonnet 4.6 --- exist-distribution/src/main/config/conf.xml | 41 +++++++++------------ schema/conf.xsd | 16 +++++++- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/exist-distribution/src/main/config/conf.xml b/exist-distribution/src/main/config/conf.xml index e614827b319..c8a3dcaa40b 100644 --- a/exist-distribution/src/main/config/conf.xml +++ b/exist-distribution/src/main/config/conf.xml @@ -58,7 +58,7 @@ - http://exist-db.org/exist/apps/doc/documentation.xml --> - + - + - + @@ -212,12 +212,12 @@ - - - - - + --> + @@ -662,8 +662,7 @@ backup Set to "yes" to create a backup whenever the job runs, not just when it detects errors. --> - - - - diff --git a/schema/conf.xsd b/schema/conf.xsd index 89be6c9ff52..940c93baf3a 100644 --- a/schema/conf.xsd +++ b/schema/conf.xsd @@ -8,7 +8,7 @@ + version="2.5.0"> @@ -43,6 +43,13 @@ + + + + Set to "no" to disable this parameter without removing it from the configuration. + + + @@ -918,6 +925,13 @@ + + + + Set to "no" to disable this property without removing it from the configuration. + + + From fcb5d34756fbe1aa27fd29970c35ee68f0c648ac Mon Sep 17 00:00:00 2001 From: duncdrum Date: Sun, 5 Jul 2026 16:14:43 +0200 Subject: [PATCH 03/11] [feature] Tighten conf.xsd types, patterns, and docs; bump schema to 2.6.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add memorySizeType (pattern [0-9]+[KkMmGgTt]?|-1); apply to cacheSize, collectionCache, minDiskSpace, recovery/@size - pool/@min, @max: xs:integer → xs:positiveInteger - lock-table/@trace-stack-depth: xs:int → xs:nonNegativeInteger - indexer/create/@type: xs:anySimpleType → xs:string - transformer/@caching: xs:string → yes_no with default="yes" - scheduler/job/@period: xs:string → xs:positiveInteger - vector-models/model/@dimension: xs:integer → xs:positiveInteger - Fix swapped docs on raise-error-on-failed-retrieval / enforce-index-use - Fix "group-comit" typo in recovery/@group-commit doc - Fix rpc-server in-memory-size default 4196 → 4096 - Fill in TODO docs: indexer/create attrs, flushAfter, n, xquery module attrs (class/uri/src), xupdate/@growth-factor Co-Authored-By: Claude Sonnet 4.6 --- exist-distribution/src/main/config/conf.xml | 2 +- schema/conf.xsd | 98 +++++++++++++-------- 2 files changed, 62 insertions(+), 38 deletions(-) diff --git a/exist-distribution/src/main/config/conf.xml b/exist-distribution/src/main/config/conf.xml index c8a3dcaa40b..4aa436deaea 100644 --- a/exist-distribution/src/main/config/conf.xml +++ b/exist-distribution/src/main/config/conf.xml @@ -58,7 +58,7 @@ - http://exist-db.org/exist/apps/doc/documentation.xml --> - + + + + + + @@ -95,14 +105,14 @@ - + Maximum number of connections allowed. - + Minimum number of connections to keep alive. @@ -172,7 +182,7 @@ it will wait until the current file buffer (32kb) is really full. This can speed up eXist on some systems where a file sync is an expensive operation (mainly windows - XP; not necessary on Linux). However, group-comit="yes" + XP; not necessary on Linux). However, group-commit="yes" will increase the risk of an already committed operation being rolled back after a database crash. @@ -187,7 +197,7 @@ - + This attributes sets the maximum allowed size of the journal @@ -288,7 +298,7 @@ - + The maximum amount of memory to use for database page buffers. @@ -324,7 +334,7 @@ - + Maximum amount of memory (in megabytes) to use for collection caches. @@ -363,7 +373,7 @@ - + The amount of disk space (in megabytes) which should be available for @@ -470,7 +480,7 @@ - + When set above 0, this captures n frames of each threads stack that performs a try/lock/release @@ -640,14 +650,18 @@ - TODO + Number of documents to index before the n-gram index + is flushed to disk. Higher values improve throughput + at the cost of memory during bulk ingest. - TODO + The n in n-gram: the number of characters per token. + Default 3 produces trigrams, which support efficient + substring searches via the ngram:contains() function. @@ -678,21 +692,25 @@ - TODO + XPath expression to the node to index (e.g. "//book/title"). + Mutually exclusive with qname. - TODO + Qualified name of the element or attribute to index + (e.g. "title" or "@id"). Mutually exclusive with path. - + - TODO + XQuery/XML Schema type to cast node values to when indexing + (e.g. "xs:integer", "xs:dateTime"). Determines the comparison + semantics available on this index. @@ -826,7 +844,7 @@ - + Can be used to define an explicit period for firing the job instead @@ -1154,11 +1172,10 @@ - + - You can enable or disable xsl caching by this option. - This option is set to "yes" by default. + Set to "no" to disable XSL stylesheet caching. @@ -1252,21 +1269,27 @@ - TODO + Fully-qualified class name of the Java module implementation. + The class must implement org.exist.xquery.Module. + Mutually exclusive with src. - TODO + Namespace URI that this module is bound to + (e.g. "http://exist-db.org/xquery/math"). + Must match the URI declared inside the module source. - TODO + Path to an XQuery library module source file. + Used instead of class for XQuery-implemented modules. + Mutually exclusive with class. @@ -1318,24 +1341,23 @@ - When set to "strict", eXist will not use a range index unless all - collections in the context sequence define it. When set to - "always", the query engine will still use an index, even if only - one collection has it defined. It thus leaves it to the user to - properly define indexes and if you forget to specify an index on - a particular collection, it will be missing in the results. + Set to "yes" if a call to doc(), xmldb:document(), collection() or + xmldb:xcollection() should raise an error (FODC0002) when an + XML resource cannot be retrieved. + Set to "no" (default) if such a call should return an empty sequence + when the resource cannot be retrieved. - Set to "yes" if a call to doc(), xmldb:document(), collection() or - xmldb:xcollection() should raise an error (FODC0002) when an - XML resource can not be retrieved. - Set to "no" if a call to doc(), xmldb:document(), collection() or - xmldb:xcollection() should return an empty sequence when an - XML resource can not be retrieved. + Controls whether range indexes must be defined on all collections + in the context sequence before the query engine will use them. + "strict" — the index is used only if every collection in the context + defines it; otherwise the engine falls back to a full scan. + "always" — the engine uses an available index even if only some + collections define it; results may be incomplete if others lack the index. @@ -1371,7 +1393,9 @@ - TODO + Percentage by which the DOM storage file grows when it needs to + be expanded to accommodate new data. A value of 20 means the + file grows by 20% of its current size each time it is extended. @@ -1410,7 +1434,7 @@ - + Defines the maximum amount of bytes stored in memory for each content file instance. @@ -1467,7 +1491,7 @@ Local path or HuggingFace/API URL - + Embedding dimension (default 384) From e80ba7afe4c441c03468431203be40351bdb6fca Mon Sep 17 00:00:00 2001 From: duncdrum Date: Sun, 5 Jul 2026 20:04:15 +0200 Subject: [PATCH 04/11] [bugfix] Fix enabled check case-sensitivity; strip @enabled from fixture triggers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ParametersExtractor.parseProperties() used case-sensitive .equals("no") while every other enabled check in Configuration.java uses .equalsIgnoreCase(). An enabled="NO" parameter would be silently included instead of skipped. generate-conf-fixture.xsl stripped @enabled from kept module/index nodes but not from surviving triggers, so BouncyCastle and URLStreamHandler emitted an explicit enabled="yes" in generated fixtures — inconsistent with the module treatment and noise in fixture diffs. Co-Authored-By: Claude Sonnet 4.6 --- .../src/main/java/org/exist/util/ParametersExtractor.java | 2 +- schema/generate-conf-fixture.xsl | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/exist-core/src/main/java/org/exist/util/ParametersExtractor.java b/exist-core/src/main/java/org/exist/util/ParametersExtractor.java index 7fc78febab0..3454fef082d 100644 --- a/exist-core/src/main/java/org/exist/util/ParametersExtractor.java +++ b/exist-core/src/main/java/org/exist/util/ParametersExtractor.java @@ -255,7 +255,7 @@ private static Properties parseProperties(final Node container, final String ele for (int i = 0; i < params.getLength(); i++) { final Element param = ((Element) params.item(i)); - if ("no".equals(param.getAttribute("enabled"))) { + if ("no".equalsIgnoreCase(param.getAttribute("enabled"))) { continue; } diff --git a/schema/generate-conf-fixture.xsl b/schema/generate-conf-fixture.xsl index f5e931d6a3d..542fcd4a8e2 100644 --- a/schema/generate-conf-fixture.xsl +++ b/schema/generate-conf-fixture.xsl @@ -159,6 +159,9 @@ + + From f67b3a6f528174c77fa37c1c997a3021d1159713 Mon Sep 17 00:00:00 2001 From: duncdrum Date: Sun, 5 Jul 2026 15:21:30 +0200 Subject: [PATCH 05/11] [feature] Add ModuleFactory SPI; wire all 27 bundled modules; trim canonical conf.xml Co-Authored-By: Claude Sonnet 4.6 --- .../org/exist/backup/xquery/BackupModule.java | 4 ++ .../java/org/exist/util/Configuration.java | 17 +++++- .../java/org/exist/xquery/ModuleFactory.java | 60 +++++++++++++++++++ .../xquery/functions/array/ArrayModule.java | 5 ++ .../functions/inspect/InspectionModule.java | 5 ++ .../exist/xquery/functions/map/MapModule.java | 5 ++ .../xquery/functions/math/MathModule.java | 5 ++ .../functions/request/RequestModule.java | 5 ++ .../functions/response/ResponseModule.java | 5 ++ .../SecurityManagerModule.java | 5 ++ .../functions/session/SessionModule.java | 5 ++ .../xquery/functions/system/SystemModule.java | 5 ++ .../functions/transform/TransformModule.java | 4 ++ .../xquery/functions/util/UtilModule.java | 5 ++ .../validation/ValidationModule.java | 5 ++ .../websocket/ConsoleCompatModule.java | 5 ++ .../functions/websocket/WebSocketModule.java | 5 ++ .../xquery/functions/xmldb/XMLDBModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 16 +++++ .../test/resources-filtered/conf-fixture.xsl | 19 ++---- .../exist/storage/statistics/conf-fixture.xsl | 17 ++---- .../org/exist/xquery/conf-fixture.xsl | 17 ++---- .../functions/transform/conf-fixture.xsl | 17 ++---- exist-distribution/src/main/config/conf.xml | 46 ++------------ .../xquery/ContentExtractionModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../main/java/org/expath/exist/ZipModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../modules/request/RequestModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../restxq/impl/xquery/RestXqModule.java | 5 ++ .../impl/xquery/exist/ExistRestXqModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 2 + .../xquery/modules/lucene/LuceneModule.java | 7 ++- .../services/org.exist.xquery.ModuleFactory | 1 + .../xquery/modules/ngram/NGramModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../modules/range/RangeIndexModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../exist/xquery/modules/sort/SortModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../xquery/modules/cache/CacheModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../compression/CompressionModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../xquery/modules/counter/CounterModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../modules/cqlparser/CQLParserModule.java | 4 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../expathrepo/ExpathPackageModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../exist/xquery/modules/file/FileModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../modules/httpclient/HttpClientModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../xquery/modules/image/ImageModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../exist/xquery/modules/jndi/JNDIModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../exist/xquery/modules/mail/MailModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../PersistentLoginModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../xquery/modules/process/ProcessModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../modules/scheduler/SchedulerModule.java | 4 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../modules/simpleql/SimpleQLModule.java | 4 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../exist/xquery/modules/sql/SQLModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../xquery/modules/xmldiff/XmlDiffModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../xquery/modules/xslfo/XSLFOModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + .../org/exist/xqdoc/xquery/XQDocModule.java | 5 ++ .../services/org.exist.xquery.ModuleFactory | 1 + 77 files changed, 354 insertions(+), 94 deletions(-) create mode 100644 exist-core/src/main/java/org/exist/xquery/ModuleFactory.java create mode 100644 exist-core/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/contentextraction/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/expath/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/exquery/modules/request/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/exquery/restxq/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/indexes/lucene/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/indexes/ngram/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/indexes/range/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/indexes/sort/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/modules/cache/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/modules/compression/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/modules/counter/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/modules/cqlparser/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/modules/expathrepo/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/modules/file/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/modules/http-client/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/modules/image/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/modules/jndi/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/modules/mail/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/modules/persistentlogin/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/modules/process/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/modules/scheduler/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/modules/simpleql/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/modules/sql/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/modules/xmldiff/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/modules/xslfo/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory create mode 100644 extensions/xqdoc/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory diff --git a/exist-core/src/main/java/org/exist/backup/xquery/BackupModule.java b/exist-core/src/main/java/org/exist/backup/xquery/BackupModule.java index f86d74dd085..d8b0114a468 100644 --- a/exist-core/src/main/java/org/exist/backup/xquery/BackupModule.java +++ b/exist-core/src/main/java/org/exist/backup/xquery/BackupModule.java @@ -75,4 +75,8 @@ public String getReleaseVersion() return( RELEASED_IN_VERSION ); } + public static final class Factory implements org.exist.xquery.ModuleFactory { + @Override public String getNamespaceURI() { return NAMESPACE_URI; } + @Override public Class getModuleClass() { return BackupModule.class; } + } } diff --git a/exist-core/src/main/java/org/exist/util/Configuration.java b/exist-core/src/main/java/org/exist/util/Configuration.java index 519d23aff27..a5b07f60ea8 100644 --- a/exist-core/src/main/java/org/exist/util/Configuration.java +++ b/exist-core/src/main/java/org/exist/util/Configuration.java @@ -36,6 +36,7 @@ import org.exist.storage.lock.LockTable; import org.exist.util.io.ContentFilePool; import org.exist.xquery.Expression; +import org.exist.xquery.ModuleFactory; import org.exist.xquery.PerformanceStats; import org.exist.xquery.XQueryWatchDog; import org.w3c.dom.Document; @@ -79,6 +80,7 @@ import java.util.Map.Entry; import java.util.Optional; import java.util.Properties; +import java.util.ServiceLoader; import java.util.function.Function; import javax.annotation.Nullable; @@ -573,6 +575,18 @@ private void loadModuleClasses(final Element xquery, // add the standard function module modulesClassMap.put(XPATH_FUNCTIONS_NS, org.exist.xquery.functions.fn.FnModule.class); + // SPI-discovered modules: any JAR on the classpath that provides a ModuleFactory + // implementation in META-INF/services/org.exist.xquery.ModuleFactory is auto-registered. + // conf.xml entries processed below can override or suppress these entries. + ServiceLoader.load(ModuleFactory.class, Configuration.class.getClassLoader()) + .forEach(factory -> { + final String uri = factory.getNamespaceURI(); + if (!modulesClassMap.containsKey(uri)) { + modulesClassMap.put(uri, factory.getModuleClass()); + LOG.debug("Auto-registered module '{}' via ModuleFactory SPI", uri); + } + }); + // add other modules specified in configuration configureElement(xquery, XQUERY_BUILTIN_MODULES_CONFIGURATION_MODULES_ELEMENT_NAME, builtIn -> { @@ -591,9 +605,10 @@ private void loadModuleClasses(final Element xquery, throw (new DatabaseConfigurationException("element 'module' requires an attribute 'uri'")); } - // enabled="no" disables the module without removing it from conf.xml + // enabled="no" disables the module; also suppresses any SPI-discovered entry if ("no".equalsIgnoreCase(elem.getAttribute("enabled"))) { LOG.debug("Module '{}' is disabled via enabled=\"no\", skipping", uri); + modulesClassMap.remove(uri); continue; } diff --git a/exist-core/src/main/java/org/exist/xquery/ModuleFactory.java b/exist-core/src/main/java/org/exist/xquery/ModuleFactory.java new file mode 100644 index 00000000000..2cbb614557c --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/ModuleFactory.java @@ -0,0 +1,60 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery; + +/** + * SPI interface for automatic XQuery module registration. + * + *

A JAR that bundles an XQuery module can self-register by: + *

    + *
  1. Providing an implementation of this interface (conventionally a static + * inner class named {@code Factory} on the module class).
  2. + *
  3. Listing the implementation's fully-qualified class name in + * {@code META-INF/services/org.exist.xquery.ModuleFactory}.
  4. + *
+ * + *

At startup, {@code Configuration} discovers all {@code ModuleFactory} + * implementations on the classpath via {@link java.util.ServiceLoader} and + * pre-populates the module registry before processing {@code conf.xml} entries. + * A {@code conf.xml} entry always wins over an SPI-discovered entry for the + * same namespace URI; {@code enabled="no"} suppresses an SPI-discovered module. + */ +public interface ModuleFactory { + + /** + * The namespace URI that uniquely identifies the module. + * Must match the value returned by {@link Module#getNamespaceURI()} for + * the module class returned by {@link #getModuleClass()}. + * + * @return namespace URI + */ + String getNamespaceURI(); + + /** + * The concrete {@link Module} implementation class to register. + * The class must have a public constructor accepting + * {@code Map> parameters}. + * + * @return module implementation class + */ + Class getModuleClass(); +} diff --git a/exist-core/src/main/java/org/exist/xquery/functions/array/ArrayModule.java b/exist-core/src/main/java/org/exist/xquery/functions/array/ArrayModule.java index c3dde997d14..71bdc86f3ba 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/array/ArrayModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/array/ArrayModule.java @@ -94,4 +94,9 @@ public String getDescription() { public String getReleaseVersion() { return "2.2.1"; } + + public static final class Factory implements org.exist.xquery.ModuleFactory { + @Override public String getNamespaceURI() { return NAMESPACE_URI; } + @Override public Class getModuleClass() { return ArrayModule.class; } + } } diff --git a/exist-core/src/main/java/org/exist/xquery/functions/inspect/InspectionModule.java b/exist-core/src/main/java/org/exist/xquery/functions/inspect/InspectionModule.java index d56cc19d721..217f428eb41 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/inspect/InspectionModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/inspect/InspectionModule.java @@ -86,4 +86,9 @@ static FunctionSignature functionSignature(final String name, final String descr static FunctionSignature[] functionSignatures(final String name, final String description, final FunctionReturnSequenceType returnType, final FunctionParameterSequenceType[][] variableParamTypes) { return FunctionDSL.functionSignatures(new QName(name, NAMESPACE_URI, PREFIX), description, returnType, variableParamTypes); } + + public static final class Factory implements org.exist.xquery.ModuleFactory { + @Override public String getNamespaceURI() { return NAMESPACE_URI; } + @Override public Class getModuleClass() { return InspectionModule.class; } + } } diff --git a/exist-core/src/main/java/org/exist/xquery/functions/map/MapModule.java b/exist-core/src/main/java/org/exist/xquery/functions/map/MapModule.java index 73a28441270..db3b5032b60 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/map/MapModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/map/MapModule.java @@ -81,4 +81,9 @@ public String getReleaseVersion() { static FunctionSignature functionSignature(final String name, final String description, final FunctionReturnSequenceType returnType, final FunctionParameterSequenceType... paramTypes) { return FunctionDSL.functionSignature(new QName(name, NAMESPACE_URI, PREFIX), description, returnType, paramTypes); } + + public static final class Factory implements org.exist.xquery.ModuleFactory { + @Override public String getNamespaceURI() { return NAMESPACE_URI; } + @Override public Class getModuleClass() { return MapModule.class; } + } } diff --git a/exist-core/src/main/java/org/exist/xquery/functions/math/MathModule.java b/exist-core/src/main/java/org/exist/xquery/functions/math/MathModule.java index be6211ee905..442f13ef4f1 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/math/MathModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/math/MathModule.java @@ -79,4 +79,9 @@ public String getDescription() { public String getReleaseVersion() { return RELEASED_IN_VERSION; } + + public static final class Factory implements org.exist.xquery.ModuleFactory { + @Override public String getNamespaceURI() { return NAMESPACE_URI; } + @Override public Class getModuleClass() { return MathModule.class; } + } } diff --git a/exist-core/src/main/java/org/exist/xquery/functions/request/RequestModule.java b/exist-core/src/main/java/org/exist/xquery/functions/request/RequestModule.java index b2099c6cb7d..a522a639a9b 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/request/RequestModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/request/RequestModule.java @@ -103,4 +103,9 @@ public String getDefaultPrefix() { public String getReleaseVersion() { return RELEASED_IN_VERSION; } + + public static final class Factory implements org.exist.xquery.ModuleFactory { + @Override public String getNamespaceURI() { return NAMESPACE_URI; } + @Override public Class getModuleClass() { return RequestModule.class; } + } } diff --git a/exist-core/src/main/java/org/exist/xquery/functions/response/ResponseModule.java b/exist-core/src/main/java/org/exist/xquery/functions/response/ResponseModule.java index d86a1870ce0..18359f38957 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/response/ResponseModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/response/ResponseModule.java @@ -82,4 +82,9 @@ public String getDefaultPrefix() { public String getReleaseVersion() { return (RELEASED_IN_VERSION); } + + public static final class Factory implements org.exist.xquery.ModuleFactory { + @Override public String getNamespaceURI() { return NAMESPACE_URI; } + @Override public Class getModuleClass() { return ResponseModule.class; } + } } diff --git a/exist-core/src/main/java/org/exist/xquery/functions/securitymanager/SecurityManagerModule.java b/exist-core/src/main/java/org/exist/xquery/functions/securitymanager/SecurityManagerModule.java index 4577446871c..7d7e9316c44 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/securitymanager/SecurityManagerModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/securitymanager/SecurityManagerModule.java @@ -140,4 +140,9 @@ public String getDescription() { public String getReleaseVersion() { return RELEASED_IN_VERSION; } + + public static final class Factory implements org.exist.xquery.ModuleFactory { + @Override public String getNamespaceURI() { return NAMESPACE_URI; } + @Override public Class getModuleClass() { return SecurityManagerModule.class; } + } } \ No newline at end of file diff --git a/exist-core/src/main/java/org/exist/xquery/functions/session/SessionModule.java b/exist-core/src/main/java/org/exist/xquery/functions/session/SessionModule.java index 905ad32ae27..9175750c08b 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/session/SessionModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/session/SessionModule.java @@ -86,4 +86,9 @@ public String getDefaultPrefix() { public String getReleaseVersion() { return RELEASED_IN_VERSION; } + + public static final class Factory implements org.exist.xquery.ModuleFactory { + @Override public String getNamespaceURI() { return NAMESPACE_URI; } + @Override public Class getModuleClass() { return SessionModule.class; } + } } diff --git a/exist-core/src/main/java/org/exist/xquery/functions/system/SystemModule.java b/exist-core/src/main/java/org/exist/xquery/functions/system/SystemModule.java index af003787f99..90253881272 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/system/SystemModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/system/SystemModule.java @@ -117,4 +117,9 @@ static FunctionSignature functionSignature(final String name, final String descr static FunctionSignature[] functionSignatures(final String name, final String description, final FunctionReturnSequenceType returnType, final FunctionParameterSequenceType[][] variableParamTypes) { return FunctionDSL.functionSignatures(new QName(name, NAMESPACE_URI, PREFIX), description, returnType, variableParamTypes); } + + public static final class Factory implements org.exist.xquery.ModuleFactory { + @Override public String getNamespaceURI() { return NAMESPACE_URI; } + @Override public Class getModuleClass() { return SystemModule.class; } + } } diff --git a/exist-core/src/main/java/org/exist/xquery/functions/transform/TransformModule.java b/exist-core/src/main/java/org/exist/xquery/functions/transform/TransformModule.java index a2f3564de8b..6b236845ae9 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/transform/TransformModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/transform/TransformModule.java @@ -76,4 +76,8 @@ public String getReleaseVersion() { return RELEASED_IN_VERSION; } + public static final class Factory implements org.exist.xquery.ModuleFactory { + @Override public String getNamespaceURI() { return NAMESPACE_URI; } + @Override public Class getModuleClass() { return TransformModule.class; } + } } diff --git a/exist-core/src/main/java/org/exist/xquery/functions/util/UtilModule.java b/exist-core/src/main/java/org/exist/xquery/functions/util/UtilModule.java index f99a4b9bbbe..8a71d3060c3 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/util/UtilModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/util/UtilModule.java @@ -219,5 +219,10 @@ static FunctionSignature functionSignature(final String name, final String descr static FunctionSignature[] functionSignatures(final String name, final String description, final FunctionReturnSequenceType returnType, final FunctionParameterSequenceType[][] variableParamTypes) { return FunctionDSL.functionSignatures(new QName(name, NAMESPACE_URI, PREFIX), description, returnType, variableParamTypes); } + + public static final class Factory implements org.exist.xquery.ModuleFactory { + @Override public String getNamespaceURI() { return NAMESPACE_URI; } + @Override public Class getModuleClass() { return UtilModule.class; } + } } diff --git a/exist-core/src/main/java/org/exist/xquery/functions/validation/ValidationModule.java b/exist-core/src/main/java/org/exist/xquery/functions/validation/ValidationModule.java index 1fd18b59c05..08d7b30396e 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/validation/ValidationModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/validation/ValidationModule.java @@ -110,4 +110,9 @@ public String getDefaultPrefix() { public String getReleaseVersion() { return RELEASED_IN_VERSION; } + + public static final class Factory implements org.exist.xquery.ModuleFactory { + @Override public String getNamespaceURI() { return NAMESPACE_URI; } + @Override public Class getModuleClass() { return ValidationModule.class; } + } } diff --git a/exist-core/src/main/java/org/exist/xquery/functions/websocket/ConsoleCompatModule.java b/exist-core/src/main/java/org/exist/xquery/functions/websocket/ConsoleCompatModule.java index f1832a569c3..9a3a0614bb2 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/websocket/ConsoleCompatModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/websocket/ConsoleCompatModule.java @@ -71,4 +71,9 @@ public String getDescription() { public String getReleaseVersion() { return "7.0.0"; } + + public static final class Factory implements org.exist.xquery.ModuleFactory { + @Override public String getNamespaceURI() { return NAMESPACE_URI; } + @Override public Class getModuleClass() { return ConsoleCompatModule.class; } + } } diff --git a/exist-core/src/main/java/org/exist/xquery/functions/websocket/WebSocketModule.java b/exist-core/src/main/java/org/exist/xquery/functions/websocket/WebSocketModule.java index 280d1166905..c32e536fb29 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/websocket/WebSocketModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/websocket/WebSocketModule.java @@ -111,4 +111,9 @@ public String getDescription() { public String getReleaseVersion() { return "7.0.0"; } + + public static final class Factory implements org.exist.xquery.ModuleFactory { + @Override public String getNamespaceURI() { return NAMESPACE_URI; } + @Override public Class getModuleClass() { return WebSocketModule.class; } + } } diff --git a/exist-core/src/main/java/org/exist/xquery/functions/xmldb/XMLDBModule.java b/exist-core/src/main/java/org/exist/xquery/functions/xmldb/XMLDBModule.java index 9f60ddd10ce..f5eec7f6e38 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/xmldb/XMLDBModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/xmldb/XMLDBModule.java @@ -153,4 +153,9 @@ static FunctionSignature functionSignature(final String name, final String descr static FunctionSignature[] functionSignatures(final String name, final String description, final FunctionReturnSequenceType returnType, final FunctionParameterSequenceType[][] variableParamTypes) { return FunctionDSL.functionSignatures(new QName(name, NAMESPACE_URI, PREFIX), description, returnType, variableParamTypes); } + + public static final class Factory implements org.exist.xquery.ModuleFactory { + @Override public String getNamespaceURI() { return NAMESPACE_URI; } + @Override public Class getModuleClass() { return XMLDBModule.class; } + } } diff --git a/exist-core/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory b/exist-core/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory new file mode 100644 index 00000000000..dfcccc6dffa --- /dev/null +++ b/exist-core/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory @@ -0,0 +1,16 @@ +org.exist.xquery.functions.map.MapModule$Factory +org.exist.xquery.functions.math.MathModule$Factory +org.exist.xquery.functions.array.ArrayModule$Factory +org.exist.backup.xquery.BackupModule$Factory +org.exist.xquery.functions.inspect.InspectionModule$Factory +org.exist.xquery.functions.request.RequestModule$Factory +org.exist.xquery.functions.response.ResponseModule$Factory +org.exist.xquery.functions.securitymanager.SecurityManagerModule$Factory +org.exist.xquery.functions.session.SessionModule$Factory +org.exist.xquery.functions.system.SystemModule$Factory +org.exist.xquery.functions.transform.TransformModule$Factory +org.exist.xquery.functions.util.UtilModule$Factory +org.exist.xquery.functions.validation.ValidationModule$Factory +org.exist.xquery.functions.websocket.WebSocketModule$Factory +org.exist.xquery.functions.websocket.ConsoleCompatModule$Factory +org.exist.xquery.functions.xmldb.XMLDBModule$Factory diff --git a/exist-core/src/test/resources-filtered/conf-fixture.xsl b/exist-core/src/test/resources-filtered/conf-fixture.xsl index ca4da98b55d..228815058b8 100644 --- a/exist-core/src/test/resources-filtered/conf-fixture.xsl +++ b/exist-core/src/test/resources-filtered/conf-fixture.xsl @@ -30,22 +30,13 @@ + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + diff --git a/extensions/vector/src/main/java/org/exist/vector/ModelRegistry.java b/extensions/vector/src/main/java/org/exist/vector/ModelRegistry.java index 09edb859952..1bfce74d953 100644 --- a/extensions/vector/src/main/java/org/exist/vector/ModelRegistry.java +++ b/extensions/vector/src/main/java/org/exist/vector/ModelRegistry.java @@ -23,6 +23,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.exist.util.Configuration; import org.exist.util.ConfigurationHelper; import javax.annotation.Nonnull; @@ -70,7 +71,33 @@ public ModelEntry(final String path, final int dimension) { } private ModelRegistry() { - load(); + } + + /** + * Initialises the registry from the parsed Configuration object. + * Called reflectively by VectorStoreServiceImpl.configure() during broker pool startup. + * Must be called before any call to getInstance() for the registry to reflect conf.xml entries. + * + * @param configuration the eXist-db configuration + */ + @SuppressWarnings("unchecked") + public static void configure(final Configuration configuration) { + final Map raw = + (Map) configuration.getProperty(Configuration.PROPERTY_VECTOR_MODELS); + final ModelRegistry r = new ModelRegistry(); + if (raw != null) { + for (final Map.Entry e : raw.entrySet()) { + final String id = e.getKey(); + final String[] vals = e.getValue(); + final String path = vals[0]; + final int dim = parseDimension(vals.length > 1 ? vals[1] : "", 384); + r.entries.put(id, new ModelEntry(path, dim)); + } + if (!r.entries.isEmpty()) { + LOG.info("Loaded {} model(s) from configuration", r.entries.size()); + } + } + instance = r; } @Nonnull @@ -80,7 +107,10 @@ public static ModelRegistry getInstance() { synchronized (ModelRegistry.class) { r = instance; if (r == null) { + // Fallback: no Configuration was provided (e.g. standalone tools, tests). + // Parse conf.xml directly as before. r = instance = new ModelRegistry(); + r.load(); } } } diff --git a/extensions/vector/src/main/java/org/exist/xquery/modules/vector/VectorModule.java b/extensions/vector/src/main/java/org/exist/xquery/modules/vector/VectorModule.java index 89beb626d97..bc7ba46bd40 100644 --- a/extensions/vector/src/main/java/org/exist/xquery/modules/vector/VectorModule.java +++ b/extensions/vector/src/main/java/org/exist/xquery/modules/vector/VectorModule.java @@ -96,6 +96,11 @@ public String getReleaseVersion() { return RELEASED_IN_VERSION; } + public static final class Factory implements org.exist.xquery.ModuleFactory { + @Override public String getNamespaceURI() { return NAMESPACE_URI; } + @Override public Class getModuleClass() { return VectorModule.class; } + } + static FunctionSignature functionSignature(final String name, final String description, final FunctionReturnSequenceType returnType, final FunctionParameterSequenceType... paramTypes) { return FunctionDSL.functionSignature(new QName(name, NAMESPACE_URI, PREFIX), description, returnType, paramTypes); diff --git a/extensions/vector/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory b/extensions/vector/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory new file mode 100644 index 00000000000..ec037bafcfb --- /dev/null +++ b/extensions/vector/src/main/resources/META-INF/services/org.exist.xquery.ModuleFactory @@ -0,0 +1 @@ +org.exist.xquery.modules.vector.VectorModule$Factory diff --git a/schema/conf.xsd b/schema/conf.xsd index 4cefe554fad..e2086335df2 100644 --- a/schema/conf.xsd +++ b/schema/conf.xsd @@ -8,7 +8,7 @@ + version="2.7.0"> @@ -1472,9 +1472,30 @@ - Global vector embedding model registry. When vector-field or vector:embed - omits model-path, the registry supplies path and dimension. Add models - here to use custom or additional embedding models. + Global vector embedding model registry used by the vector extension + (extensions/vector). Requires the vector extension JAR on the classpath + and the vector XQuery module enabled in <builtin-modules>. + + This registry maps short model identifiers to their resolved path and + embedding dimension. When a collection.xconf <vector-field> element + or a vector:embed() / vector:embed-batch() call omits the model-path + parameter, the registry resolves path and dimension from the matching + <model id="..."> entry here. If no entry matches, the runtime falls + back to looking for a directory named "onnx-models/{model-id}" under + $EXIST_HOME. + + Registered models are enumerable at query time via vector:models(). + + Example — register a local ONNX model and an OpenAI API endpoint: + + <vector-models enabled="yes"> + <model id="all-MiniLM-L6-v2" + path="onnx-models/all-MiniLM-L6-v2" + dimension="384"/> + <model id="text-embedding-3-small" + path="https://api.openai.com/v1" + dimension="1536"/> + </vector-models> @@ -1483,22 +1504,69 @@ - Model identifier (e.g. all-MiniLM-L6-v2) + + Short identifier for this model. Must match the value + used in collection.xconf <vector-field model="..."> + and in vector:embed($text, $model-id, ...) calls. + Typical values: "all-MiniLM-L6-v2", "multilingual-e5-large", + "text-embedding-3-small". + - Local path or HuggingFace/API URL + + Location of the embedding model. Three forms are accepted: + + 1. Relative path — resolved against $EXIST_HOME. The + directory must contain model.onnx and tokenizer.json. + Example: "onnx-models/all-MiniLM-L6-v2" + + 2. Absolute path — e.g. "/opt/models/all-MiniLM-L6-v2". + Same layout requirements as relative paths. + + 3. HTTP API base URL — for OpenAI-compatible embedding + APIs (OpenAI, Azure OpenAI, local proxies such as + Ollama). The runtime appends /embeddings to the base + URL and authenticates via the OPENAI_API_KEY + environment variable (or the apiKey parameter of + vector:embed). + Example: "https://api.openai.com/v1" + - Embedding dimension (default 384) + + Number of float32 values in the output embedding vector. + Must match the actual model output dimension; used to size + the Lucene KNN field and validate embeddings at index time. + + Defaults to 384 when omitted. + + Common values: + 384 — all-MiniLM-L6-v2, paraphrase-MiniLM-L6-v2 + 768 — multilingual-e5-base, BERT-base variants + 1024 — multilingual-e5-large + 1536 — text-embedding-3-small (OpenAI) + 3072 — text-embedding-3-large (OpenAI) + + + + + Set to "no" to disable the vector model registry without removing + it from the configuration. When disabled, vector-field and vector:embed + will not resolve model IDs via this registry; calls that rely on + registry-supplied paths or dimensions will fall back to the + $EXIST_HOME/onnx-models/{model-id} default location. + + + From cd0eac237530f498c7d84053393b5fea3c2b50d2 Mon Sep 17 00:00:00 2001 From: duncdrum Date: Sun, 5 Jul 2026 15:21:39 +0200 Subject: [PATCH 07/11] [bugfix] Suppress repeated WARN logging for unavailable vector models Co-Authored-By: Claude Sonnet 4.6 --- .../exist/vector/VectorEmbeddingService.java | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/extensions/vector/src/main/java/org/exist/vector/VectorEmbeddingService.java b/extensions/vector/src/main/java/org/exist/vector/VectorEmbeddingService.java index c41a949d428..5859773d03a 100644 --- a/extensions/vector/src/main/java/org/exist/vector/VectorEmbeddingService.java +++ b/extensions/vector/src/main/java/org/exist/vector/VectorEmbeddingService.java @@ -27,7 +27,9 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.nio.file.Path; +import java.util.Collections; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** @@ -41,6 +43,7 @@ public final class VectorEmbeddingService { private static volatile VectorEmbeddingService instance; private final Map cache = new ConcurrentHashMap<>(); + private final Set failed = Collections.newSetFromMap(new ConcurrentHashMap<>()); private VectorEmbeddingService() { } @@ -121,8 +124,13 @@ public VectorEmbeddingProvider getProvider(@Nonnull final String modelId, } final Path modelPath = ModelPathResolver.resolve(modelId, path); if (modelPath == null) { - LOG.warn("Model '{}' could not be resolved (path='{}'). Ensure the directory exists under exist.home and contains model.onnx and tokenizer.json, or configure in conf.xml, or use an absolute path or URL.", - modelId, path); + final String unresolvedKey = "unresolved:" + modelId + ":" + path; + if (failed.add(unresolvedKey)) { + LOG.warn("Model '{}' could not be resolved (path='{}'). Ensure the directory exists under exist.home and contains model.onnx and tokenizer.json, or configure in conf.xml, or use an absolute path or URL.", + modelId, path); + } else { + LOG.debug("Model '{}' still not available at '{}'", modelId, path); + } return null; } return getProviderByPath(modelId, modelPath, dim); @@ -141,18 +149,24 @@ public VectorEmbeddingProvider getProviderByPath(@Nonnull final String modelId, @Nonnull final Path modelPath, final int dimension) { final String cacheKey = modelId + ":" + modelPath.toAbsolutePath(); - return cache.computeIfAbsent(cacheKey, k -> { - try { - final VectorEmbeddingProvider p = OnnxVectorProvider.create(modelPath, dimension); - if (p != null) { - LOG.info("Loaded ONNX embedding model: {} from {}", modelId, modelPath); - return p; - } - } catch (final Exception e) { + final VectorEmbeddingProvider cached = cache.get(cacheKey); + if (cached != null) return cached; + if (failed.contains(cacheKey)) return null; + try { + final VectorEmbeddingProvider p = OnnxVectorProvider.create(modelPath, dimension); + if (p != null) { + LOG.info("Loaded ONNX embedding model: {} from {}", modelId, modelPath); + cache.put(cacheKey, p); + return p; + } + } catch (final Exception e) { + if (failed.add(cacheKey)) { LOG.warn("Failed to load ONNX model {} from {}: {}", modelId, modelPath, e.getMessage()); } return null; - }); + } + failed.add(cacheKey); + return null; } @Nonnull @@ -169,6 +183,7 @@ private static String httpCacheKey(@Nonnull final String modelId, @Nonnull final public void evict(@Nonnull final String modelId, @Nonnull final Path modelPath) { final String cacheKey = modelId + ":" + modelPath.toAbsolutePath(); cache.remove(cacheKey); + failed.remove(cacheKey); } /** From 2cda6d20460479be79327a05eda901504a92e081 Mon Sep 17 00:00:00 2001 From: duncdrum Date: Sun, 5 Jul 2026 15:41:55 +0200 Subject: [PATCH 08/11] [feature] Add IndexFactory SPI; wire all bundled index modules; trim canonical conf.xml Co-Authored-By: Claude Sonnet 4.6 --- .../java/org/exist/indexing/IndexFactory.java | 42 +++++++++++++++++++ .../java/org/exist/util/Configuration.java | 22 +++++++++- exist-distribution/src/main/config/conf.xml | 27 +++++------- .../exist/indexing/lucene/LuceneIndex.java | 35 +++++++++------- .../services/org.exist.indexing.IndexFactory | 1 + .../org/exist/indexing/ngram/NGramIndex.java | 23 ++++++---- .../services/org.exist.indexing.IndexFactory | 1 + .../org/exist/indexing/range/RangeIndex.java | 5 +++ .../services/org.exist.indexing.IndexFactory | 1 + .../org/exist/indexing/sort/SortIndex.java | 5 +++ .../services/org.exist.indexing.IndexFactory | 1 + 11 files changed, 122 insertions(+), 41 deletions(-) create mode 100644 exist-core/src/main/java/org/exist/indexing/IndexFactory.java create mode 100644 extensions/indexes/lucene/src/main/resources/META-INF/services/org.exist.indexing.IndexFactory create mode 100644 extensions/indexes/ngram/src/main/resources/META-INF/services/org.exist.indexing.IndexFactory create mode 100644 extensions/indexes/range/src/main/resources/META-INF/services/org.exist.indexing.IndexFactory create mode 100644 extensions/indexes/sort/src/main/resources/META-INF/services/org.exist.indexing.IndexFactory diff --git a/exist-core/src/main/java/org/exist/indexing/IndexFactory.java b/exist-core/src/main/java/org/exist/indexing/IndexFactory.java new file mode 100644 index 00000000000..283ba4ab476 --- /dev/null +++ b/exist-core/src/main/java/org/exist/indexing/IndexFactory.java @@ -0,0 +1,42 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.indexing; + +/** + * SPI for index module auto-discovery via {@code ServiceLoader}. + * + *

Register implementations in + * {@code META-INF/services/org.exist.indexing.IndexFactory}. + * A conf.xml {@code } entry with the same id takes precedence; + * an entry with {@code enabled="no"} suppresses the SPI-registered module.

+ */ +public interface IndexFactory { + + /** + * The default conf.xml {@code id} for this index (e.g. {@code "lucene-index"}). + * Used as the key in the index registry and to match conf.xml override entries. + */ + String getDefaultId(); + + /** The concrete {@link AbstractIndex} subclass to instantiate. */ + Class getIndexClass(); +} diff --git a/exist-core/src/main/java/org/exist/util/Configuration.java b/exist-core/src/main/java/org/exist/util/Configuration.java index eacd7e2bda4..2376b0d4ca9 100644 --- a/exist-core/src/main/java/org/exist/util/Configuration.java +++ b/exist-core/src/main/java/org/exist/util/Configuration.java @@ -51,6 +51,7 @@ import org.xml.sax.XMLReader; import org.exist.Indexer; +import org.exist.indexing.IndexFactory; import org.exist.indexing.IndexManager; import org.exist.dom.memtree.SAXAdapter; import org.exist.scheduler.JobConfig; @@ -77,10 +78,12 @@ import java.util.Locale; import java.util.HashMap; import java.util.Map; +import java.util.HashSet; import java.util.Map.Entry; import java.util.Optional; import java.util.Properties; import java.util.ServiceLoader; +import java.util.Set; import java.util.function.Function; import javax.annotation.Nullable; @@ -1228,18 +1231,21 @@ private void configureIndexer(final Document doc, final Element indexer) throws } final NodeList module = ((Element) modules.item(0)).getElementsByTagName(IndexManager.CONFIGURATION_MODULE_ELEMENT_NAME); final List modConfigList = new ArrayList<>(); + final Set configuredIds = new HashSet<>(); + final Set disabledIds = new HashSet<>(); for (int i = 0; i < module.getLength(); i++) { final Element elem = (Element) module.item(i); + final String id = elem.getAttribute(IndexManager.INDEXER_MODULES_ID_ATTRIBUTE); // enabled="no" disables the index module without removing it from conf.xml if ("no".equalsIgnoreCase(elem.getAttribute("enabled"))) { - LOG.debug("Index module '{}' is disabled via enabled=\"no\", skipping", elem.getAttribute(IndexManager.INDEXER_MODULES_ID_ATTRIBUTE)); + LOG.debug("Index module '{}' is disabled via enabled=\"no\", skipping", id); + disabledIds.add(id); continue; } final String className = elem.getAttribute(IndexManager.INDEXER_MODULES_CLASS_ATTRIBUTE); - final String id = elem.getAttribute(IndexManager.INDEXER_MODULES_ID_ATTRIBUTE); if (className.isEmpty()) { throw (new DatabaseConfigurationException("Required attribute class is missing for module")); @@ -1249,8 +1255,20 @@ private void configureIndexer(final Document doc, final Element indexer) throws throw (new DatabaseConfigurationException("Required attribute id is missing for module")); } + configuredIds.add(id); modConfigList.add(new IndexModuleConfig(id, className, elem)); } + + // SPI: auto-discover index modules whose id is not explicitly listed in conf.xml + for (final IndexFactory factory : ServiceLoader.load(IndexFactory.class, Configuration.class.getClassLoader())) { + final String id = factory.getDefaultId(); + if (configuredIds.contains(id) || disabledIds.contains(id)) { + continue; + } + LOG.debug("SPI-registered index module: {} ({})", id, factory.getIndexClass().getName()); + modConfigList.add(new IndexModuleConfig(id, factory.getIndexClass().getName(), null)); + } + setProperty(IndexManager.PROPERTY_INDEXER_MODULES, modConfigList.toArray(new IndexModuleConfig[0])); } diff --git a/exist-distribution/src/main/config/conf.xml b/exist-distribution/src/main/config/conf.xml index 162b6a8d8d7..54e3f1a5b06 100644 --- a/exist-distribution/src/main/config/conf.xml +++ b/exist-distribution/src/main/config/conf.xml @@ -545,29 +545,22 @@ suppress-whitespace="none"> - - - - - - - + - - + + + diff --git a/extensions/indexes/lucene/src/main/java/org/exist/indexing/lucene/LuceneIndex.java b/extensions/indexes/lucene/src/main/java/org/exist/indexing/lucene/LuceneIndex.java index 21d7370c16a..da97a7cb66b 100644 --- a/extensions/indexes/lucene/src/main/java/org/exist/indexing/lucene/LuceneIndex.java +++ b/extensions/indexes/lucene/src/main/java/org/exist/indexing/lucene/LuceneIndex.java @@ -91,21 +91,23 @@ public void configure(BrokerPool pool, Path dataDir, Element config) throws Data if (LOG.isDebugEnabled()) LOG.debug("Configuring Lucene index"); - String bufferSizeParam = config.getAttribute("buffer"); - if (!bufferSizeParam.isEmpty()) - try { - bufferSize = Double.parseDouble(bufferSizeParam); - } catch (NumberFormatException e) { - LOG.warn("Invalid buffer size setting for Lucene index: {}", bufferSizeParam, e); + if (config != null) { + String bufferSizeParam = config.getAttribute("buffer"); + if (!bufferSizeParam.isEmpty()) + try { + bufferSize = Double.parseDouble(bufferSizeParam); + } catch (NumberFormatException e) { + LOG.warn("Invalid buffer size setting for Lucene index: {}", bufferSizeParam, e); + } + + if (LOG.isDebugEnabled()) + LOG.debug("Using buffer size: {}", bufferSize); + + NodeList nl = config.getElementsByTagName("analyzer"); + if (nl.getLength() > 0) { + Element node = (Element) nl.item(0); + defaultAnalyzer = AnalyzerConfig.configureAnalyzer(node); } - - if (LOG.isDebugEnabled()) - LOG.debug("Using buffer size: {}", bufferSize); - - NodeList nl = config.getElementsByTagName("analyzer"); - if (nl.getLength() > 0) { - Element node = (Element) nl.item(0); - defaultAnalyzer = AnalyzerConfig.configureAnalyzer(node); } if (defaultAnalyzer == null) @@ -114,6 +116,11 @@ public void configure(BrokerPool pool, Path dataDir, Element config) throws Data LOG.debug("Using default analyzer: {}", defaultAnalyzer.getClass().getName()); } + public static final class Factory implements org.exist.indexing.IndexFactory { + @Override public String getDefaultId() { return "lucene-index"; } + @Override public Class getIndexClass() { return LuceneIndex.class; } + } + @Override public void open() throws DatabaseConfigurationException { Path dir = getDataDir().resolve(getDirName()); diff --git a/extensions/indexes/lucene/src/main/resources/META-INF/services/org.exist.indexing.IndexFactory b/extensions/indexes/lucene/src/main/resources/META-INF/services/org.exist.indexing.IndexFactory new file mode 100644 index 00000000000..9cbb03d790c --- /dev/null +++ b/extensions/indexes/lucene/src/main/resources/META-INF/services/org.exist.indexing.IndexFactory @@ -0,0 +1 @@ +org.exist.indexing.lucene.LuceneIndex$Factory diff --git a/extensions/indexes/ngram/src/main/java/org/exist/indexing/ngram/NGramIndex.java b/extensions/indexes/ngram/src/main/java/org/exist/indexing/ngram/NGramIndex.java index 9f581656b93..74fa66e105c 100644 --- a/extensions/indexes/ngram/src/main/java/org/exist/indexing/ngram/NGramIndex.java +++ b/extensions/indexes/ngram/src/main/java/org/exist/indexing/ngram/NGramIndex.java @@ -58,17 +58,24 @@ public class NGramIndex extends AbstractIndex implements RawBackupSupport { public void configure(BrokerPool pool, Path dataDir, Element config) throws DatabaseConfigurationException { super.configure(pool, dataDir, config); String fileName = "ngram.dbx"; - if (config.hasAttribute("file")) - fileName = config.getAttribute("file"); - if (config.hasAttribute("n")) - try { - gramSize = Integer.parseInt(config.getAttribute("n")); - } catch (NumberFormatException e) { - throw new DatabaseConfigurationException("Configuration parameter 'n' should be an integer."); - } + if (config != null) { + if (config.hasAttribute("file")) + fileName = config.getAttribute("file"); + if (config.hasAttribute("n")) + try { + gramSize = Integer.parseInt(config.getAttribute("n")); + } catch (NumberFormatException e) { + throw new DatabaseConfigurationException("Configuration parameter 'n' should be an integer."); + } + } dataFile = dataDir.resolve(fileName); } + public static final class Factory implements org.exist.indexing.IndexFactory { + @Override public String getDefaultId() { return "ngram-index"; } + @Override public Class getIndexClass() { return NGramIndex.class; } + } + @Override public void open() throws DatabaseConfigurationException { try { diff --git a/extensions/indexes/ngram/src/main/resources/META-INF/services/org.exist.indexing.IndexFactory b/extensions/indexes/ngram/src/main/resources/META-INF/services/org.exist.indexing.IndexFactory new file mode 100644 index 00000000000..10fef024311 --- /dev/null +++ b/extensions/indexes/ngram/src/main/resources/META-INF/services/org.exist.indexing.IndexFactory @@ -0,0 +1 @@ +org.exist.indexing.ngram.NGramIndex$Factory diff --git a/extensions/indexes/range/src/main/java/org/exist/indexing/range/RangeIndex.java b/extensions/indexes/range/src/main/java/org/exist/indexing/range/RangeIndex.java index d2908750249..029154cc628 100644 --- a/extensions/indexes/range/src/main/java/org/exist/indexing/range/RangeIndex.java +++ b/extensions/indexes/range/src/main/java/org/exist/indexing/range/RangeIndex.java @@ -43,6 +43,11 @@ public class RangeIndex extends LuceneIndex { public final static String ID = RangeIndex.class.getName(); + public static final class Factory implements org.exist.indexing.IndexFactory { + @Override public String getDefaultId() { return "range-index"; } + @Override public Class getIndexClass() { return RangeIndex.class; } + } + /** * Enumeration of supported operators and optimized functions. */ diff --git a/extensions/indexes/range/src/main/resources/META-INF/services/org.exist.indexing.IndexFactory b/extensions/indexes/range/src/main/resources/META-INF/services/org.exist.indexing.IndexFactory new file mode 100644 index 00000000000..0e4c0c33aa1 --- /dev/null +++ b/extensions/indexes/range/src/main/resources/META-INF/services/org.exist.indexing.IndexFactory @@ -0,0 +1 @@ +org.exist.indexing.range.RangeIndex$Factory diff --git a/extensions/indexes/sort/src/main/java/org/exist/indexing/sort/SortIndex.java b/extensions/indexes/sort/src/main/java/org/exist/indexing/sort/SortIndex.java index 4ce3117eb74..f427f2337f1 100644 --- a/extensions/indexes/sort/src/main/java/org/exist/indexing/sort/SortIndex.java +++ b/extensions/indexes/sort/src/main/java/org/exist/indexing/sort/SortIndex.java @@ -61,6 +61,11 @@ public class SortIndex extends AbstractIndex implements RawBackupSupport { protected static final Logger LOG = LogManager.getLogger(SortIndex.class); protected BTreeStore btree; + public static final class Factory implements org.exist.indexing.IndexFactory { + @Override public String getDefaultId() { return "sort-index"; } + @Override public Class getIndexClass() { return SortIndex.class; } + } + @Override public void open() throws DatabaseConfigurationException { final Path file = getDataDir().resolve(FILE_NAME); diff --git a/extensions/indexes/sort/src/main/resources/META-INF/services/org.exist.indexing.IndexFactory b/extensions/indexes/sort/src/main/resources/META-INF/services/org.exist.indexing.IndexFactory new file mode 100644 index 00000000000..eaa663be1e3 --- /dev/null +++ b/extensions/indexes/sort/src/main/resources/META-INF/services/org.exist.indexing.IndexFactory @@ -0,0 +1 @@ +org.exist.indexing.sort.SortIndex$Factory From 9e914d91fc95ed05126fd0c7e895c84703bc2278 Mon Sep 17 00:00:00 2001 From: duncdrum Date: Sun, 5 Jul 2026 16:51:13 +0200 Subject: [PATCH 09/11] [bugfix] Set index name from config id when SPI registers without conf.xml element AbstractIndex.configure() only sets name from config.getAttribute("id") when config != null. IndexFactory SPI registration passes config=null, leaving name null. IndexController.getWorkerByIndexName() matches on name, so SPI-registered indexes (range-index, ngram-index, sort-index, lucene-index) were never found, causing NPE at every range:index-keys-for-field() call. Fix: after configure(), if name is still null, call setName(id) with the configuration id that was used to key the indexers map. Co-Authored-By: Claude Sonnet 4.6 --- .../src/main/java/org/exist/indexing/IndexManager.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/exist-core/src/main/java/org/exist/indexing/IndexManager.java b/exist-core/src/main/java/org/exist/indexing/IndexManager.java index 786af20cd21..d25e8d2a3db 100644 --- a/exist-core/src/main/java/org/exist/indexing/IndexManager.java +++ b/exist-core/src/main/java/org/exist/indexing/IndexManager.java @@ -124,10 +124,7 @@ public void prepare(final BrokerPool brokerPool) throws BrokerPoolServiceExcepti // check if a structural index was configured. If not, create one based on default settings. AbstractIndex structural = (AbstractIndex) indexers.get(StructuralIndex.STRUCTURAL_INDEX_ID); if (structural == null) { - structural = initIndex(pool, StructuralIndex.STRUCTURAL_INDEX_ID, null, dataDir, StructuralIndex.DEFAULT_CLASS); - if (structural != null) { - structural.setName(StructuralIndex.STRUCTURAL_INDEX_ID); - } + initIndex(pool, StructuralIndex.STRUCTURAL_INDEX_ID, null, dataDir, StructuralIndex.DEFAULT_CLASS); } } catch(final DatabaseConfigurationException e) { throw new BrokerPoolServiceException(e); @@ -145,6 +142,9 @@ private AbstractIndex initIndex(final BrokerPool pool, final String id, final El } final AbstractIndex index = (AbstractIndex) clazz.newInstance(); index.configure(pool, dataDir, config); + if (index.getIndexName() == null) { + index.setName(id); + } index.open(); indexers.put(id, index); if (LOG.isInfoEnabled()) { From e52b754706dadec61a758f7fe5c25f76526a93b1 Mon Sep 17 00:00:00 2001 From: duncdrum Date: Mon, 6 Jul 2026 21:07:51 +0200 Subject: [PATCH 10/11] [ignore] Drop redundant FQNs in Factory inner classes; use simple names covered by existing imports Co-Authored-By: Claude Sonnet 4.6 --- exist-core/src/main/java/org/exist/util/Configuration.java | 2 +- .../org/exist/xquery/functions/request/RequestModule.java | 5 +++-- .../org/exist/xquery/functions/response/ResponseModule.java | 5 +++-- .../org/exist/xquery/functions/session/SessionModule.java | 5 +++-- .../java/org/exist/xquery/functions/util/UtilModule.java | 5 +++-- .../src/main/java/org/exist/indexing/sort/SortIndex.java | 2 +- .../java/org/exist/xquery/modules/cache/CacheModule.java | 5 +++-- .../exist/xquery/modules/expathrepo/ExpathPackageModule.java | 5 +++-- .../main/java/org/exist/xquery/modules/sql/SQLModule.java | 5 +++-- .../java/org/exist/xquery/modules/xmldiff/XmlDiffModule.java | 5 +++-- 10 files changed, 26 insertions(+), 18 deletions(-) diff --git a/exist-core/src/main/java/org/exist/util/Configuration.java b/exist-core/src/main/java/org/exist/util/Configuration.java index 2376b0d4ca9..c410a977e77 100644 --- a/exist-core/src/main/java/org/exist/util/Configuration.java +++ b/exist-core/src/main/java/org/exist/util/Configuration.java @@ -1388,7 +1388,7 @@ private void configureVectorModels(final Element vectorModels) { if ("no".equalsIgnoreCase(vectorModels.getAttribute("enabled"))) { return; } - final org.w3c.dom.NodeList models = vectorModels.getElementsByTagName("model"); + final NodeList models = vectorModels.getElementsByTagName("model"); final Map entries = new HashMap<>(); for (int i = 0; i < models.getLength(); i++) { final Element model = (Element) models.item(i); diff --git a/exist-core/src/main/java/org/exist/xquery/functions/request/RequestModule.java b/exist-core/src/main/java/org/exist/xquery/functions/request/RequestModule.java index a522a639a9b..28b614899ae 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/request/RequestModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/request/RequestModule.java @@ -27,6 +27,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.exist.xquery.*; +import org.exist.xquery.Module; /** * Module function definitions for transform module. @@ -104,8 +105,8 @@ public String getReleaseVersion() { return RELEASED_IN_VERSION; } - public static final class Factory implements org.exist.xquery.ModuleFactory { + public static final class Factory implements ModuleFactory { @Override public String getNamespaceURI() { return NAMESPACE_URI; } - @Override public Class getModuleClass() { return RequestModule.class; } + @Override public Class getModuleClass() { return RequestModule.class; } } } diff --git a/exist-core/src/main/java/org/exist/xquery/functions/response/ResponseModule.java b/exist-core/src/main/java/org/exist/xquery/functions/response/ResponseModule.java index 18359f38957..fc37866db39 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/response/ResponseModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/response/ResponseModule.java @@ -27,6 +27,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.exist.xquery.*; +import org.exist.xquery.Module; /** * Module function definitions for xmldb module. @@ -83,8 +84,8 @@ public String getReleaseVersion() { return (RELEASED_IN_VERSION); } - public static final class Factory implements org.exist.xquery.ModuleFactory { + public static final class Factory implements ModuleFactory { @Override public String getNamespaceURI() { return NAMESPACE_URI; } - @Override public Class getModuleClass() { return ResponseModule.class; } + @Override public Class getModuleClass() { return ResponseModule.class; } } } diff --git a/exist-core/src/main/java/org/exist/xquery/functions/session/SessionModule.java b/exist-core/src/main/java/org/exist/xquery/functions/session/SessionModule.java index 9175750c08b..d857bc56fe9 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/session/SessionModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/session/SessionModule.java @@ -27,6 +27,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.exist.xquery.*; +import org.exist.xquery.Module; /** * Module function definitions for transform module. @@ -87,8 +88,8 @@ public String getReleaseVersion() { return RELEASED_IN_VERSION; } - public static final class Factory implements org.exist.xquery.ModuleFactory { + public static final class Factory implements ModuleFactory { @Override public String getNamespaceURI() { return NAMESPACE_URI; } - @Override public Class getModuleClass() { return SessionModule.class; } + @Override public Class getModuleClass() { return SessionModule.class; } } } diff --git a/exist-core/src/main/java/org/exist/xquery/functions/util/UtilModule.java b/exist-core/src/main/java/org/exist/xquery/functions/util/UtilModule.java index 8a71d3060c3..07b8f65e7df 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/util/UtilModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/util/UtilModule.java @@ -26,6 +26,7 @@ import org.exist.dom.QName; import org.exist.xquery.*; +import org.exist.xquery.Module; import org.exist.xquery.value.FunctionParameterSequenceType; import org.exist.xquery.value.FunctionReturnSequenceType; @@ -220,9 +221,9 @@ static FunctionSignature[] functionSignatures(final String name, final String de return FunctionDSL.functionSignatures(new QName(name, NAMESPACE_URI, PREFIX), description, returnType, variableParamTypes); } - public static final class Factory implements org.exist.xquery.ModuleFactory { + public static final class Factory implements ModuleFactory { @Override public String getNamespaceURI() { return NAMESPACE_URI; } - @Override public Class getModuleClass() { return UtilModule.class; } + @Override public Class getModuleClass() { return UtilModule.class; } } } diff --git a/extensions/indexes/sort/src/main/java/org/exist/indexing/sort/SortIndex.java b/extensions/indexes/sort/src/main/java/org/exist/indexing/sort/SortIndex.java index f427f2337f1..cf2e17f8e8b 100644 --- a/extensions/indexes/sort/src/main/java/org/exist/indexing/sort/SortIndex.java +++ b/extensions/indexes/sort/src/main/java/org/exist/indexing/sort/SortIndex.java @@ -63,7 +63,7 @@ public class SortIndex extends AbstractIndex implements RawBackupSupport { public static final class Factory implements org.exist.indexing.IndexFactory { @Override public String getDefaultId() { return "sort-index"; } - @Override public Class getIndexClass() { return SortIndex.class; } + @Override public Class getIndexClass() { return SortIndex.class; } } @Override diff --git a/extensions/modules/cache/src/main/java/org/exist/xquery/modules/cache/CacheModule.java b/extensions/modules/cache/src/main/java/org/exist/xquery/modules/cache/CacheModule.java index 6e12fa530ca..c0d321ec4b2 100644 --- a/extensions/modules/cache/src/main/java/org/exist/xquery/modules/cache/CacheModule.java +++ b/extensions/modules/cache/src/main/java/org/exist/xquery/modules/cache/CacheModule.java @@ -30,6 +30,7 @@ import org.apache.logging.log4j.Logger; import org.exist.dom.QName; import org.exist.xquery.*; +import org.exist.xquery.Module; import org.exist.xquery.value.FunctionParameterSequenceType; import org.exist.xquery.value.FunctionReturnSequenceType; @@ -195,8 +196,8 @@ Optional getLazyCacheConfig() { return lazyCacheConfig; } - public static final class Factory implements org.exist.xquery.ModuleFactory { + public static final class Factory implements ModuleFactory { @Override public String getNamespaceURI() { return NAMESPACE_URI; } - @Override public Class getModuleClass() { return CacheModule.class; } + @Override public Class getModuleClass() { return CacheModule.class; } } } diff --git a/extensions/modules/expathrepo/src/main/java/org/exist/xquery/modules/expathrepo/ExpathPackageModule.java b/extensions/modules/expathrepo/src/main/java/org/exist/xquery/modules/expathrepo/ExpathPackageModule.java index d64aedc9f73..729c94ab955 100644 --- a/extensions/modules/expathrepo/src/main/java/org/exist/xquery/modules/expathrepo/ExpathPackageModule.java +++ b/extensions/modules/expathrepo/src/main/java/org/exist/xquery/modules/expathrepo/ExpathPackageModule.java @@ -24,6 +24,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.exist.xquery.*; +import org.exist.xquery.Module; import java.util.List; import java.util.Map; @@ -86,8 +87,8 @@ public String getReleaseVersion() { return RELEASED_IN_VERSION; } - public static final class Factory implements org.exist.xquery.ModuleFactory { + public static final class Factory implements ModuleFactory { @Override public String getNamespaceURI() { return NAMESPACE_URI; } - @Override public Class getModuleClass() { return ExpathPackageModule.class; } + @Override public Class getModuleClass() { return ExpathPackageModule.class; } } } diff --git a/extensions/modules/sql/src/main/java/org/exist/xquery/modules/sql/SQLModule.java b/extensions/modules/sql/src/main/java/org/exist/xquery/modules/sql/SQLModule.java index b8661bc5377..5f732d13c30 100644 --- a/extensions/modules/sql/src/main/java/org/exist/xquery/modules/sql/SQLModule.java +++ b/extensions/modules/sql/src/main/java/org/exist/xquery/modules/sql/SQLModule.java @@ -38,6 +38,7 @@ import org.apache.logging.log4j.Logger; import org.exist.xquery.*; +import org.exist.xquery.Module; import java.sql.Connection; import java.sql.SQLException; @@ -289,8 +290,8 @@ public void modifyEntry(final Entry entry) { }); } - public static final class Factory implements org.exist.xquery.ModuleFactory { + public static final class Factory implements ModuleFactory { @Override public String getNamespaceURI() { return NAMESPACE_URI; } - @Override public Class getModuleClass() { return SQLModule.class; } + @Override public Class getModuleClass() { return SQLModule.class; } } } diff --git a/extensions/modules/xmldiff/src/main/java/org/exist/xquery/modules/xmldiff/XmlDiffModule.java b/extensions/modules/xmldiff/src/main/java/org/exist/xquery/modules/xmldiff/XmlDiffModule.java index 247927c3ca7..65ff3a0ba27 100644 --- a/extensions/modules/xmldiff/src/main/java/org/exist/xquery/modules/xmldiff/XmlDiffModule.java +++ b/extensions/modules/xmldiff/src/main/java/org/exist/xquery/modules/xmldiff/XmlDiffModule.java @@ -37,6 +37,7 @@ import org.exist.dom.QName; import org.exist.xquery.*; +import org.exist.xquery.Module; import org.exist.xquery.value.FunctionParameterSequenceType; import org.exist.xquery.value.FunctionReturnSequenceType; @@ -98,8 +99,8 @@ private XmldDiffModuleErrorCode(final String code, final String description) { static final ErrorCodes.ErrorCode UNSUPPORTED_DOM_IMPLEMENTATION = new XmldDiffModuleErrorCode("unsupported-dom-impl", "The DOM implementation of a Node is unsupported."); - public static final class Factory implements org.exist.xquery.ModuleFactory { + public static final class Factory implements ModuleFactory { @Override public String getNamespaceURI() { return NAMESPACE_URI; } - @Override public Class getModuleClass() { return XmlDiffModule.class; } + @Override public Class getModuleClass() { return XmlDiffModule.class; } } } From c03d82458430fc5f7a1cb97b9f4a4b0cf41ad9b8 Mon Sep 17 00:00:00 2001 From: duncdrum Date: Mon, 13 Jul 2026 15:18:44 +0200 Subject: [PATCH 11/11] [bugfix] Guard against null/blank id from IndexFactory SPI factory.getDefaultId() is a third-party contract; a null or blank return would silently register the index under a useless key and leave its name unset. Validate at the Configuration.java SPI loop (primary gate) and add a belt-and-suspenders check in IndexManager.initIndex so the name is never set to null/blank. Co-Authored-By: Claude Sonnet 4.6 --- exist-core/src/main/java/org/exist/indexing/IndexManager.java | 2 +- exist-core/src/main/java/org/exist/util/Configuration.java | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/exist-core/src/main/java/org/exist/indexing/IndexManager.java b/exist-core/src/main/java/org/exist/indexing/IndexManager.java index d25e8d2a3db..ca74af5be35 100644 --- a/exist-core/src/main/java/org/exist/indexing/IndexManager.java +++ b/exist-core/src/main/java/org/exist/indexing/IndexManager.java @@ -142,7 +142,7 @@ private AbstractIndex initIndex(final BrokerPool pool, final String id, final El } final AbstractIndex index = (AbstractIndex) clazz.newInstance(); index.configure(pool, dataDir, config); - if (index.getIndexName() == null) { + if (index.getIndexName() == null && id != null && !id.isBlank()) { index.setName(id); } index.open(); diff --git a/exist-core/src/main/java/org/exist/util/Configuration.java b/exist-core/src/main/java/org/exist/util/Configuration.java index c410a977e77..84b8cb27484 100644 --- a/exist-core/src/main/java/org/exist/util/Configuration.java +++ b/exist-core/src/main/java/org/exist/util/Configuration.java @@ -1262,6 +1262,10 @@ private void configureIndexer(final Document doc, final Element indexer) throws // SPI: auto-discover index modules whose id is not explicitly listed in conf.xml for (final IndexFactory factory : ServiceLoader.load(IndexFactory.class, Configuration.class.getClassLoader())) { final String id = factory.getDefaultId(); + if (id == null || id.isBlank()) { + LOG.warn("IndexFactory {} returned a null or blank default id; skipping SPI registration", factory.getClass().getName()); + continue; + } if (configuredIds.contains(id) || disabledIds.contains(id)) { continue; }