This is an automated email from the ASF dual-hosted git repository. tballison pushed a commit to branch TIKA-4872 in repository https://gitbox.apache.org/repos/asf/tika.git
commit 8c20152d6a996d359025b8d7678f9a933554f2b7 Author: tallison <[email protected]> AuthorDate: Wed Sep 2 15:43:11 2026 -0400 TIKA-4872: review fixes --- CHANGES.txt | 11 ++- docs/modules/ROOT/pages/configuration/index.adoc | 5 +- .../parser/enricher/CompositeContentEnricher.java | 8 +- .../tika/parser/enricher/ContentEnrichers.java | 56 +++++++++---- .../parser/enricher/LegacyDispatchEnricher.java | 17 ++-- .../tika/parser/enricher/ContentEnrichersTest.java | 92 ++++++++++++++++++++++ .../tika/parser/image/AbstractImageParser.java | 5 -- .../apache/tika/parser/pdf/AbstractPDF2XHTML.java | 6 +- .../tika/config/loader/ContentEnricherLoader.java | 15 +++- .../apache/tika/config/loader/ParserLoader.java | 8 +- .../config/loader/ContentEnricherLoaderTest.java | 17 ++++ .../config/loader/TestUnavailableEnricher.java | 49 ++++++++++++ 12 files changed, 249 insertions(+), 40 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 782bf4700d..fb54122b1e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -12,9 +12,14 @@ Release 4.1.0 - unreleased Every enricher matching a media type runs, in config order (e.g. an OCR engine then a VLM tagger for the same image), best-effort: one enricher's failure does not stop the others and is still reported; - timeouts abort the chain. With no "content-enrichers" configured, the - legacy ocr-* dispatch applies unchanged; a WARN at config load now - names colliding OCR engines and the winner. TesseractOCRParser's + timeouts abort the chain. The list is authoritative: a media type no + configured enricher matches gets no enrichment -- never a classpath + engine that was not named -- and a named engine that reports no media + types at load (missing binary, unreachable inference server) fails + config load instead of going silently inert. With no + "content-enrichers" configured, the legacy ocr-* dispatch applies + unchanged; a WARN at config load now names colliding OCR engines and + the winner. TesseractOCRParser's component name is pinned as "tesseract-ocr-parser". * Inference/OCR hardening (TIKA-4871): OpenAIVLMParser no longer diff --git a/docs/modules/ROOT/pages/configuration/index.adoc b/docs/modules/ROOT/pages/configuration/index.adoc index c2804aa51d..8a1c25f468 100644 --- a/docs/modules/ROOT/pages/configuration/index.adoc +++ b/docs/modules/ROOT/pages/configuration/index.adoc @@ -146,7 +146,10 @@ parser registered for those types: `image-parser` still parses the image and cal (The bundled OCR engines still advertise legacy `image/ocr-*` types; those are mapped to the real type, so naming them here just works.) *Every* enricher matching a media type runs, in the order listed — so an OCR engine followed by a VLM that tags images is two entries, both invoked -per image. Failures are best-effort: one enricher failing does not stop the others, and every +per image. The list is authoritative: a media type no configured enricher matches gets no +enrichment — never a classpath engine you did not name — and a named engine that reports no +media types at startup (missing native binary, unreachable inference server) fails config load +rather than going silently inert. Failures are best-effort: one enricher failing does not stop the others, and every failure is still reported through the parser's normal exception handling (timeouts abort the chain immediately). With no `content-enrichers` configured, behavior is unchanged — whichever OCR engine is on the classpath is used, exactly as before, and a WARN is logged at startup when diff --git a/tika-core/src/main/java/org/apache/tika/parser/enricher/CompositeContentEnricher.java b/tika-core/src/main/java/org/apache/tika/parser/enricher/CompositeContentEnricher.java index db614d3118..767ab9d3cf 100644 --- a/tika-core/src/main/java/org/apache/tika/parser/enricher/CompositeContentEnricher.java +++ b/tika-core/src/main/java/org/apache/tika/parser/enricher/CompositeContentEnricher.java @@ -63,7 +63,7 @@ public class CompositeContentEnricher implements Serializable { for (MediaType mediaType : enricher.getSupportedTypes(empty)) { // legacy engines (Tesseract, VLM, ...) still advertise the image/ocr-* // pseudo-types; key them under the real type so they are nameable here - MediaType keyType = stripLegacyOcrPrefix(mediaType); + MediaType keyType = stripLegacyOcrPrefix(mediaType.getBaseType()); List<Parser> forType = tmp.computeIfAbsent(keyType, k -> new ArrayList<>()); if (!forType.contains(enricher)) { forType.add(enricher); @@ -84,11 +84,11 @@ public class CompositeContentEnricher implements Serializable { } /** - * @return the enrichers configured for this exact media type, in config order; - * empty when none + * @return the enrichers configured for this media type (parameters ignored; alias + * normalization is the caller's job), in config order; empty when none */ public List<Parser> getEnrichers(MediaType mediaType) { - List<Parser> enrichers = enricherMap.get(mediaType); + List<Parser> enrichers = enricherMap.get(mediaType.getBaseType()); return enrichers == null ? Collections.emptyList() : enrichers; } diff --git a/tika-core/src/main/java/org/apache/tika/parser/enricher/ContentEnrichers.java b/tika-core/src/main/java/org/apache/tika/parser/enricher/ContentEnrichers.java index f2177c75e0..81abc69863 100644 --- a/tika-core/src/main/java/org/apache/tika/parser/enricher/ContentEnrichers.java +++ b/tika-core/src/main/java/org/apache/tika/parser/enricher/ContentEnrichers.java @@ -28,6 +28,7 @@ import org.apache.tika.exception.TikaException; import org.apache.tika.exception.TikaTimeoutException; import org.apache.tika.extractor.EmbeddedDocumentUtil; import org.apache.tika.io.TikaInputStream; +import org.apache.tika.metadata.HttpHeaders; import org.apache.tika.metadata.Metadata; import org.apache.tika.mime.MediaType; import org.apache.tika.parser.ParseContext; @@ -48,6 +49,9 @@ import org.apache.tika.parser.Parser; * must still fire the enricher selected for the type it was dispatched on).</li> * <li>The enricher writes into the caller's {@link Metadata}; the caller must not assume * the metadata is untouched beyond the derived content.</li> + * <li>An enricher that re-enters parsing must propagate the caller's + * {@link ParseContext}: the recursion guard (like Tika's other in-parse limits) + * rides the context, so a fresh context defeats it.</li> * </ul> * * @since Apache Tika 4.1 @@ -59,12 +63,14 @@ public final class ContentEnrichers { /** * Returns the enricher to invoke for one media type, or null when none applies. - * Explicitly configured enrichers win: every one matching the type runs, in config - * order, behind the single Parser returned here. Otherwise the legacy - * {@code image/ocr-*} dispatch through the composite parser applies when an engine - * claims the synthetic type. Returns null while a enrichment is already in progress - * in this context, so an enricher that is (or invokes) a container parser cannot - * recurse into enrichment. + * A configured {@code "content-enrichers"} list is authoritative: every configured + * enricher matching the type runs, in config order, behind the single Parser + * returned here, and a type no configured enricher matches gets no enrichment -- + * never a classpath engine the user did not name. Only when no list is configured + * at all does the legacy {@code image/ocr-*} dispatch through the composite parser + * apply. Returns null while an enrichment is already in progress in this context, + * so an enricher that is (or invokes) a container parser cannot recurse into + * enrichment. * * @param enrichers the injected composite; may be null when none is configured * @param mediaType the real, normalized media type of the bytes; may be null @@ -81,15 +87,16 @@ public final class ContentEnrichers { } if (enrichers != null) { List<Parser> matched = enrichers.getEnrichers(mediaType); - if (!matched.isEmpty()) { - return new GuardedEnricher(matched.size() == 1 - ? matched.get(0) : new SequentialEnricher(matched)); + if (matched.isEmpty()) { + return null; } + return new GuardedEnricher(matched.size() == 1 + ? matched.get(0) : new SequentialEnricher(matched)); } Parser composite = EmbeddedDocumentUtil.getStatelessParser(context); if (composite != null && composite.getSupportedTypes(context) .contains(LegacyDispatchEnricher.toOcrMediaType(mediaType))) { - return new GuardedEnricher(new LegacyDispatchEnricher(mediaType)); + return new GuardedEnricher(new LegacyDispatchEnricher(mediaType, composite)); } return null; } @@ -98,9 +105,10 @@ public final class ContentEnrichers { * Runs each enricher in config order, best-effort: one enricher's failure does not * stop the others. The first failure is rethrown after the chain completes, with * later failures attached as suppressed, so call sites report every failure through - * their existing exception handling. Timeouts, SecurityException and SAXException - * (incl. write-limit aborts) propagate immediately -- a spent budget or a suspect - * handler must not fund further enrichments. + * their existing exception handling. Timeouts, SecurityException, SAXException + * (incl. write-limit aborts) and other runtime exceptions propagate immediately -- + * a spent budget or a suspect handler must not fund further enrichments -- with any + * earlier recorded failure attached as suppressed. */ private static final class SequentialEnricher implements Parser { @@ -137,6 +145,11 @@ public final class ContentEnrichers { } else { first.addSuppressed(e); } + } catch (RuntimeException e) { + if (first != null) { + e.addSuppressed(first); + } + throw e; } } if (first instanceof IOException e) { @@ -153,7 +166,11 @@ public final class ContentEnrichers { boolean active; } - /** Marks enrichment in progress around the delegate so {@link #get} refuses re-entry. */ + /** + * Marks enrichment in progress around the delegate so {@link #get} refuses re-entry, + * and restores Content-Type afterwards: an enricher derives content, it does not get + * to re-type the caller's document. + */ private static final class GuardedEnricher implements Parser { private static final long serialVersionUID = 1L; @@ -177,11 +194,20 @@ public final class ContentEnrichers { active = new ActiveEnrichment(); context.set(ActiveEnrichment.class, active); } + String contentType = metadata.get(HttpHeaders.CONTENT_TYPE); + // restore rather than clear: a nested invocation must not strip the + // outer enrichment's re-entry protection when it completes + boolean wasActive = active.active; active.active = true; try { delegate.parse(tis, handler, metadata, context); } finally { - active.active = false; + active.active = wasActive; + if (contentType == null) { + metadata.remove(HttpHeaders.CONTENT_TYPE); + } else { + metadata.set(HttpHeaders.CONTENT_TYPE, contentType); + } } } } diff --git a/tika-core/src/main/java/org/apache/tika/parser/enricher/LegacyDispatchEnricher.java b/tika-core/src/main/java/org/apache/tika/parser/enricher/LegacyDispatchEnricher.java index 7b40938fb5..f880250e2a 100644 --- a/tika-core/src/main/java/org/apache/tika/parser/enricher/LegacyDispatchEnricher.java +++ b/tika-core/src/main/java/org/apache/tika/parser/enricher/LegacyDispatchEnricher.java @@ -24,7 +24,6 @@ import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import org.apache.tika.exception.TikaException; -import org.apache.tika.extractor.EmbeddedDocumentUtil; import org.apache.tika.io.TikaInputStream; import org.apache.tika.metadata.HttpHeaders; import org.apache.tika.metadata.Metadata; @@ -34,8 +33,8 @@ import org.apache.tika.parser.ParseContext; import org.apache.tika.parser.Parser; /** - * Reproduces the pre-4.1 {@code image/ocr-*} dispatch when no enricher is configured for - * a media type: mints the synthetic {@code ocr-} media type, sets + * Reproduces the pre-4.1 {@code image/ocr-*} dispatch when no {@code "content-enrichers"} + * list is configured: mints the synthetic {@code ocr-} media type, sets * {@link TikaCoreProperties#CONTENT_TYPE_PARSER_OVERRIDE} and re-enters the composite * parser, restoring the metadata afterwards. Whichever engine won the {@code ocr-*} * registration in the composite still wins here, so precedence-by-presence (adding @@ -55,11 +54,17 @@ public class LegacyDispatchEnricher implements Parser { private final MediaType mediaType; + private final Parser composite; + /** * @param mediaType the real (already normalized) media type of the bytes to derive from + * @param composite the composite parser to re-enter; the caller has already verified + * it claims the synthetic {@code ocr-} type (re-verifying here would + * rebuild the composite's full supported-types map per invocation) */ - public LegacyDispatchEnricher(MediaType mediaType) { + public LegacyDispatchEnricher(MediaType mediaType, Parser composite) { this.mediaType = mediaType; + this.composite = composite; } /** @@ -81,9 +86,7 @@ public class LegacyDispatchEnricher implements Parser { public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata, ParseContext context) throws IOException, SAXException, TikaException { MediaType ocrMediaType = toOcrMediaType(mediaType); - Parser composite = EmbeddedDocumentUtil.getStatelessParser(context); - if (composite == null - || !composite.getSupportedTypes(context).contains(ocrMediaType)) { + if (composite == null) { throw new TikaException("No parser is registered for " + ocrMediaType); } String originalOverride = metadata.get(TikaCoreProperties.CONTENT_TYPE_PARSER_OVERRIDE); diff --git a/tika-core/src/test/java/org/apache/tika/parser/enricher/ContentEnrichersTest.java b/tika-core/src/test/java/org/apache/tika/parser/enricher/ContentEnrichersTest.java index 8f2dbdc22f..884a9972b5 100644 --- a/tika-core/src/test/java/org/apache/tika/parser/enricher/ContentEnrichersTest.java +++ b/tika-core/src/test/java/org/apache/tika/parser/enricher/ContentEnrichersTest.java @@ -122,6 +122,98 @@ public class ContentEnrichersTest { assertNull(ContentEnrichers.get(null, null, context)); } + @Test + public void testConfiguredListIsAuthoritative() throws Exception { + // the composite claims ocr-tiff, but a configured list that doesn't cover tiff + // must yield no enricher -- never a classpath engine the user did not name + RecordingParser explicit = new RecordingParser(Collections.singleton(PNG)); + RecordingParser composite = + new RecordingParser(Collections.singleton(MediaType.image("ocr-tiff"))); + CompositeContentEnricher enrichers = new CompositeContentEnricher(List.of(explicit)); + ParseContext context = new ParseContext(); + context.set(Parser.class, composite); + + assertNull(ContentEnrichers.get(enrichers, MediaType.image("tiff"), context)); + // with no list configured, the same composite is reachable via legacy dispatch + assertNotNull(ContentEnrichers.get(null, MediaType.image("tiff"), context)); + } + + @Test + public void testParametersIgnoredInMatching() throws Exception { + RecordingParser explicit = new RecordingParser(Collections.singleton(PNG)); + CompositeContentEnricher enrichers = new CompositeContentEnricher(List.of(explicit)); + ParseContext context = new ParseContext(); + + Parser enricher = ContentEnrichers.get(enrichers, + MediaType.parse("image/png; charset=binary"), context); + assertNotNull(enricher, "parameterized type must match the base-type registration"); + invoke(enricher, new Metadata(), context); + assertEquals(1, explicit.calls); + } + + @Test + public void testEnricherCannotRewriteContentType() throws Exception { + Parser rewriting = new Parser() { + private static final long serialVersionUID = 1L; + + @Override + public Set<MediaType> getSupportedTypes(ParseContext context) { + return Collections.singleton(PNG); + } + + @Override + public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata, + ParseContext context) { + metadata.set(HttpHeaders.CONTENT_TYPE, "application/pdf"); + } + }; + CompositeContentEnricher enrichers = new CompositeContentEnricher(List.of(rewriting)); + ParseContext context = new ParseContext(); + + Metadata metadata = new Metadata(); + metadata.set(HttpHeaders.CONTENT_TYPE, PNG.toString()); + invoke(ContentEnrichers.get(enrichers, PNG, context), metadata, context); + assertEquals(PNG.toString(), metadata.get(HttpHeaders.CONTENT_TYPE)); + + Metadata unset = new Metadata(); + invoke(ContentEnrichers.get(enrichers, PNG, context), unset, context); + assertNull(unset.get(HttpHeaders.CONTENT_TYPE)); + } + + @Test + public void testRuntimeFailureAbortsChainWithEarlierFailureSuppressed() throws Exception { + List<String> order = new java.util.ArrayList<>(); + Parser failing = namedEnricher("failing", order, true); + Parser blowingUp = new Parser() { + private static final long serialVersionUID = 1L; + + @Override + public Set<MediaType> getSupportedTypes(ParseContext context) { + return Collections.singleton(PNG); + } + + @Override + public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata, + ParseContext context) { + order.add("blowingUp"); + throw new NullPointerException("boom"); + } + }; + Parser third = namedEnricher("third", order, false); + CompositeContentEnricher enrichers = + new CompositeContentEnricher(List.of(failing, blowingUp, third)); + ParseContext context = new ParseContext(); + + Parser enricher = ContentEnrichers.get(enrichers, PNG, context); + assertNotNull(enricher); + NullPointerException thrown = org.junit.jupiter.api.Assertions.assertThrows( + NullPointerException.class, () -> invoke(enricher, new Metadata(), context)); + assertEquals(List.of("failing", "blowingUp"), order); + // the recorded checked failure rides along instead of vanishing + assertEquals(1, thrown.getSuppressed().length); + assertEquals("failing failed", thrown.getSuppressed()[0].getMessage()); + } + @Test public void testAllMatchingEnrichersRunInOrder() throws Exception { List<String> order = new java.util.ArrayList<>(); diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/AbstractImageParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/AbstractImageParser.java index 2e53f2a2c6..76f9af40e0 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/AbstractImageParser.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/AbstractImageParser.java @@ -34,17 +34,12 @@ import org.apache.tika.parser.Parser; import org.apache.tika.parser.enricher.CompositeContentEnricher; import org.apache.tika.parser.enricher.ContentEnrichers; import org.apache.tika.parser.enricher.EnrichingParser; -import org.apache.tika.parser.enricher.LegacyDispatchEnricher; import org.apache.tika.sax.BodyContentHandler; import org.apache.tika.sax.EmbeddedContentHandler; import org.apache.tika.sax.XHTMLContentHandler; public abstract class AbstractImageParser implements Parser, EnrichingParser { - /** @deprecated use {@link LegacyDispatchEnricher#OCR_MEDIATYPE_PREFIX} */ - @Deprecated - public static String OCR_MEDIATYPE_PREFIX = LegacyDispatchEnricher.OCR_MEDIATYPE_PREFIX; - private CompositeContentEnricher contentEnrichers; abstract void extractMetadata(InputStream is, ContentHandler contentHandler, Metadata metadata, diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/AbstractPDF2XHTML.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/AbstractPDF2XHTML.java index b7cde47b4a..7502d29c16 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/AbstractPDF2XHTML.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/AbstractPDF2XHTML.java @@ -576,8 +576,10 @@ class AbstractPDF2XHTML extends PDFTextStripper { if (ocrStrategy == OCR_ONLY || ocrStrategy == OCR_AND_TEXT_EXTRACTION) { throw new TikaException( "I regret that I couldn't find an OCR engine to handle " + - imageMediaType + ". Configure one in \"content-enrichers\", " + - "add one to the classpath, or set the OCR strategy to NO_OCR."); + imageMediaType + ". Name one that covers it in " + + "\"content-enrichers\" (a configured list is authoritative), " + + "add one to the classpath when no list is configured, " + + "or set the OCR strategy to NO_OCR."); } else if (ocrStrategy == AUTO) { //silently skip if there's no engine to run ocr return; 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 56330a790f..98aeb535cf 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 @@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.tika.exception.TikaConfigException; +import org.apache.tika.parser.ParseContext; import org.apache.tika.parser.Parser; import org.apache.tika.parser.enricher.CompositeContentEnricher; @@ -43,15 +44,27 @@ class ContentEnricherLoader implements ComponentLoader<CompositeContentEnricher> return null; } List<Parser> enrichers = new ArrayList<>(); + ParseContext empty = new ParseContext(); for (Map.Entry<String, JsonNode> entry : entries) { + Parser enricher; try { ObjectNode wrapper = context.getObjectMapper().createObjectNode(); wrapper.set(entry.getKey(), entry.getValue()); - enrichers.add(context.getObjectMapper().treeToValue(wrapper, Parser.class)); + enricher = context.getObjectMapper().treeToValue(wrapper, Parser.class); } catch (Exception e) { throw new TikaConfigException( "Failed to load content enricher: " + entry.getKey(), e); } + // engines report no types when unusable (missing binary, unreachable server); + // the media-type snapshot taken here lasts the life of the process, so an + // explicitly named engine that can never run must fail load, not go silent + if (enricher.getSupportedTypes(empty).isEmpty()) { + throw new TikaConfigException("Content enricher \"" + entry.getKey() + + "\" advertises no media types. Is the engine unavailable " + + "(missing native binary, unreachable inference server) or " + + "configured to skip enrichment?"); + } + enrichers.add(enricher); } return new CompositeContentEnricher(enrichers); } diff --git a/tika-serialization/src/main/java/org/apache/tika/config/loader/ParserLoader.java b/tika-serialization/src/main/java/org/apache/tika/config/loader/ParserLoader.java index 5e44cf5cf7..9fb1cdbf64 100644 --- a/tika-serialization/src/main/java/org/apache/tika/config/loader/ParserLoader.java +++ b/tika-serialization/src/main/java/org/apache/tika/config/loader/ParserLoader.java @@ -138,7 +138,9 @@ public class ParserLoader extends AbstractSpiComponentLoader<Parser> { Renderer renderer = context.getRenderer(); CompositeContentEnricher contentEnrichers = context.getContentEnrichers(); injectDependenciesRecursively(parser, encodingDetector, renderer, contentEnrichers); - warnOnAmbiguousOcrRegistrations(parser); + if (contentEnrichers == null) { + warnOnAmbiguousOcrRegistrations(parser); + } return parser; } @@ -171,7 +173,9 @@ public class ParserLoader extends AbstractSpiComponentLoader<Parser> { * The image/ocr-* pseudo-types are claimed by several OCR engines whose availability * is environmental, and the composite resolves a collision by last registration with * no warning. Name the collision and the winner once at load so engine selection is - * debuggable; select an engine explicitly with "content-enrichers". + * debuggable; select an engine explicitly with "content-enrichers". Skipped when + * content-enrichers is configured: the list is authoritative and legacy ocr-* + * dispatch never runs, so the collision is moot and the advice already taken. */ private void warnOnAmbiguousOcrRegistrations(Parser parser) { if (!(parser instanceof CompositeParser cp)) { 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 46fcfeff84..372987f348 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 @@ -67,6 +67,23 @@ public class ContentEnricherLoaderTest { assertEquals(enrichers, enrichingParser.getContentEnrichers()); } + @Test + public void testZeroTypeEnricherFailsLoad() throws Exception { + // an explicitly named engine that cannot run (missing binary, unreachable + // server) must fail config load, not become a silent no-op + TikaLoader loader = load(""" + { + "content-enrichers": [ {"test-unavailable-enricher": {}} ] + } + """); + org.apache.tika.exception.TikaConfigException e = + org.junit.jupiter.api.Assertions.assertThrows( + org.apache.tika.exception.TikaConfigException.class, + () -> loader.get(CompositeContentEnricher.class)); + assertTrue(e.getMessage().contains("advertises no media types"), + "unexpected message: " + e.getMessage()); + } + @Test public void testNoContentEnrichersConfigured() throws Exception { TikaLoader loader = load(""" diff --git a/tika-serialization/src/test/java/org/apache/tika/config/loader/TestUnavailableEnricher.java b/tika-serialization/src/test/java/org/apache/tika/config/loader/TestUnavailableEnricher.java new file mode 100644 index 0000000000..4f3f28d02e --- /dev/null +++ b/tika-serialization/src/test/java/org/apache/tika/config/loader/TestUnavailableEnricher.java @@ -0,0 +1,49 @@ +/* + * 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.config.loader; + +import java.util.Collections; +import java.util.Set; + +import org.xml.sax.ContentHandler; + +import org.apache.tika.annotation.TikaComponent; +import org.apache.tika.io.TikaInputStream; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.mime.MediaType; +import org.apache.tika.parser.ParseContext; +import org.apache.tika.parser.Parser; + +/** + * Content-enricher fixture mimicking an engine that is unusable at load time + * (missing binary, unreachable server): it advertises no media types. + */ +@TikaComponent(name = "test-unavailable-enricher", spi = false) +public class TestUnavailableEnricher implements Parser { + + private static final long serialVersionUID = 1L; + + @Override + public Set<MediaType> getSupportedTypes(ParseContext context) { + return Collections.emptySet(); + } + + @Override + public void parse(TikaInputStream stream, ContentHandler handler, Metadata metadata, + ParseContext context) { + } +}
