ContentEnricherLoaderTest.java
/*
* 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 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;
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 {
@TempDir
Path tmp;
private TikaLoader load(String json) throws Exception {
Path config = tmp.resolve("tika-config.json");
Files.writeString(config, json);
return TikaLoader.load(config);
}
private static Metadata target(MediaType type) {
Metadata target = new Metadata();
target.set(HttpHeaders.CONTENT_TYPE, type.toString());
return target;
}
@Test
public void testContentEnrichersLoadAndInject() throws Exception {
TikaLoader loader = load("""
{
"parsers": [ {"enriching-test-parser": {}} ],
"text-recognizers": [ {"test-png-enricher": {}} ]
}
""");
CompositeContentEnricher enrichers = loader.get(CompositeContentEnricher.class);
assertNotNull(enrichers);
java.util.List<Parser> matched = enrichers.getEnrichers(MediaType.image("png"));
assertEquals(1, matched.size());
assertTrue(matched.get(0) instanceof TestPngEnricher,
"expected TestPngEnricher, got " + matched.get(0));
assertEquals(1, enrichers.getSupportedTypes().size());
EnrichingTestParser enrichingParser = findEnrichingParser(loader.get(Parser.class));
assertNotNull(enrichingParser, "enriching-test-parser not found in loaded parsers");
assertNotNull(enrichingParser.getContentEnrichers(),
"content enrichers were not injected into the EnrichingParser");
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
TikaLoader loader = load("""
{
"text-recognizers": [ {"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 testMimeExcludeReachesLegacyPseudoType() throws Exception {
TikaLoader loader = load("""
{
"text-recognizers": [
{"test-legacy-ocr-enricher": {"_mime-exclude": ["image/tiff"]}}
]
}
""");
CompositeContentEnricher enrichers = loader.get(CompositeContentEnricher.class);
assertEquals(java.util.Set.of(MediaType.image("png")), enrichers.getSupportedTypes());
assertTrue(enrichers.getEnrichers(MediaType.image("tiff")).isEmpty());
assertEquals(1, enrichers.getEnrichers(MediaType.image("png")).size());
}
/** The pseudo-type spelling of a filter fails load with the real type in the message. */
@ParameterizedTest
@ValueSource(strings = {
"{\"text-recognizers\": [{\"test-legacy-ocr-enricher\": {\"_mime-exclude\": [\"image/ocr-tiff\"]}}]}",
"{\"text-recognizers\": [{\"test-png-enricher\": {\"_mime-include\": [\"image/ocr-tiff\"]}}]}",
"{\"parsers\": [{\"enriching-test-parser\": {\"_mime-exclude\": [\"image/ocr-tiff\"]}}]}",
"{\"parsers\": [{\"default-parser\": {\"_mime-exclude\": [\"image/ocr-tiff\"]}}]}"})
public void testLegacyPseudoTypeInFilterFailsLoad(String json) throws Exception {
TikaLoader loader = load(json);
org.apache.tika.exception.TikaConfigException e =
org.junit.jupiter.api.Assertions.assertThrows(
org.apache.tika.exception.TikaConfigException.class, () -> {
loader.get(CompositeContentEnricher.class);
loader.get(Parser.class);
});
String message = e.getMessage() + (e.getCause() == null ? "" : e.getCause().getMessage());
assertTrue(message.contains("image/ocr-tiff") && message.contains("\"image/tiff\""),
"unexpected message: " + message);
}
/**
* A parser configured before default-parser keeps its type over an SPI enricher inside
* default-parser, which is still discovered (TIKA-4884).
*/
@Test
public void testConfiguredParserKeepsTypeOverSpiEnricher() throws Exception {
TikaLoader loader = load("""
{
"parsers": [ {"minimal-test-parser": {}}, {"default-parser": {}} ]
}
""");
MediaType type = MediaType.parse("application/test+minimal");
CompositeParser parsers = (CompositeParser) loader.loadParsers();
ParseContext context = new ParseContext();
assertEquals(MinimalTestParser.class.getName(),
ParserUtils.getParserClassname(parsers.getParsers(context).get(type)));
context.set(Parser.class, parsers);
Parser enricher = ContentEnrichers.get(null, type, target(type), context);
assertNotNull(enricher, "the SPI enricher is still discovered for the type");
Metadata metadata = new Metadata();
try (TikaInputStream tis = TikaInputStream.get(new byte[0])) {
enricher.parse(tis, new DefaultHandler(), metadata, context);
}
assertEquals("test-spi-enricher", metadata.get("derived-by"));
}
@Test
public void testMimeIncludeDoesNotMaskUnavailableEngine() throws Exception {
TikaLoader loader = load("""
{
"text-recognizers": [
{"test-unavailable-enricher": {"_mime-include": ["image/png"]}}
]
}
""");
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 testMimeIncludeNarrowsLegacyEngine() throws Exception {
TikaLoader loader = load("""
{
"text-recognizers": [
{"test-legacy-ocr-enricher": {"_mime-include": ["image/png"]}}
]
}
""");
CompositeContentEnricher enrichers = loader.get(CompositeContentEnricher.class);
assertEquals(java.util.Set.of(MediaType.image("png")), enrichers.getSupportedTypes());
}
/** With no list, the loader resolves one engine per type from the loaded parsers. */
@Test
public void testAbsentListResolvesFromLoadedParsers() throws Exception {
TikaLoader loader = load("""
{
"parsers": [ {"enriching-test-parser": {}}, {"default-parser": {}} ]
}
""");
assertNull(loader.get(CompositeContentEnricher.class), "nothing was configured");
EnrichingTestParser enrichingParser = findEnrichingParser(loader.get(Parser.class));
assertNotNull(enrichingParser);
CompositeContentEnricher resolved = enrichingParser.getContentEnrichers();
assertNotNull(resolved, "the resolved composite is injected");
MediaType type = MediaType.parse("application/test+minimal");
assertEquals(1, resolved.getEnrichers(type).size());
assertTrue(resolved.getEnrichers(type).get(0) instanceof TestSpiEnricher);
loader = load("""
{
"parsers": [ {"enriching-test-parser": {}} ]
}
""");
enrichingParser = findEnrichingParser(loader.get(Parser.class));
assertNotNull(enrichingParser.getContentEnrichers());
assertTrue(enrichingParser.getContentEnrichers().isEmpty(),
"no enricher among the loaded parsers: empty, not null");
}
/** An empty list is an explicit off switch, unlike an absent key. */
@Test
public void testEmptyListDisablesEnrichment() throws Exception {
TikaLoader loader = load("""
{
"parsers": [ {"enriching-test-parser": {}}, {"default-parser": {}} ],
"text-recognizers": []
}
""");
CompositeContentEnricher enrichers = loader.get(CompositeContentEnricher.class);
assertNotNull(enrichers);
assertTrue(enrichers.isEmpty());
EnrichingTestParser enrichingParser = findEnrichingParser(loader.get(Parser.class));
assertEquals(enrichers, enrichingParser.getContentEnrichers());
ParseContext context = new ParseContext();
context.set(Parser.class, loader.get(Parser.class));
assertNull(ContentEnrichers.get(enrichers, MediaType.parse("application/test+minimal"),
target(MediaType.parse("application/test+minimal")),
context), "the SPI enricher in default-parser is not consulted");
}
/**
* An enricher under "parsers" whose every type another parser claims is never dispatched
* to; the loader names it so a config that looks like a parser choice is not a silent
* enricher choice.
*/
@Test
public void testUndispatchedEnricherUnderParsersIsReported() throws Exception {
TikaLoader loader = load("""
{
"parsers": [ {"minimal-test-parser": {}}, {"test-spi-enricher": {}} ]
}
""");
java.util.List<Parser> inert = ParserLoader.undispatchedEnrichers(loader.loadParsers());
assertEquals(1, inert.size());
assertTrue(inert.get(0) instanceof TestSpiEnricher);
loader = load("""
{
"parsers": [ {"test-spi-enricher": {}} ]
}
""");
assertTrue(ParserLoader.undispatchedEnrichers(loader.loadParsers()).isEmpty(),
"alone it is the parser for its type");
}
/** Two text recognizers on one type both run; the loader names them at startup. */
@Test
public void testOverlappingTextRecognizersAreReported() throws Exception {
TikaLoader loader = load("""
{
"text-recognizers": [
{"test-png-recognizer": {}}, {"test-legacy-ocr-enricher": {}}
]
}
""");
java.util.Map<java.util.List<Parser>, java.util.Set<MediaType>> overlaps =
ParserLoader.overlappingTextRecognizers(loader.get(CompositeContentEnricher.class));
assertEquals(1, overlaps.size());
java.util.Map.Entry<java.util.List<Parser>, java.util.Set<MediaType>> overlap =
overlaps.entrySet().iterator().next();
assertEquals(2, overlap.getKey().size(), "the legacy claimant counts as a recognizer");
assertEquals(java.util.Set.of(MediaType.image("png")), overlap.getValue(),
"only the shared type; the legacy engine's tiff is not an overlap");
loader = load("""
{
"text-recognizers": [
{"test-png-recognizer": {}}, {"test-png-enricher": {}}
]
}
""");
assertTrue(ParserLoader.overlappingTextRecognizers(
loader.get(CompositeContentEnricher.class)).isEmpty(),
"a recognizer beside an annotator is the intended shape");
}
private EnrichingTestParser findEnrichingParser(Parser parser) {
if (parser instanceof EnrichingTestParser dtp) {
return dtp;
}
if (parser instanceof CompositeParser cp) {
for (Parser child : cp.getAllComponentParsers()) {
EnrichingTestParser found = findEnrichingParser(child);
if (found != null) {
return found;
}
}
}
return null;
}
}