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

tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git


The following commit(s) were added to refs/heads/main by this push:
     new 8c8deae232 TIKA-4988: Activate catalog presets from the command line 
and environment (TIKA_PRESETS) (#3383)
8c8deae232 is described below

commit 8c8deae2322e6aab5579824b1634ba6600e8c82b
Author: Dominik Schmidt <[email protected]>
AuthorDate: Wed Oct 21 14:56:06 2026 +0200

    TIKA-4988: Activate catalog presets from the command line and environment 
(TIKA_PRESETS) (#3383)
    
    * TIKA-4988: Activate catalog presets from the command line and environment
    
    tika-server takes --preset <name> (repeatable) and TIKA_PRESETS (comma
    separated); both add "<name>": true to the config's presets before the
    front end and the forks load it. The config file's own entry wins.
    
    * TIKA-4988: --preset and TIKA_PRESETS override the config file's entry
    
    * TIKA-4988: review follow-ups
    
    Log what --preset and TIKA_PRESETS replace in the config file, hand
    /async the config with the presets folded in, and refuse a presets
    value that is not an object instead of overwriting it.
    
    ---------
    
    Co-authored-by: Tim Allison <[email protected]>
---
 CHANGES.txt                                        |  5 ++
 docs/modules/ROOT/pages/using-tika/docker.adoc     | 11 +++
 .../ROOT/pages/using-tika/server/index.adoc        | 25 +++++--
 .../tika/pipes/core/config/ConfigMerger.java       | 11 +++
 .../tika/pipes/core/config/ConfigOverrides.java    | 17 +++++
 .../tika/pipes/core/config/ConfigMergerTest.java   | 37 ++++++++++
 .../org/apache/tika/server/core/TikaServerCli.java |  3 +
 .../apache/tika/server/core/TikaServerConfig.java  | 75 ++++++++++++++++++-
 .../apache/tika/server/core/TikaServerProcess.java | 22 +++++-
 .../tika/server/core/IntegrationTestBase.java      | 12 ++++
 .../core/PresetActivationIntegrationTest.java      | 83 ++++++++++++++++++++++
 .../tika/server/core/TikaServerConfigTest.java     | 39 ++++++++++
 .../META-INF/tika/preset-catalog.properties        | 19 +++++
 .../test-presets/catalog-xml-content.json          |  3 +
 14 files changed, 354 insertions(+), 8 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index ef14a82210..819f1a9bcd 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,10 @@
 Release 4.2.0 - unreleased
 
+   * tika-server activates catalog presets from the command line (--preset 
<name>, repeatable)
+     and the environment (TIKA_PRESETS, comma-separated), the equivalent of 
"<name>": true in
+     the config's presets, overriding the file's own entry for the name; a 
config file needs
+     no longer be mounted for that (TIKA-4988).
+
   BREAKING CHANGES AND DEPRECATIONS
    * Extract stored line and paragraph counts from legacy Microsoft Word
      (.doc) documents. The existing metadata properties are now populated
diff --git a/docs/modules/ROOT/pages/using-tika/docker.adoc 
b/docs/modules/ROOT/pages/using-tika/docker.adoc
index efb07e6162..02e255da57 100644
--- a/docs/modules/ROOT/pages/using-tika/docker.adoc
+++ b/docs/modules/ROOT/pages/using-tika/docker.adoc
@@ -105,6 +105,17 @@ For example, to keep the full image's parsers but turn OCR 
off:
 
 See xref:configuration/index.adoc[Configuration] for the file format.
 
+A catalog preset needs no file: name it in `TIKA_PRESETS` (comma-separated) or 
with `--preset`,
+and its route is live:
+
+[source,bash]
+----
+docker run -d -p 127.0.0.1:9998:9998 -e TIKA_PRESETS=thumbnail 
apache/tika:X.Y.Z-full
+curl -T document.pdf http://localhost:9998/unpack/preset/thumbnail
+----
+
+See 
xref:using-tika/server/index.adoc#_presets_named_configuration_without_config[Presets].
+
 == Extra jars
 
 Mount a directory of jars at `/tika-extras` — it is already on the container's 
classpath, and the
diff --git a/docs/modules/ROOT/pages/using-tika/server/index.adoc 
b/docs/modules/ROOT/pages/using-tika/server/index.adoc
index cd073a78c0..7930aa5a01 100644
--- a/docs/modules/ROOT/pages/using-tika/server/index.adoc
+++ b/docs/modules/ROOT/pages/using-tika/server/index.adoc
@@ -129,8 +129,8 @@ Nothing is active unless the top-level `presets` key names 
it. `true` activates
 the classpath *catalog* (definitions shipped with Tika, so their content 
tracks the Tika
 version); an object defines a preset in place, replacing any same-named 
catalog definition
 wholesale; `false`/`null` is an explicit no-op. Catalog jars can never 
activate themselves —
-every active preset is a visible line in the operator's config, and `true` 
naming nothing in
-the catalog fails startup:
+every active preset is a visible line in the operator's config, command line 
or environment,
+and `true` naming nothing in the catalog fails startup:
 
 [source,json]
 ----
@@ -143,6 +143,18 @@ the catalog fails startup:
 }
 ----
 
+A catalog preset can also be activated without touching the config file: 
`--preset <name>`
+(repeatable) or the environment variable `TIKA_PRESETS` with a comma-separated 
list. Either is
+the same as `"<name>": true` in `presets`, so it needs no `-c` at all, and the 
catalog must know
+the name. Both win over the config file: a name listed there is active with 
the catalog's
+content even if the file sets it to `false` or defines it inline.
+
+[source,bash]
+----
+java -jar tika-server-standard-X.Y.Z.jar --preset thumbnail --preset 
render-thumbnail
+TIKA_PRESETS=thumbnail,render-thumbnail java -jar 
tika-server-standard-X.Y.Z.jar
+----
+
 Presets are deliberately narrow: a request selects exactly one, the preset 
routes take no
 `config` part, and a preset never combines with request-supplied configuration 
— a variant is
 another preset. Because preset content is operator- or Tika-vetted, the preset 
routes do *not*
@@ -361,12 +373,17 @@ The server starts on `localhost:9998` by default.
 |Enable Prometheus metrics on this port. See
 xref:using-tika/server/monitoring.adoc[Monitoring with Prometheus].
 
+|`--preset <name>`
+|Activate a catalog preset, as `"presets": {"<name>": true}` in the config 
would. Repeatable.
+The environment variable `TIKA_PRESETS` takes a comma-separated list. See 
<<_presets_named_configuration_without_config,Presets>>.
+
 |`-?`, `--help`
 |Print the usage message.
 |===
 
-NOTE: `-h`, `-p`, `-i` and `--metricsPort` override the JSON config. 
Everything else — `allowPipes`,
-`allowPerRequestConfig`, CORS, TLS, timeouts — is JSON-only.
+NOTE: `-h`, `-p`, `-i` and `--metricsPort` override the JSON config; 
`--preset` and `TIKA_PRESETS`
+add to its `presets`. Everything else — `allowPipes`, `allowPerRequestConfig`, 
CORS, TLS,
+timeouts — is JSON-only.
 
 == Endpoints
 
diff --git 
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/ConfigMerger.java
 
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/ConfigMerger.java
index 7432943a42..7f6b00bf83 100644
--- 
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/ConfigMerger.java
+++ 
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/ConfigMerger.java
@@ -230,6 +230,17 @@ public class ConfigMerger {
             getOrCreateObject(mapper, root, "pipes").put("trustedRequests", 
true);
         }
 
+        if (!overrides.getPresets().isEmpty()) {
+            if (root.has("presets") && !root.get("presets").isObject()) {
+                throw new IOException("'presets' must be an object of preset 
definitions"
+                        + (existingConfig == null ? "" : " in " + 
existingConfig));
+            }
+            ObjectNode presetsNode = getOrCreateObject(mapper, root, 
"presets");
+            for (String preset : overrides.getPresets()) {
+                presetsNode.put(preset, true);
+            }
+        }
+
         // Apply emit strategy
         if (overrides.getEmitStrategy() != null) {
             ObjectNode pipesNode = getOrCreateObject(mapper, root, "pipes");
diff --git 
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/ConfigOverrides.java
 
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/ConfigOverrides.java
index bb2c88f13c..3ecb005ad4 100644
--- 
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/ConfigOverrides.java
+++ 
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/ConfigOverrides.java
@@ -56,6 +56,7 @@ public class ConfigOverrides {
     private final Integer writeLimit;
     private final boolean classpathPlugins;
     private final boolean trustedRequests;
+    private final List<String> presets;
 
     private ConfigOverrides(Builder builder) {
         this.fetchers = Collections.unmodifiableList(new 
ArrayList<>(builder.fetchers));
@@ -68,6 +69,7 @@ public class ConfigOverrides {
         this.writeLimit = builder.writeLimit;
         this.classpathPlugins = builder.classpathPlugins;
         this.trustedRequests = builder.trustedRequests;
+        this.presets = Collections.unmodifiableList(new 
ArrayList<>(builder.presets));
     }
 
     public static Builder builder() {
@@ -116,6 +118,11 @@ public class ConfigOverrides {
         return trustedRequests;
     }
 
+    /** Catalog presets to activate, as {@code "name": true} would; see 
ConfigMerger. */
+    public List<String> getPresets() {
+        return presets;
+    }
+
     /**
      * Represents a fetcher configuration override.
      */
@@ -238,10 +245,20 @@ public class ConfigOverrides {
         private Integer writeLimit;
         private boolean classpathPlugins;
         private boolean trustedRequests;
+        private final List<String> presets = new ArrayList<>();
 
         private Builder() {
         }
 
+        /**
+         * Activates a catalog preset, as {@code "presets": {"name": true}} in 
the config would,
+         * replacing whatever value the existing config has for the name.
+         */
+        public Builder addPreset(String name) {
+            this.presets.add(name);
+            return this;
+        }
+
         /**
          * Sets {@code pipes.trustedRequests}: the requests come from the 
operator, so their
          * parseContext is trusted and their limits and timeouts are used as 
given.
diff --git 
a/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/config/ConfigMergerTest.java
 
b/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/config/ConfigMergerTest.java
index 98ed690781..b0a4f9f1d8 100644
--- 
a/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/config/ConfigMergerTest.java
+++ 
b/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/config/ConfigMergerTest.java
@@ -107,6 +107,43 @@ public class ConfigMergerTest {
         assertEquals(50, 
parseContext.get("output-limits").get("maxXmlDepth").asInt());
     }
 
+    @Test
+    public void testPresetsOverrideTheConfigsOwnEntries() throws IOException {
+        Path existing = tempDir.resolve("existing.json");
+        Files.writeString(existing, "{\"presets\": {\"off\": false, 
\"inline\": {\"pages\": {}}}}");
+        ConfigOverrides overrides = ConfigOverrides.builder()
+                .addPreset("thumbnail")
+                .addPreset("off")
+                .addPreset("inline")
+                .build();
+
+        JsonNode presets = new ObjectMapper()
+                .readTree(ConfigMerger.merge(existing, overrides).json())
+                .get("presets");
+
+        assertTrue(presets.get("thumbnail").asBoolean());
+        assertTrue(presets.get("off").asBoolean());
+        assertTrue(presets.get("inline").asBoolean());
+        assertEquals(3, presets.size());
+    }
+
+    @Test
+    public void testPresetsRefuseAMalformedPresetsValue() throws IOException {
+        Path existing = tempDir.resolve("existing.json");
+        Files.writeString(existing, "{\"presets\": \"thumbnail\"}");
+        ConfigOverrides overrides = 
ConfigOverrides.builder().addPreset("thumbnail").build();
+        assertThrows(IOException.class, () -> ConfigMerger.merge(existing, 
overrides));
+        // without presets to fold in, the value is left for the loader to 
refuse
+        assertNotNull(ConfigMerger.merge(existing, 
ConfigOverrides.builder().build()));
+    }
+
+    @Test
+    public void testPresetsWithoutExistingConfig() throws IOException {
+        ConfigOverrides overrides = 
ConfigOverrides.builder().addPreset("thumbnail").build();
+        JsonNode root = new ObjectMapper().readTree(ConfigMerger.merge(null, 
overrides).json());
+        assertTrue(root.get("presets").get("thumbnail").asBoolean());
+    }
+
     /** TIKA-4834: the config docs permit comments; the merge step must not 
reject them. */
     @Test
     public void testMergeWithCommentedConfig() throws IOException {
diff --git 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerCli.java
 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerCli.java
index 49dab1b970..1adb5ed060 100644
--- 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerCli.java
+++ 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerCli.java
@@ -42,6 +42,9 @@ public class TikaServerCli {
         options.addOption(null, "metricsPort", true,
                 "serve Prometheus metrics on this port (9404 by convention); "
                         + "unset means metrics are off\n");
+        options.addOption(null, "preset", true,
+                "activate a catalog preset, as \"presets\": {\"<name>\": true} 
in the config "
+                        + "would; repeatable, and TIKA_PRESETS takes a 
comma-separated list");
         return options;
     }
 
diff --git 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerConfig.java
 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerConfig.java
index 13774917ef..6747f953ac 100644
--- 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerConfig.java
+++ 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerConfig.java
@@ -22,10 +22,13 @@ import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
 
+import com.fasterxml.jackson.databind.JsonNode;
 import org.apache.commons.cli.CommandLine;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -35,12 +38,17 @@ import org.apache.tika.config.loader.TikaLoader;
 import org.apache.tika.exception.TikaConfigException;
 import org.apache.tika.exception.TikaException;
 import org.apache.tika.pipes.core.PipesConfig;
+import org.apache.tika.pipes.core.config.ConfigMerger;
+import org.apache.tika.pipes.core.config.ConfigOverrides;
 
 public class TikaServerConfig {
 
     public static final int DEFAULT_PORT = 9998;
     public static final String DEFAULT_HOST = "localhost";
     private static final Logger LOG = 
LoggerFactory.getLogger(TikaServerConfig.class);
+
+    /** Comma-separated catalog presets to activate, read at startup like 
{@code --preset}. */
+    public static final String PRESETS_ENV = "TIKA_PRESETS";
     /**
      * Endpoints that expose the pipes/fetch machinery (process-isolated pipes
      * parsing and async batch processing). Selecting any of these requires
@@ -73,6 +81,7 @@ public class TikaServerConfig {
     private String host = DEFAULT_HOST;
     private String requestLogLevel = "";
     private Path configPath;
+    private List<String> presets = new ArrayList<>();
     private ArrayList<String> endpoints = new ArrayList<>();
 
     private TlsConfig tlsConfig = new TlsConfig();
@@ -86,15 +95,37 @@ public class TikaServerConfig {
     }
 
     public static TikaServerConfig load(CommandLine commandLine) throws 
IOException, TikaException {
+        return load(commandLine, System.getenv(PRESETS_ENV));
+    }
+
+    static TikaServerConfig load(CommandLine commandLine, String presetsEnv)
+            throws IOException, TikaException {
+
+        Map<String, String> presets = new LinkedHashMap<>();
+        if (commandLine.hasOption("preset")) {
+            for (String name : commandLine.getOptionValues("preset")) {
+                presets.putIfAbsent(name.trim(), "--preset");
+            }
+        }
+        if (presetsEnv != null) {
+            for (String name : presetsEnv.split(",")) {
+                if (!name.isBlank()) {
+                    presets.putIfAbsent(name.trim(), PRESETS_ENV);
+                }
+            }
+        }
 
         TikaServerConfig config = null;
         Set<String> settings = new HashSet<>();
 
         if (commandLine.hasOption("c")) {
-            config = load(Paths.get(commandLine.getOptionValue("c")), 
commandLine, settings);
+            config = load(Paths.get(commandLine.getOptionValue("c")), 
commandLine, settings,
+                    presets);
         } else {
             config = new TikaServerConfig();
+            presets.forEach((name, source) -> LOG.info("preset '{}' activated 
by {}", name, source));
         }
+        config.presets = new ArrayList<>(presets.keySet());
 
         //port, host and id can be overwritten on the commandline at runtime
         if (commandLine.hasOption("p")) {
@@ -127,7 +158,26 @@ public class TikaServerConfig {
     }
 
     static TikaServerConfig load(Path tikaConfigPath, CommandLine commandLine, 
Set<String> settings) throws IOException, TikaException {
+        return load(tikaConfigPath, commandLine, settings, Map.of());
+    }
+
+    private static TikaServerConfig load(Path tikaConfigPath, CommandLine 
commandLine,
+                                         Set<String> settings, Map<String, 
String> presets)
+            throws IOException, TikaException {
         TikaJsonConfig tikaJsonConfig = 
TikaLoader.load(tikaConfigPath).getConfig();
+        JsonNode filePresets = tikaJsonConfig.getRootNode().get("presets");
+        presets.forEach((name, source) -> {
+            JsonNode entry = filePresets == null ? null : 
filePresets.get(name);
+            if (entry == null) {
+                LOG.info("preset '{}' activated by {}", name, source);
+            } else if (entry.isObject()) {
+                LOG.warn("preset '{}' activated by {}: the catalog preset 
replaces the inline "
+                        + "definition in {}", name, source, tikaConfigPath);
+            } else {
+                LOG.warn("preset '{}' activated by {}: overrides \"{}\": {} in 
{}", name, source,
+                        name, entry, tikaConfigPath);
+            }
+        });
         TikaServerConfig tikaServerConfig = 
tikaJsonConfig.deserialize("server", TikaServerConfig.class);
         if (tikaServerConfig == null) {
             throw new TikaConfigException("Couldn't find 'server' element");
@@ -251,6 +301,29 @@ public class TikaServerConfig {
         return configPath != null;
     }
 
+    /**
+     * Catalog presets named with {@code --preset} or in {@value 
#PRESETS_ENV}, activated like
+     * {@code "presets": {"name": true}} in the config, over the config's own 
entry for the name.
+     */
+    @com.fasterxml.jackson.annotation.JsonIgnore
+    public List<String> getPresets() {
+        return presets;
+    }
+
+    /**
+     * The config file with {@link #getPresets()} folded in: the file itself 
when there are
+     * none, else a temp copy, for components that load their configuration 
from a path.
+     */
+    @com.fasterxml.jackson.annotation.JsonIgnore
+    public Path getConfigPathWithPresets() throws IOException {
+        if (presets.isEmpty()) {
+            return configPath;
+        }
+        ConfigOverrides.Builder overrides = ConfigOverrides.builder();
+        presets.forEach(overrides::addPreset);
+        return ConfigMerger.mergeOrCreate(configPath, 
overrides.build()).configPath();
+    }
+
     @com.fasterxml.jackson.annotation.JsonIgnore
     public Path getConfigPath() {
         return configPath;
diff --git 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
index c8713ca74f..a986773f0b 100644
--- 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
+++ 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
@@ -125,6 +125,9 @@ public class TikaServerProcess {
         options.addOption(null, "metricsPort", true,
                 "serve Prometheus metrics on this port (9404 by convention); "
                         + "unset means metrics are off\n");
+        options.addOption(null, "preset", true,
+                "activate a catalog preset, as \"presets\": {\"<name>\": true} 
in the config "
+                        + "would; repeatable, and TIKA_PRESETS takes a 
comma-separated list");
         options.addOption("?", "help", false, "this help message");
         return options;
     }
@@ -262,6 +265,15 @@ public class TikaServerProcess {
 
         if (tikaServerConfig.hasConfigFile()) {
             LOG.info("Using custom config: {}", 
tikaServerConfig.getConfigPath());
+        }
+        if (!tikaServerConfig.getPresets().isEmpty()) {
+            // the front end's copy; createServerConfig gives the forks theirs
+            ConfigOverrides.Builder presets = ConfigOverrides.builder();
+            tikaServerConfig.getPresets().forEach(presets::addPreset);
+            Path existing = tikaServerConfig.hasConfigFile() ? 
tikaServerConfig.getConfigPath() : null;
+            tikaLoader = TikaLoader.load(ConfigMerger.merge(existing, 
presets.build()).load(),
+                    Thread.currentThread().getContextClassLoader());
+        } else if (tikaServerConfig.hasConfigFile()) {
             tikaLoader = TikaLoader.load(tikaServerConfig.getConfigPath());
         } else {
             tikaLoader = TikaLoader.loadDefault();
@@ -537,7 +549,8 @@ public class TikaServerProcess {
         }
 
         if (addAsyncResource) {
-            final AsyncResource localAsyncResource = new 
AsyncResource(tikaServerConfig.getConfigPath());
+            final AsyncResource localAsyncResource =
+                    new 
AsyncResource(tikaServerConfig.getConfigPathWithPresets());
             
localAsyncResource.setMaxQueuePauseMillis(tikaServerConfig.getMaxQueuePauseMillis());
             Runtime
                     .getRuntime()
@@ -701,7 +714,7 @@ public class TikaServerProcess {
         Path existingConfigPath = tikaServerConfig.hasConfigFile() ?
                 tikaServerConfig.getConfigPath() : null;
         TikaJsonConfig tikaJsonConfig = createServerConfig(existingConfigPath,
-                inputTempDirectory, unpackTempDirectory);
+                inputTempDirectory, unpackTempDirectory, 
tikaServerConfig.getPresets());
 
         // Load or create PipesConfig with defaults
         PipesConfig pipesConfig = tikaJsonConfig.deserialize("pipes", 
PipesConfig.class);
@@ -746,11 +759,13 @@ public class TikaServerProcess {
      * @param existingConfigPath the existing config file path (may be null)
      * @param inputTempDirectory the temp directory for input files
      * @param unpackTempDirectory the temp directory for unpack output files 
(may be null)
+     * @param presets catalog presets activated on the command line or in 
TIKA_PRESETS
      * @return the merged config
      */
     private static TikaJsonConfig createServerConfig(Path existingConfigPath,
                                                      Path inputTempDirectory,
-                                                     Path unpackTempDirectory)
+                                                     Path unpackTempDirectory,
+                                                     List<String> presets)
             throws IOException, TikaConfigException {
         LOG.info("Configuring {} with basePath={}", 
PipesParsingHelper.DEFAULT_FETCHER_ID, inputTempDirectory);
 
@@ -765,6 +780,7 @@ public class TikaServerProcess {
                 .setEmitStrategy(EmitStrategy.PASSBACK_ALL)
                 // Set plugin roots
                 
.setPluginRoots(DefaultPluginsDir.resolve(TikaServerProcess.class));
+        presets.forEach(builder::addPreset);
 
         // Only set default pipes config if there's no existing config
         // This allows user-provided config to specify their own numClients, 
etc.
diff --git 
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/IntegrationTestBase.java
 
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/IntegrationTestBase.java
index 24aaf28cfd..bebc5d8267 100644
--- 
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/IntegrationTestBase.java
+++ 
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/IntegrationTestBase.java
@@ -32,6 +32,7 @@ import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
@@ -171,7 +172,17 @@ public class IntegrationTestBase extends TikaTest {
         process = start(extraArgs, TEMP_WORKING_DIR);
     }
 
+    public void startProcess(String[] extraArgs, Map<String, String> 
environment)
+            throws IOException {
+        process = start(extraArgs, TEMP_WORKING_DIR, environment);
+    }
+
     private Process start(String[] extraArgs, Path workingDir) throws 
IOException {
+        return start(extraArgs, workingDir, Map.of());
+    }
+
+    private Process start(String[] extraArgs, Path workingDir, Map<String, 
String> environment)
+            throws IOException {
         String[] base = new String[]{"java",
                 "-Djava.io.tmpdir=" + workingDir.toAbsolutePath(), // make 
sure we're using subdir cleaned up by JUnit
                 "-cp", System.getProperty("java.class.path"), 
"org.apache.tika.server.core.TikaServerCli",
@@ -179,6 +190,7 @@ public class IntegrationTestBase extends TikaTest {
         List<String> args = new ArrayList<>(Arrays.asList(base));
         args.addAll(Arrays.asList(extraArgs));
         ProcessBuilder pb = new ProcessBuilder(args);
+        pb.environment().putAll(environment);
         pb.inheritIO();
 //        pb.redirectInput(Files.createTempFile(STREAMS_DIR, 
"tika-stream-out", ".log").toFile());
         //      pb.redirectError(Files.createTempFile(STREAMS_DIR,
diff --git 
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/PresetActivationIntegrationTest.java
 
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/PresetActivationIntegrationTest.java
new file mode 100644
index 0000000000..8f20c3ca3b
--- /dev/null
+++ 
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/PresetActivationIntegrationTest.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.server.core;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.ws.rs.core.Response;
+import org.apache.cxf.jaxrs.client.WebClient;
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.serialization.JsonMetadataList;
+
+/**
+ * A catalog preset activated with {@code --preset} or {@code TIKA_PRESETS} 
instead of the
+ * config's {@code presets} block, resolved by the front end and the forked 
worker alike. The
+ * catalog entry is the test classpath's {@code catalog-xml-content}.
+ */
+public class PresetActivationIntegrationTest extends IntegrationTestBase {
+
+    private static final String PRESET = "catalog-xml-content";
+
+    @Test
+    public void testPresetOption() throws Exception {
+        startProcess(new String[]{"-config", 
getConfig("tika-config-server-basic.json"),
+                "--preset", PRESET});
+        assertPresetServesXml();
+    }
+
+    @Test
+    public void testPresetsEnvironmentVariable() throws Exception {
+        startProcess(new String[]{"-config", 
getConfig("tika-config-server-basic.json")},
+                Map.of(TikaServerConfig.PRESETS_ENV, " " + PRESET + ", "));
+        assertPresetServesXml();
+    }
+
+    @Test
+    public void testUnknownPresetFailsStartup() throws Exception {
+        startProcess(new String[]{"-config", 
getConfig("tika-config-server-basic.json"),
+                "--preset", "no-such-preset"});
+        assertTrue(process.waitFor(MAX_WAIT_MS, TimeUnit.MILLISECONDS), 
"server should exit");
+        assertNotEquals(0, process.exitValue());
+    }
+
+    private void assertPresetServesXml() throws Exception {
+        awaitServerStartup();
+        Response response = WebClient
+                .create(endPoint + RMETA_PATH + "/preset/" + PRESET)
+                .accept("application/json")
+                .put(ClassLoader.getSystemResourceAsStream(TEST_HELLO_WORLD));
+        assertEquals(200, response.getStatus());
+        Reader reader = new InputStreamReader((InputStream) 
response.getEntity(), UTF_8);
+        List<Metadata> metadataList = JsonMetadataList.fromJson(reader);
+        assertEquals(1, metadataList.size());
+        assertContains("<body><p>hello world</p>",
+                metadataList.get(0).get(TikaCoreProperties.TIKA_CONTENT));
+    }
+}
diff --git 
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerConfigTest.java
 
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerConfigTest.java
index 2b516a65a0..7ce1bbb390 100644
--- 
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerConfigTest.java
+++ 
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerConfigTest.java
@@ -18,14 +18,18 @@ package org.apache.tika.server.core;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.nio.file.Path;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.CommandLineParser;
 import org.apache.commons.cli.DefaultParser;
@@ -81,6 +85,41 @@ public class TikaServerConfigTest extends TikaTest {
         assertEquals(60000L, TikaServerConfig.DEFAULT_MAX_QUEUE_PAUSE_MILLIS);
     }
 
+    @Test
+    public void testPresetsFromCommandLineAndEnvironment() throws Exception {
+        Options options = new 
Options().addOption(Option.builder().longOpt("preset").hasArg().get());
+        CommandLine commandLine = new DefaultParser().parse(options,
+                new String[]{"--preset", "thumbnail", "--preset", 
"render-thumbnail"});
+
+        TikaServerConfig config = TikaServerConfig.load(commandLine, " 
render-thumbnail, ,no-ocr ");
+        assertEquals(List.of("thumbnail", "render-thumbnail", "no-ocr"), 
config.getPresets());
+
+        config = TikaServerConfig.load(commandLine, null);
+        assertEquals(List.of("thumbnail", "render-thumbnail"), 
config.getPresets());
+
+        config = TikaServerConfig.load(new DefaultParser().parse(options, new 
String[]{}), null);
+        assertTrue(config.getPresets().isEmpty());
+    }
+
+    @Test
+    public void testConfigPathWithPresets() throws Exception {
+        Path path = getConfigPath(getClass(), "tika-config-server.json");
+        Options options = new Options()
+                .addOption(Option.builder().longOpt("preset").hasArg().get())
+                
.addOption(Option.builder("c").longOpt("config").hasArg().get());
+        String[] args = {"-c", 
ProcessUtils.escapeCommandLine(path.toAbsolutePath().toString())};
+
+        TikaServerConfig config = TikaServerConfig.load(new 
DefaultParser().parse(options, args), null);
+        assertEquals(path.toAbsolutePath(), config.getConfigPathWithPresets());
+
+        config = TikaServerConfig.load(new DefaultParser().parse(options, 
args), "thumbnail");
+        Path merged = config.getConfigPathWithPresets();
+        assertNotEquals(path.toAbsolutePath(), merged);
+        JsonNode root = new ObjectMapper().readTree(merged.toFile());
+        assertTrue(root.get("presets").get("thumbnail").asBoolean());
+        assertTrue(root.has("server"));
+    }
+
     @Test
     public void testPorts() throws Exception {
         CommandLineParser parser = new DefaultParser();
diff --git 
a/tika-server/tika-server-core/src/test/resources/META-INF/tika/preset-catalog.properties
 
b/tika-server/tika-server-core/src/test/resources/META-INF/tika/preset-catalog.properties
new file mode 100644
index 0000000000..b687a83138
--- /dev/null
+++ 
b/tika-server/tika-server-core/src/test/resources/META-INF/tika/preset-catalog.properties
@@ -0,0 +1,19 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# inert until --preset, TIKA_PRESETS or a config activates it
+catalog-xml-content=/test-presets/catalog-xml-content.json
diff --git 
a/tika-server/tika-server-core/src/test/resources/test-presets/catalog-xml-content.json
 
b/tika-server/tika-server-core/src/test/resources/test-presets/catalog-xml-content.json
new file mode 100644
index 0000000000..572b142162
--- /dev/null
+++ 
b/tika-server/tika-server-core/src/test/resources/test-presets/catalog-xml-content.json
@@ -0,0 +1,3 @@
+{
+  "basic-content-handler-factory": {"type": "XML"}
+}

Reply via email to