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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<? extends org.exist.xquery.Module> getModuleClass() { return BackupModule.class; }
}
}
42 changes: 42 additions & 0 deletions exist-core/src/main/java/org/exist/indexing/IndexFactory.java
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p>Register implementations in
* {@code META-INF/services/org.exist.indexing.IndexFactory}.
* A conf.xml {@code <module>} entry with the same id takes precedence;
* an entry with {@code enabled="no"} suppresses the SPI-registered module.</p>
*/
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<? extends AbstractIndex> getIndexClass();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 && id != null && !id.isBlank()) {
index.setName(id);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

what if if==null or "" or blank?

@duncdrum duncdrum Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Handled — id != null && !id.isBlank() covers null, empty, and whitespace-only together; see the guard right above this line (21cd10a8cf, renumbered from 11d1c929d9 by today's rebase).

}
index.open();
indexers.put(id, index);
if (LOG.isInfoEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ public void configure(final Configuration configuration) throws BrokerPoolServic
if (dataDir == null) {
throw new BrokerPoolServiceException("Could not determine " + BrokerPool.PROPERTY_DATA_DIR + " from the configuration");
}
configureVectorModelRegistry(configuration);
}

private static void configureVectorModelRegistry(final Configuration configuration) {
try {
final Class<?> registryClass = Class.forName("org.exist.vector.ModelRegistry");
registryClass.getMethod("configure", Configuration.class).invoke(null, configuration);
} catch (final ClassNotFoundException e) {
LOG.debug("Vector extension not present; vector model registry not configured");
} catch (final ReflectiveOperationException e) {
LOG.warn("Failed to configure vector model registry: {}", e.getMessage(), e);
}
}

@Override
Expand Down
92 changes: 88 additions & 4 deletions exist-core/src/main/java/org/exist/util/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -50,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;
Expand All @@ -76,9 +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;
Expand Down Expand Up @@ -232,6 +237,7 @@

public class Configuration implements ErrorHandler {
public static final String BINARY_CACHE_CLASS_PROPERTY = "binary.cache.class";
public static final String PROPERTY_VECTOR_MODELS = "vector.models";
private static final String PRP_DETAILS = "{}: {}";
private static final Logger LOG = LogManager.getLogger(Configuration.class); //Logger
private static final String XQUERY_CONFIGURATION_ELEMENT_NAME = "xquery";
Expand Down Expand Up @@ -376,6 +382,8 @@ public Configuration(@Nullable String configFilename, Optional<Path> existHomeDi
configureElement(doc, XMLReaderObjectFactory.CONFIGURATION_ELEMENT_NAME, element -> configureValidation(existHomePath, element));
// RPC server
configureElement(doc, "rpc-server", this::configureRpcServer);
// Vector model registry
configureElement(doc, "vector-models", this::configureVectorModels);
} catch (final SAXException | IOException | ParserConfigurationException e) {
LOG.error("error while reading config file: {}", configFilename, e);
throw new DatabaseConfigurationException(e.getMessage(), e);
Expand Down Expand Up @@ -573,6 +581,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 -> {

Expand All @@ -591,6 +611,13 @@ private void loadModuleClasses(final Element xquery,
throw (new DatabaseConfigurationException("element 'module' requires an attribute 'uri'"));
}

// 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;
}

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
Expand Down Expand Up @@ -836,6 +863,12 @@ private void configureScheduler(final Element scheduler) {
}

private void addJobToList(final List<JobConfig> 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);

Expand Down Expand Up @@ -1107,6 +1140,12 @@ private void configureStartup(final Element startup) throws DatabaseConfiguratio
// Get <trigger> 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");

Expand Down Expand Up @@ -1191,13 +1230,23 @@ 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<IndexModuleConfig> modConfigList = new ArrayList<>();
final Set<String> configuredIds = new HashSet<>();
final Set<String> disabledIds = new HashSet<>();

for (int i = 0; i < module.getLength(); i++) {
final Element elem = (Element) module.item(i);
final String className = elem.getAttribute(IndexManager.INDEXER_MODULES_CLASS_ATTRIBUTE);
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", id);
disabledIds.add(id);
continue;
}

final String className = elem.getAttribute(IndexManager.INDEXER_MODULES_CLASS_ATTRIBUTE);

if (className.isEmpty()) {
throw (new DatabaseConfigurationException("Required attribute class is missing for module"));
}
Expand All @@ -1206,9 +1255,25 @@ 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);
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 (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;
}
LOG.debug("SPI-registered index module: {} ({})", id, factory.getIndexClass().getName());
modConfigList.add(new IndexModuleConfig(id, factory.getIndexClass().getName(), null));
}
setProperty(IndexManager.PROPERTY_INDEXER_MODULES, modConfig);

setProperty(IndexManager.PROPERTY_INDEXER_MODULES, modConfigList.toArray(new IndexModuleConfig[0]));
}

private void configureValidation(final Optional<Path> dbHome, final Element validation) {
Expand Down Expand Up @@ -1323,6 +1388,25 @@ private void configureRpcServer(final Element validation) throws DatabaseConfigu
});
}

private void configureVectorModels(final Element vectorModels) {
if ("no".equalsIgnoreCase(vectorModels.getAttribute("enabled"))) {
return;
}
final NodeList models = vectorModels.getElementsByTagName("model");
final Map<String, String[]> entries = new HashMap<>();
for (int i = 0; i < models.getLength(); i++) {
final Element model = (Element) models.item(i);
final String id = model.getAttribute("id");
final String path = model.getAttribute("path");
final String dimension = model.getAttribute("dimension");
if (id.isEmpty() || path.isEmpty()) {
continue;
}
entries.put(id.trim(), new String[]{path.trim(), dimension.trim()});
}
setProperty(PROPERTY_VECTOR_MODELS, entries);
}

/**
* Gets the value of a configuration attribute
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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".equalsIgnoreCase(param.getAttribute("enabled"))) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

"no" or "false" or both?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

"no" only — @enabled's type in conf.xsd is yes_no, an enumeration restricted to exactly yes/no (not xs:boolean), so "false" isn't a valid value to begin with. All 5 call sites (4 in Configuration.java, this one) check the same literal for that reason.

continue;
}

final String name = param.getAttribute("name");
final String value = param.getAttribute("value");

Expand Down
60 changes: 60 additions & 0 deletions exist-core/src/main/java/org/exist/xquery/ModuleFactory.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>A JAR that bundles an XQuery module can self-register by:
* <ol>
* <li>Providing an implementation of this interface (conventionally a static
* inner class named {@code Factory} on the module class).</li>
* <li>Listing the implementation's fully-qualified class name in
* {@code META-INF/services/org.exist.xquery.ModuleFactory}.</li>
* </ol>
*
* <p>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<String, List<?>> parameters}.
*
* @return module implementation class
*/
Class<? extends Module> getModuleClass();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<? extends org.exist.xquery.Module> getModuleClass() { return ArrayModule.class; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<? extends org.exist.xquery.Module> getModuleClass() { return InspectionModule.class; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<? extends org.exist.xquery.Module> getModuleClass() { return MapModule.class; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<? extends org.exist.xquery.Module> getModuleClass() { return MathModule.class; }
}
}
Loading
Loading