This is an automated email from the ASF dual-hosted git repository.
nddipiazza 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 88220e158b TIKA-4735: Fix --content-only batch output and remove
-h/--help short option conflict (#2919)
88220e158b is described below
commit 88220e158b50fe5a13b35148160861db56c79b50
Author: Nicholas DiPiazza <[email protected]>
AuthorDate: Wed Jul 1 15:13:22 2026 +0000
TIKA-4735: Fix --content-only batch output and remove -h/--help short
option conflict (#2919)
* TIKA-4735: Fix --content-only batch output and remove -h/--help short
option conflict
- ParseHandler now writes the resolved ParseMode back into ParseContext so
EmitHandler (which reads ParseContext directly with no defaultParseMode
fallback) correctly invokes content-only emit when parseMode is set only
as a PipesConfig default and not explicitly in the per-request context.
- Removed the -h short option from --help in TikaAsyncCLI to eliminate the
conflict with the documented handler shorthand; --help still works as a
long option.
- Added reproducer tests:
AsyncCliParserTest: testTIKA4735_ContentOnlyHandlerMarkdown,
testTIKA4735_ContentOnlyDefaultHandler
TikaConfigAsyncWriterTest: testTIKA4735_ContentOnlyMarkdownConfig,
testTIKA4735_ContentOnlyDefaultMarkdownConfig
AsyncProcessorTest: testContentOnlyFromConfigDefault (integration test
that sends a request without ParseMode in the context and asserts the
output file is raw content, not a JSON metadata wrapper)
Co-authored-by: Copilot <[email protected]>
* TIKA-4735: Fix CONTENT_ONLY passback path in EmitHandler
When emitStrategy=DYNAMIC and the file is small, EmitHandler was
returning the result as a passback to AsyncProcessor. The parent-side
AsyncEmitter then calls emitter.emit(List<EmitData>) which serialises
the metadata list as JSON, bypassing the StreamEmitter path that
produces raw content output.
Fix: CONTENT_ONLY mode now always emits directly from the server side
(shouldEmit returns true for any strategy other than PASSBACK_ALL),
ensuring emitContentOnly() is always used and the output is raw text/
markdown rather than JSON.
Co-authored-by: Copilot <[email protected]>
* TIKA-4735: Fix EmitHandler CONTENT_ONLY to use
emitterManager.getSupported()
Use emitterManager.getSupported().contains(emitterId) instead of null
check to determine whether to force direct emission in CONTENT_ONLY mode.
This correctly handles the case where PipesClient is used directly without
a configured emitter (null emitterId), allowing passback to work normally
while still forcing direct emission (via emitContentOnly) when a real
StreamEmitter is registered.
Fixes CI failures in PipesClientTest.testContentOnlyMode and
testContentOnlyModeWithUserFilter.
Co-authored-by: Copilot <[email protected]>
* TIKA-4735: Replace noise tests with DYNAMIC-passback regression test
Remove tests that did not exercise the actual changed code:
- AsyncCliParserTest: testTIKA4735_ContentOnlyHandlerMarkdown,
testTIKA4735_ContentOnlyDefaultHandler (tested arg parsing; did not
cover the -h removal or any emit-path change)
- TikaConfigAsyncWriterTest: testTIKA4735_ContentOnlyMarkdownConfig,
testTIKA4735_ContentOnlyDefaultMarkdownConfig (tested unchanged config
generation; two tests for identical behaviour)
Add AsyncProcessorTest#testContentOnlyDynamicEmitStrategy: uses
emitStrategy=DYNAMIC with a 10 MB threshold so the small test file goes
through the passback path. Before the EmitHandler fix, the AsyncEmitter
would call emitter.emit(List<EmitData>) and produce JSON; after the fix
it calls emitContentOnly() and the output is raw text.
Co-authored-by: Copilot <[email protected]>
---------
Co-authored-by: Copilot <[email protected]>
---
.../org/apache/tika/async/cli/TikaAsyncCLI.java | 2 +-
.../apache/tika/async/cli/AsyncProcessorTest.java | 39 ++++++++++++++++
.../tika/async/cli/TikaConfigAsyncWriterTest.java | 1 -
.../configs/config-content-only-dynamic.json | 53 ++++++++++++++++++++++
.../apache/tika/pipes/core/server/EmitHandler.java | 10 +++-
.../tika/pipes/core/server/ParseHandler.java | 3 ++
6 files changed, 105 insertions(+), 3 deletions(-)
diff --git
a/tika-pipes/tika-async-cli/src/main/java/org/apache/tika/async/cli/TikaAsyncCLI.java
b/tika-pipes/tika-async-cli/src/main/java/org/apache/tika/async/cli/TikaAsyncCLI.java
index d9b96d359b..cd06371b20 100644
---
a/tika-pipes/tika-async-cli/src/main/java/org/apache/tika/async/cli/TikaAsyncCLI.java
+++
b/tika-pipes/tika-async-cli/src/main/java/org/apache/tika/async/cli/TikaAsyncCLI.java
@@ -69,7 +69,7 @@ public class TikaAsyncCLI {
options.addOption("o", "outputDir", true, "output directory");
options.addOption("n", "numClients", true, "number of forked clients");
options.addOption(null, "Xmx", true, "heap for the forked clients,
e.g. --Xmx 1g");
- options.addOption("h", "help", false, "this help message");
+ options.addOption(null, "help", false, "this help message");
options.addOption("T", "timeoutMs", true, "timeout for each parse in
milliseconds");
options.addOption(null, "handler", true, "handler type: t=text,
h=html, x=xml, m=markdown, b=body, i=ignore (default: m)");
options.addOption("p", "pluginsDir", true, "plugins directory");
diff --git
a/tika-pipes/tika-async-cli/src/test/java/org/apache/tika/async/cli/AsyncProcessorTest.java
b/tika-pipes/tika-async-cli/src/test/java/org/apache/tika/async/cli/AsyncProcessorTest.java
index 17fc05d2b7..4092c01029 100644
---
a/tika-pipes/tika-async-cli/src/test/java/org/apache/tika/async/cli/AsyncProcessorTest.java
+++
b/tika-pipes/tika-async-cli/src/test/java/org/apache/tika/async/cli/AsyncProcessorTest.java
@@ -201,6 +201,45 @@ public class AsyncProcessorTest extends TikaTest {
"content-only output must be raw content, not a JSON wrapper:
" + emitted);
}
+ @Test
+ public void testContentOnlyDynamicEmitStrategy() throws Exception {
+ // TIKA-4735: With emitStrategy=DYNAMIC and a file smaller than the
threshold,
+ // EmitHandler would previously let the file passback to AsyncEmitter,
which called
+ // emitter.emit(List<EmitData>) and serialised it as JSON rather than
raw content.
+ // The fix forces direct server-side emission (via emitContentOnly)
when an emitter
+ // is configured and parseMode=CONTENT_ONLY, regardless of emit
strategy.
+ Path contentOnlyConfig =
configDir.resolve("tika-config-content-only-dynamic.json");
+ Map<String, Object> replacements = new HashMap<>();
+ replacements.put("FETCHER_BASE_PATH", inputDir);
+ replacements.put("JSON_EMITTER_BASE_PATH", jsonOutputDir);
+ replacements.put("BYTES_EMITTER_BASE_PATH", bytesOutputDir);
+ replacements.put("PLUGIN_ROOTS", Paths.get("target/plugins"));
+
JsonConfigHelper.writeConfigFromResource("/configs/config-content-only-dynamic.json",
+ AsyncProcessorTest.class, replacements, contentOnlyConfig);
+
+ AsyncProcessor processor = AsyncProcessor.load(contentOnlyConfig);
+
+ FetchEmitTuple t = new FetchEmitTuple("co-dyn-1", new FetchKey("fsf",
"mock.xml"),
+ new EmitKey("fse-json", "emit-co-dyn"), new Metadata(), new
ParseContext(),
+ FetchEmitTuple.ON_PARSE_EXCEPTION.EMIT);
+ processor.offer(t, 1000);
+ for (int i = 0; i < 10; i++) {
+ processor.offer(PipesIterator.COMPLETED_SEMAPHORE, 1000);
+ }
+ while (processor.checkActive()) {
+ Thread.sleep(100);
+ }
+ processor.close();
+
+ String emitted =
Files.readString(jsonOutputDir.resolve("emit-co-dyn"));
+ assertContains("content", emitted);
+
assertFalse(emitted.contains(TikaCoreProperties.TIKA_CONTENT.getName()),
+ "TIKA-4735: DYNAMIC strategy must not produce JSON wrapper in
CONTENT_ONLY mode: " + emitted);
+ String trimmed = emitted.trim();
+ assertFalse(trimmed.startsWith("[") || trimmed.startsWith("{"),
+ "TIKA-4735: DYNAMIC strategy must produce raw content, not
JSON, in CONTENT_ONLY mode: " + emitted);
+ }
+
@Test
public void testStopsOnApplicationError() throws Exception {
AsyncProcessor processor =
AsyncProcessor.load(configDir.resolve("tika-config.json"));
diff --git
a/tika-pipes/tika-async-cli/src/test/java/org/apache/tika/async/cli/TikaConfigAsyncWriterTest.java
b/tika-pipes/tika-async-cli/src/test/java/org/apache/tika/async/cli/TikaConfigAsyncWriterTest.java
index c9b0f534bd..2be74747a2 100644
---
a/tika-pipes/tika-async-cli/src/test/java/org/apache/tika/async/cli/TikaConfigAsyncWriterTest.java
+++
b/tika-pipes/tika-async-cli/src/test/java/org/apache/tika/async/cli/TikaConfigAsyncWriterTest.java
@@ -48,5 +48,4 @@ public class TikaConfigAsyncWriterTest {
PipesConfig pipesConfig = PipesConfig.load(tikaJsonConfig);
assertEquals("-Xmx1g", pipesConfig.getForkedJvmArgs().get(0));
}
-
}
diff --git
a/tika-pipes/tika-async-cli/src/test/resources/configs/config-content-only-dynamic.json
b/tika-pipes/tika-async-cli/src/test/resources/configs/config-content-only-dynamic.json
new file mode 100644
index 0000000000..d7ef51200f
--- /dev/null
+++
b/tika-pipes/tika-async-cli/src/test/resources/configs/config-content-only-dynamic.json
@@ -0,0 +1,53 @@
+{
+ "fetchers": {
+ "fsf": {
+ "file-system-fetcher": {
+ "basePath": "FETCHER_BASE_PATH",
+ "extractFileSystemMetadata": false
+ }
+ }
+ },
+ "emitters": {
+ "fse-json": {
+ "file-system-emitter": {
+ "basePath": "JSON_EMITTER_BASE_PATH",
+ "fileExtension": "",
+ "onExists": "EXCEPTION"
+ }
+ },
+ "fse-bytes": {
+ "file-system-emitter": {
+ "basePath": "BYTES_EMITTER_BASE_PATH",
+ "fileExtension": "",
+ "onExists": "EXCEPTION"
+ }
+ }
+ },
+ "parse-context": {
+ "timeout-limits": {
+ "progressTimeoutMillis": 60000
+ }
+ },
+ "pipes": {
+ "parseMode": "CONTENT_ONLY",
+ "emitWithinMillis": 10000,
+ "queueSize": 10000,
+ "numEmitters": 1,
+ "emitIntermediateResults": false,
+ "shutdownClientAfterMillis": 300000,
+ "numClients": 2,
+ "maxFilesProcessedPerProcess": 10000,
+ "staleFetcherTimeoutSeconds": 600,
+ "staleFetcherDelaySeconds": 60,
+ "forkedJvmArgs": [
+ "-Xmx1g",
+ "-XX:+UseG1GC"
+ ],
+ "javaPath": "java",
+ "emitStrategy": {
+ "type": "DYNAMIC",
+ "thresholdBytes": 10000000
+ }
+ },
+ "plugin-roots": "PLUGIN_ROOTS"
+}
diff --git
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/EmitHandler.java
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/EmitHandler.java
index 78a21bfa23..94074e048a 100644
---
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/EmitHandler.java
+++
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/EmitHandler.java
@@ -83,7 +83,15 @@ class EmitHandler {
}
EmitDataImpl emitDataTuple = new
EmitDataImpl(t.getEmitKey().getEmitKey(), parseData.getMetadataList(), stack);
ParseMode parseMode = parseContext.get(ParseMode.class);
- if (shouldEmit(parseMode, parseData, emitDataTuple, parseContext))
{
+ String emitterId = emitKey.getEmitterId();
+ // For CONTENT_ONLY mode: force direct emission only when a real
emitter is configured,
+ // so the content is emitted as a raw byte stream via
emitContentOnly() instead of
+ // being returned as passback JSON to the parent AsyncEmitter.
+ boolean forceEmit = parseMode == ParseMode.CONTENT_ONLY
+ && emitterId != null
+ && emitterManager.getSupported().contains(emitterId);
+ boolean willEmit = forceEmit || shouldEmit(parseMode, parseData,
emitDataTuple, parseContext);
+ if (willEmit) {
return emit(t.getId(), emitKey, parseMode == ParseMode.UNPACK,
parseData, stack, parseContext);
} else {
diff --git
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/ParseHandler.java
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/ParseHandler.java
index cd02d99767..c52008d0d0 100644
---
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/ParseHandler.java
+++
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/ParseHandler.java
@@ -87,6 +87,9 @@ class ParseHandler {
List<Metadata> metadataList;
//this adds the EmbeddedDocumentByteStore to the parsecontext
ParseMode parseMode = getParseMode(parseContext);
+ // Ensure the resolved mode is visible to EmitHandler, which checks
parseContext directly
+ // and does not have access to the defaultParseMode fallback.
+ parseContext.set(ParseMode.class, parseMode);
ContentHandlerFactory contentHandlerFactory =
getContentHandlerFactory(parseContext);
if (parseMode == ParseMode.NO_PARSE) {
metadataList = detectOnly(fetchEmitTuple, stream, metadata,
parseContext);