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 db1e2e6b94 TIKA-4889: a text recognizer is an engine (#3156)
db1e2e6b94 is described below
commit db1e2e6b945c553df0ff23080a81acab77845eb3
Author: Tim Allison <[email protected]>
AuthorDate: Sat Sep 12 07:00:54 2026 -0400
TIKA-4889: a text recognizer is an engine (#3156)
---
CHANGES.txt | 6 ++
.../ROOT/pages/advanced/local-vlm-server.adoc | 7 +-
docs/modules/ROOT/pages/configuration/index.adoc | 16 ++-
.../ROOT/pages/configuration/inference.adoc | 6 ++
.../parsers/tesseract-ocr-parser.adoc | 3 +-
.../ROOT/pages/developers/custom-ocr-engine.adoc | 21 ++--
.../tika/parser/enricher/ContentEnricher.java | 7 +-
.../resources/config-examples/tesseract-basic.json | 10 +-
.../tika/config/loader/ComponentInstantiator.java | 10 ++
.../tika/config/loader/ContentEnricherLoader.java | 109 ++++++++++++++++-----
.../org/apache/tika/config/loader/TikaLoader.java | 6 ++
.../config/loader/ContentEnricherLoaderTest.java | 47 +++++++++
.../tika/config/loader/InferenceLoaderTest.java | 10 ++
.../configs/tika-config-text-recognizers.json | 3 +-
14 files changed, 220 insertions(+), 41 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 7d9d547f0d..5c269968e1 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,11 @@
Release 4.1.0 - unreleased
+ * A text recognizer is an engine: configure Tesseract, Tess4J or a VLM once
+ under "engines" and name it from "text-recognizers" with
+ {"engine": "<name>"} (plus _mime-include/_mime-exclude). The 4.0 form with
+ the engine inline still works. An inference binding cannot use a
+ recognizer for a task it does not fit (TIKA-4889).
+
* PAGES input for inference bindings: pdf-parser.inference.input lists what
the PDF releases (TEXT by default; PAGES renders every page for a PAGES
binding). One render per page feeds OCR and the bindings, and each page's
diff --git a/docs/modules/ROOT/pages/advanced/local-vlm-server.adoc
b/docs/modules/ROOT/pages/advanced/local-vlm-server.adoc
index 42965309e4..9ebf5cf8d5 100644
--- a/docs/modules/ROOT/pages/advanced/local-vlm-server.adoc
+++ b/docs/modules/ROOT/pages/advanced/local-vlm-server.adoc
@@ -344,15 +344,16 @@ transcribes embedded images and rendered PDF pages while
every default parser st
[source,json]
----
{
- "text-recognizers": [
- {
+ "engines": {
+ "local-vlm": {
"openai-vlm-parser": {
"baseUrl": "http://127.0.0.1:8000",
"model": "jinaai/jina-vlm",
"timeoutMillis": 600000
}
}
- ]
+ },
+ "text-recognizers": [ { "engine": "local-vlm" } ]
}
----
diff --git a/docs/modules/ROOT/pages/configuration/index.adoc
b/docs/modules/ROOT/pages/configuration/index.adoc
index 34f4219083..507015ff37 100644
--- a/docs/modules/ROOT/pages/configuration/index.adoc
+++ b/docs/modules/ROOT/pages/configuration/index.adoc
@@ -133,18 +133,26 @@ Text recognizers are engines that produce a document's
text from its pixels: an
on an embedded image or a rendered PDF page, a VLM prompted to transcribe. A
container parser
*invokes* them on bytes it has already parsed. Two recipes cover almost every
deployment.
-*Choose the engine.* Name it here; every default parser stays loaded, and
`image-parser` and
-`pdf-parser` call it:
+*Choose the engine.* Configure it once under `engines` and name it here; every
default parser
+stays loaded, and `image-parser` and `pdf-parser` call it:
[source,json]
----
{
+ "engines": {
+ "tesseract": { "tesseract-ocr-parser": { "language": "eng" } }
+ },
"text-recognizers": [
- { "tesseract-ocr-parser": { "language": "eng" } }
+ { "engine": "tesseract" }
]
}
----
+An entry naming an engine takes only `engine`, `_mime-include` and
`_mime-exclude`; the engine's
+own settings live under `engines`, where an
+xref:configuration/inference.adoc[inference binding] can name the same engine.
The 4.0 form,
+the engine inline, still works: `{ "tesseract-ocr-parser": { "language": "eng"
} }`.
+
*Turn OCR off.* An empty list is authoritative:
[source,json]
@@ -152,7 +160,7 @@ on an embedded image or a rendered PDF page, a VLM prompted
to transcribe. A con
{ "text-recognizers": [] }
----
-Engine names: `tesseract-ocr-parser`, `tess4j-parser`, `openai-vlm-parser`,
`claude-vlm-parser`,
+Engine types: `tesseract-ocr-parser`, `tess4j-parser`, `openai-vlm-parser`,
`claude-vlm-parser`,
`gemini-vlm-parser`. At startup Tika logs one line per engine with the media
types it enriches, so
the effective engine is never a guess.
diff --git a/docs/modules/ROOT/pages/configuration/inference.adoc
b/docs/modules/ROOT/pages/configuration/inference.adoc
index 5b242d20f8..c2069243e6 100644
--- a/docs/modules/ROOT/pages/configuration/inference.adoc
+++ b/docs/modules/ROOT/pages/configuration/inference.adoc
@@ -43,6 +43,12 @@ different models. Secrets and paths live here and only here.
`maxBatchSize` images per request. Options: `baseUrl`, `model`, `apiKey`,
`timeoutMillis`
(120000), `maxBatchSize` (32), `embeddingsPath`, `apiKeyHeaderName`,
`apiKeyPrefix`.
+A text recognizer is an engine too: `"tesseract": { "tesseract-ocr-parser": {
"language": "eng" } }`
+here, and `{ "engine": "tesseract" }` in
+xref:configuration/index.adoc#text-recognizers[`text-recognizers`], so an OCR
engine or a VLM is
+configured in one place. A binding cannot use a recognizer for a task it does
not fit; `embed`
+on Tesseract fails config load.
+
== `inference`: what runs on what
A list of bindings. Each names an engine, says what it is fed, and what to ask:
diff --git
a/docs/modules/ROOT/pages/configuration/parsers/tesseract-ocr-parser.adoc
b/docs/modules/ROOT/pages/configuration/parsers/tesseract-ocr-parser.adoc
index 7257d0040d..18f6da8b2a 100644
--- a/docs/modules/ROOT/pages/configuration/parsers/tesseract-ocr-parser.adoc
+++ b/docs/modules/ROOT/pages/configuration/parsers/tesseract-ocr-parser.adoc
@@ -28,7 +28,8 @@ Name the engine in the top-level `text-recognizers` list;
every default parser s
[source,json]
----
{
- "text-recognizers": [ { "tesseract-ocr-parser": { "language": "eng" } } ]
+ "engines": { "tesseract": { "tesseract-ocr-parser": { "language": "eng" } }
},
+ "text-recognizers": [ { "engine": "tesseract" } ]
}
----
diff --git a/docs/modules/ROOT/pages/developers/custom-ocr-engine.adoc
b/docs/modules/ROOT/pages/developers/custom-ocr-engine.adoc
index 4691e89ea8..401966e6dc 100644
--- a/docs/modules/ROOT/pages/developers/custom-ocr-engine.adoc
+++ b/docs/modules/ROOT/pages/developers/custom-ocr-engine.adoc
@@ -202,22 +202,31 @@ xref:configuration/index.adoc#tika-extras-dir[Adding
extra jars].
[source,json]
----
{
+ "engines": {
+ "jina": { "jina-ocr-parser": { "endpoint": "http://jina:9000", "apiKey":
"..." } }
+ },
"text-recognizers": [
- { "jina-ocr-parser": { "endpoint": "http://jina:9000", "apiKey": "..." } }
+ { "engine": "jina" }
]
}
----
-Every default parser stays loaded, and Tesseract on the classpath is ignored:
the list is
-authoritative. To keep Tesseract for the formats your engine does not read, or
to run an annotator
-next to it, add entries with filters:
+A `ContentEnricher` is an engine, so it is configured under `engines` like any
other and named
+from `text-recognizers` (the 4.0 form, the engine inline in the list, still
works). Every default
+parser stays loaded, and Tesseract on the classpath is ignored: the list is
authoritative. To
+keep Tesseract for the formats your engine does not read, or to run an
annotator next to it, add
+entries with filters:
[source,json]
----
{
+ "engines": {
+ "jina": { "jina-ocr-parser": { "endpoint": "http://jina:9000" } },
+ "tesseract": { "tesseract-ocr-parser": {} }
+ },
"text-recognizers": [
- { "jina-ocr-parser": { "endpoint": "http://jina:9000" } },
- { "tesseract-ocr-parser": { "_mime-exclude": ["image/png", "image/jpeg",
"image/tiff"] } }
+ { "engine": "jina" },
+ { "engine": "tesseract", "_mime-exclude": ["image/png", "image/jpeg",
"image/tiff"] }
]
}
----
diff --git
a/tika-core/src/main/java/org/apache/tika/parser/enricher/ContentEnricher.java
b/tika-core/src/main/java/org/apache/tika/parser/enricher/ContentEnricher.java
index c02fca9c74..c29cb97206 100644
---
a/tika-core/src/main/java/org/apache/tika/parser/enricher/ContentEnricher.java
+++
b/tika-core/src/main/java/org/apache/tika/parser/enricher/ContentEnricher.java
@@ -16,14 +16,19 @@
*/
package org.apache.tika.parser.enricher;
+import org.apache.tika.parser.inference.Engine;
+
/**
* A {@link org.apache.tika.parser.Parser} a container parser <em>invokes</em>
on bytes it
* has already parsed (OCR on an image, an embedding of a rendered page)
rather than one the
* composite dispatches to; its supported types are the types it can enrich.
An enricher the
* classpath supplied is never dispatched to; named under {@code "parsers"} it
also parses
* the types no other parser there claims. See {@link ContentEnrichers#get}.
+ * <p>
+ * An enricher is an {@link Engine}: configured once under {@code "engines"}, a
+ * {@code "text-recognizers"} entry names it.
*
* @since Apache Tika 4.1
*/
-public interface ContentEnricher {
+public interface ContentEnricher extends Engine {
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/config-examples/tesseract-basic.json
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/config-examples/tesseract-basic.json
index 63d6ebc372..d46d134ff9 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/config-examples/tesseract-basic.json
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/config-examples/tesseract-basic.json
@@ -1,12 +1,16 @@
{
// Tesseract enriches every image Tika's parsers meet (embedded images,
rendered PDF
- // pages) with OCR text. Every default parser stays loaded: "parsers" is not
needed.
- "text-recognizers": [
- {
+ // pages) with OCR text. Configured once under "engines", named in
"text-recognizers".
+ // Every default parser stays loaded: "parsers" is not needed.
+ "engines": {
+ "tesseract": {
"tesseract-ocr-parser": {
"language": "eng",
"timeoutMillis": 120000
}
}
+ },
+ "text-recognizers": [
+ { "engine": "tesseract" }
]
}
diff --git
a/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentInstantiator.java
b/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentInstantiator.java
index 4f82f9d7b5..84126311b2 100644
---
a/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentInstantiator.java
+++
b/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentInstantiator.java
@@ -225,6 +225,16 @@ public class ComponentInstantiator {
}
}
+ /** The parser behind the entry's {@code _mime-include}/{@code
_mime-exclude}, if any. */
+ static Parser withMimeFilters(Parser parser, JsonNode entry) throws
TikaConfigException {
+ Set<MediaType> includeTypes = extractMimeTypes(entry, "_mime-include");
+ Set<MediaType> excludeTypes = extractMimeTypes(entry, "_mime-exclude");
+ if (includeTypes.isEmpty() && excludeTypes.isEmpty()) {
+ return parser;
+ }
+ return ParserDecorator.withMimeFilters(parser, includeTypes,
excludeTypes);
+ }
+
private static Set<MediaType> extractMimeTypes(JsonNode configNode, String
fieldName)
throws TikaConfigException {
Set<MediaType> types = new HashSet<>();
diff --git
a/tika-serialization/src/main/java/org/apache/tika/config/loader/ContentEnricherLoader.java
b/tika-serialization/src/main/java/org/apache/tika/config/loader/ContentEnricherLoader.java
index af935de82d..54abb2cb53 100644
---
a/tika-serialization/src/main/java/org/apache/tika/config/loader/ContentEnricherLoader.java
+++
b/tika-serialization/src/main/java/org/apache/tika/config/loader/ContentEnricherLoader.java
@@ -17,8 +17,10 @@
package org.apache.tika.config.loader;
import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -31,57 +33,120 @@ import org.apache.tika.parser.Parser;
import org.apache.tika.parser.ParserDecorator;
import org.apache.tika.parser.enricher.CompositeContentEnricher;
import org.apache.tika.parser.enricher.ContentEnrichers;
+import org.apache.tika.parser.inference.Engine;
+import org.apache.tika.parser.inference.EngineRegistry;
/**
- * Loads the top-level {@code "text-recognizers"} list: parsers selected by
component name
- * that container parsers invoke for derived content (OCR, ...). Members come
from the same
- * registry as {@code "parsers"} entries but never join the composite's
media-type dispatch.
- * Null when the key is absent: {@link ParserLoader} then resolves the
enrichers from the
- * loaded parsers.
+ * Loads {@code "text-recognizers"}. An entry either names an engine from
{@code "engines"}
+ * ({@code {"engine": "tesseract", "_mime-include": [...]}}) or carries the
engine inline
+ * ({@code {"tesseract-ocr-parser": {...}}}), the 4.0 form. An empty list is
authoritative;
+ * an absent key means the recognizers are resolved from the loaded parsers.
*/
class ContentEnricherLoader implements
ComponentLoader<CompositeContentEnricher> {
+ static final String KEY = "text-recognizers";
private static final Logger LOG =
LoggerFactory.getLogger(ContentEnricherLoader.class);
+ private static final Set<String> REFERENCE_KEYS = Set.of("engine",
"_mime-include",
+ "_mime-exclude");
@Override
public CompositeContentEnricher load(TikaJsonConfig config, LoaderContext
context)
throws TikaConfigException {
- List<Map.Entry<String, JsonNode>> entries =
config.getArrayComponents("text-recognizers");
- if (entries.isEmpty()) {
- // [] is an explicit "nothing", authoritative; an absent key means
"find them"
- return config.hasComponentSection("text-recognizers")
- ? new CompositeContentEnricher(List.of()) : null;
+ JsonNode node = config.getRootNode().get(KEY);
+ if (node == null) {
+ return null;
+ }
+ if (!node.isArray()) {
+ throw new TikaConfigException("\"" + KEY + "\" must be an array");
}
List<Parser> enrichers = new ArrayList<>();
ParseContext empty = new ParseContext();
- for (Map.Entry<String, JsonNode> entry : entries) {
+ for (JsonNode item : node) {
+ String label;
Parser enricher;
- try {
- ObjectNode wrapper =
context.getObjectMapper().createObjectNode();
- wrapper.set(entry.getKey(), entry.getValue());
- enricher = context.getObjectMapper().treeToValue(wrapper,
Parser.class);
- } catch (Exception e) {
- throw new TikaConfigException(
- "Failed to load text recognizer: " + entry.getKey(),
e);
+ if (item.isObject() && item.has("engine")) {
+ label = "engine \"" + item.get("engine").asText() + "\"";
+ enricher = referenced(item, context);
+ } else {
+ Map.Entry<String, JsonNode> entry = inline(item, context);
+ label = "\"" + entry.getKey() + "\"";
+ enricher = instantiate(entry, context);
}
// lifetime snapshot: an empty engine must fail load, not go
inert; ask the
// engine itself, since a _mime-include answers for the decorator
if (unwrap(enricher).getSupportedTypes(empty).isEmpty()) {
- throw new TikaConfigException("Text recognizer \"" +
entry.getKey()
- + "\" advertises no media types (a _mime-include list
does not "
+ throw new TikaConfigException("Text recognizer " + label
+ + " advertises no media types (a _mime-include list
does not "
+ "count). Is the engine unavailable (missing native
binary, "
+ "unreachable inference server) or configured to skip
enrichment?");
}
if (ContentEnrichers.asTextRecognizer(enricher) == null &&
!advertisesLegacyOcr(enricher)) {
- LOG.warn("\"text-recognizers\" entry \"{}\" recognizes no
text; it runs as an "
+ LOG.warn("\"text-recognizers\" entry {} recognizes no text; it
runs as an "
+ "annotator on the images and pages it is offered
(4.2 gives annotators "
- + "a list of their own)", entry.getKey());
+ + "a list of their own)", label);
}
enrichers.add(enricher);
}
return new CompositeContentEnricher(enrichers);
}
+ /** The engine the entry names, from {@code "engines"}, behind the entry's
mime filters. */
+ private static Parser referenced(JsonNode item, LoaderContext context)
+ throws TikaConfigException {
+ Iterator<String> names = item.fieldNames();
+ while (names.hasNext()) {
+ String name = names.next();
+ if (!REFERENCE_KEYS.contains(name)) {
+ throw new TikaConfigException("\"" + KEY + "\" entry naming an
engine has "
+ + "unknown key \"" + name + "\"; known: " +
REFERENCE_KEYS
+ + ". The engine's own settings belong under
\"engines\".");
+ }
+ }
+ JsonNode nameNode = item.get("engine");
+ if (!nameNode.isTextual()) {
+ throw new TikaConfigException("\"" + KEY + "\" entry: \"engine\"
must be a name "
+ + "from \"engines\"");
+ }
+ String engineName = nameNode.asText();
+ EngineRegistry engines = context.get(EngineRegistry.class);
+ Engine engine = engines == null ? null : engines.get(engineName);
+ if (engine == null) {
+ throw new TikaConfigException("\"" + KEY + "\" entry names engine
\"" + engineName
+ + "\", which is not in \"engines\"");
+ }
+ if (!(engine instanceof Parser parser) ||
!ContentEnrichers.isEnricher(parser)) {
+ throw new TikaConfigException("\"" + KEY + "\" entry names engine
\"" + engineName
+ + "\" (" + engine.getClass().getName() + "), which is not
a text "
+ + "recognizer: a recognizer implements ContentEnricher");
+ }
+ return ComponentInstantiator.withMimeFilters(parser, item);
+ }
+
+ /** The 4.0 form: {@code "name"} or {@code {"name": {...}}}, first field
only. */
+ private static Map.Entry<String, JsonNode> inline(JsonNode item,
LoaderContext context)
+ throws TikaConfigException {
+ if (item.isTextual()) {
+ return Map.entry(item.asText(), (JsonNode)
context.getObjectMapper().createObjectNode());
+ }
+ if (item.isObject() && !item.isEmpty()) {
+ Map.Entry<String, JsonNode> first = item.fields().next();
+ return Map.entry(first.getKey(), first.getValue());
+ }
+ throw new TikaConfigException("\"" + KEY + "\" entries are
{\"engine\": \"<name>\"} "
+ + "or {\"<recognizer>\": {...}}; got " + item);
+ }
+
+ private static Parser instantiate(Map.Entry<String, JsonNode> entry,
LoaderContext context)
+ throws TikaConfigException {
+ try {
+ ObjectNode wrapper = context.getObjectMapper().createObjectNode();
+ wrapper.set(entry.getKey(), entry.getValue());
+ return context.getObjectMapper().treeToValue(wrapper,
Parser.class);
+ } catch (Exception e) {
+ throw new TikaConfigException("Failed to load text recognizer: " +
entry.getKey(), e);
+ }
+ }
+
private static Parser unwrap(Parser parser) {
while (parser instanceof ParserDecorator decorator) {
parser = decorator.getWrappedParser();
diff --git
a/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaLoader.java
b/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaLoader.java
index 8cf2b44c91..13d9d55bc6 100644
---
a/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaLoader.java
+++
b/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaLoader.java
@@ -842,6 +842,12 @@ public class TikaLoader {
if (config.hasComponentSection("text-recognizers")) {
output.set("text-recognizers",
config.getRootNode().get("text-recognizers"));
}
+ // a text-recognizers entry may name an engine, so both travel with it
+ for (String section : new String[]{EngineLoader.KEY,
InferenceLoader.KEY}) {
+ if (config.hasComponentSection(section)) {
+ output.set(section, config.getRootNode().get(section));
+ }
+ }
// Preserve auto-detect-parser config if present
JsonNode adpNode = config.getRootNode().get("auto-detect-parser");
diff --git
a/tika-serialization/src/test/java/org/apache/tika/config/loader/ContentEnricherLoaderTest.java
b/tika-serialization/src/test/java/org/apache/tika/config/loader/ContentEnricherLoaderTest.java
index 1136b5ad07..8b0c5ae05a 100644
---
a/tika-serialization/src/test/java/org/apache/tika/config/loader/ContentEnricherLoaderTest.java
+++
b/tika-serialization/src/test/java/org/apache/tika/config/loader/ContentEnricherLoaderTest.java
@@ -19,17 +19,22 @@ package org.apache.tika.config.loader;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.xml.sax.helpers.DefaultHandler;
+import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.HttpHeaders;
import org.apache.tika.metadata.Metadata;
@@ -37,8 +42,10 @@ import org.apache.tika.mime.MediaType;
import org.apache.tika.parser.CompositeParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.Parser;
+import org.apache.tika.parser.ParserDecorator;
import org.apache.tika.parser.enricher.CompositeContentEnricher;
import org.apache.tika.parser.enricher.ContentEnrichers;
+import org.apache.tika.parser.inference.EngineRegistry;
import org.apache.tika.utils.ParserUtils;
public class ContentEnricherLoaderTest {
@@ -82,6 +89,46 @@ public class ContentEnricherLoaderTest {
assertEquals(enrichers, enrichingParser.getContentEnrichers());
}
+ /** An entry names an engine configured once under "engines"; one instance
serves both. */
+ @Test
+ public void testEntryNamesAnEngine() throws Exception {
+ TikaLoader loader = load("""
+ {
+ "engines": { "png": { "mock-enricher": {} } },
+ "text-recognizers": [ { "engine": "png", "_mime-include":
["image/png"] } ]
+ }
+ """);
+ CompositeContentEnricher enrichers =
loader.get(CompositeContentEnricher.class);
+ List<Parser> matched = enrichers.getEnrichers(MediaType.image("png"));
+ assertEquals(1, matched.size());
+ Parser entry = matched.get(0);
+ assertTrue(entry instanceof ParserDecorator, "the entry's mime filter
wraps the engine");
+ assertSame(loader.get(EngineRegistry.class).get("png"), unwrap(entry));
+ assertTrue(ContentEnrichers.isEnricher(entry));
+ }
+
+ @ParameterizedTest
+ @CsvSource(delimiter = '|', value = {
+ "{ \"engines\": { \"png\": { \"mock-enricher\": {} } },
\"text-recognizers\": [ { \"engine\": \"nope\" } ] }| is not in \"engines\"",
+ "{ \"text-recognizers\": [ { \"engine\": \"png\" } ] }| is not in
\"engines\"",
+ "{ \"engines\": { \"plain\": { \"test-engine\": {} } },
\"text-recognizers\": [ { \"engine\": \"plain\" } ] }| is not a text
recognizer",
+ "{ \"engines\": { \"png\": { \"mock-enricher\": {} } },
\"text-recognizers\": [ { \"engine\": \"png\", \"language\": \"eng\" } ] }|
unknown key \"language\"",
+ "{ \"engines\": { \"png\": { \"mock-enricher\": {} } },
\"text-recognizers\": [ { \"engine\": 3 } ] }| must be a name",
+ "{ \"text-recognizers\": [ 3 ] }| entries are"
+ })
+ public void testEngineReferenceMisconfigurationsFailLoad(String json,
String message) {
+ TikaConfigException e = assertThrows(TikaConfigException.class,
+ () -> load(json).get(CompositeContentEnricher.class));
+ assertTrue(e.getMessage().contains(message), e.getMessage());
+ }
+
+ private static Parser unwrap(Parser parser) {
+ while (parser instanceof ParserDecorator decorator) {
+ parser = decorator.getWrappedParser();
+ }
+ return parser;
+ }
+
@Test
public void testZeroTypeEnricherFailsLoad() throws Exception {
// a named engine that cannot run must fail load, not become a silent
no-op
diff --git
a/tika-serialization/src/test/java/org/apache/tika/config/loader/InferenceLoaderTest.java
b/tika-serialization/src/test/java/org/apache/tika/config/loader/InferenceLoaderTest.java
index 167e16d39f..04c0c0c80b 100644
---
a/tika-serialization/src/test/java/org/apache/tika/config/loader/InferenceLoaderTest.java
+++
b/tika-serialization/src/test/java/org/apache/tika/config/loader/InferenceLoaderTest.java
@@ -90,6 +90,16 @@ public class InferenceLoaderTest {
"the dispatcher rides every parse as a hook");
}
+ /** A recognizer is an engine a binding may name; the task decides whether
it fits. */
+ @Test
+ public void testRecognizerEngineIsValidatedByTheTask() {
+ TikaConfigException e = assertThrows(TikaConfigException.class, () ->
load("{"
+ + " \"engines\": { \"ocr\": { \"mock-enricher\": {} } },"
+ + " \"inference\": [ { \"engine\": \"ocr\", \"input\":
\"IMAGES\","
+ + " \"tasks\": [\"test-task\"] } ]
}").get(InferenceDispatcher.class));
+ assertTrue(e.getMessage().contains("test-task needs a test-engine"),
e.getMessage());
+ }
+
@Test
public void testPagesBindingLoads() throws Exception {
TikaLoader loader = load("{" + ENGINES + ", \"inference\": ["
diff --git
a/tika-server/tika-server-standard/src/test/resources/configs/tika-config-text-recognizers.json
b/tika-server/tika-server-standard/src/test/resources/configs/tika-config-text-recognizers.json
index c89790d11a..0b84b3e60c 100644
---
a/tika-server/tika-server-standard/src/test/resources/configs/tika-config-text-recognizers.json
+++
b/tika-server/tika-server-standard/src/test/resources/configs/tika-config-text-recognizers.json
@@ -1,5 +1,6 @@
{
- "text-recognizers": [ { "mock-enricher": {} } ],
+ "engines": { "mock": { "mock-enricher": {} } },
+ "text-recognizers": [ { "engine": "mock" } ],
"auto-detect-parser": {
"throwOnZeroBytes": false
},