ParserLoader.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 java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.tika.config.ServiceLoader;
import org.apache.tika.detect.EncodingDetector;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.mime.MediaType;
import org.apache.tika.mime.MediaTypeRegistry;
import org.apache.tika.parser.AbstractEncodingDetectorParser;
import org.apache.tika.parser.CompositeParser;
import org.apache.tika.parser.DefaultParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.Parser;
import org.apache.tika.parser.ParserDecorator;
import org.apache.tika.parser.RenderingParser;
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.renderer.Renderer;
import org.apache.tika.utils.ParserUtils;
/**
* Loader for parsers with support for:
* <ul>
* <li>SPI fallback via "default-parser" marker with exclusions</li>
* <li>Mime type filtering decorations (_mime-include, _mime-exclude)</li>
* <li>EncodingDetector, Renderer and content-enricher dependency injection</li>
* </ul>
*/
public class ParserLoader extends AbstractSpiComponentLoader<Parser> {
private static final Logger LOG = LoggerFactory.getLogger(ParserLoader.class);
public ParserLoader() {
super("parsers", "default-parser", Parser.class);
}
@Override
protected Parser loadComponent(String name, JsonNode configNode,
LoaderContext context) throws TikaConfigException {
try {
// Extract framework config (decorations like _mime-include/_mime-exclude)
FrameworkConfig framework = FrameworkConfig.extract(
configNode, context.getObjectMapper());
// Instantiate the parser
Parser parser = context.instantiate(name, framework.getComponentConfigNode());
// Apply mime filtering decorations if present
if (framework.getDecoration() != null && framework.getDecoration().hasFiltering()) {
parser = applyMimeFiltering(parser, framework.getDecoration());
}
return parser;
} catch (IOException e) {
throw new TikaConfigException("Failed to load parser: " + name, e);
}
}
@Override
protected Parser createDefaultComposite(Set<Class<? extends Parser>> exclusions,
LoaderContext context) {
return new DefaultParser(
TikaLoader.getMediaTypeRegistry(),
new ServiceLoader(context.getClassLoader()),
exclusions);
}
@Override
protected Set<String> getAllowedMarkerKeys() {
// ParserLoader honors framework mime-filter decorators on default-parser
// in addition to the standard "exclude" key.
return Set.of("exclude", "_mime-include", "_mime-exclude");
}
@Override
protected Parser decorateDefaultComposite(Parser parser, JsonNode configNode,
LoaderContext context) throws TikaConfigException {
if (configNode == null) {
return parser;
}
try {
FrameworkConfig framework = FrameworkConfig.extract(
configNode, context.getObjectMapper());
if (framework.getDecoration() != null && framework.getDecoration().hasFiltering()) {
return applyMimeFiltering(parser, framework.getDecoration());
}
} catch (IOException e) {
throw new TikaConfigException("Failed to apply mime filtering to default-parser", e);
}
return parser;
}
@Override
protected Parser wrapInComposite(List<Parser> parsers, LoaderContext context) {
return new CompositeParser(TikaLoader.getMediaTypeRegistry(), parsers);
}
@Override
@SuppressWarnings("unchecked")
protected Class<? extends Parser> unwrapClass(Parser component) {
if (component instanceof ParserDecorator pd) {
return (Class<? extends Parser>) pd.getWrappedParser().getClass();
}
return component.getClass();
}
@Override
protected Parser postProcess(Parser parser, LoaderContext context)
throws TikaConfigException {
EncodingDetector encodingDetector = context.getEncodingDetector();
Renderer renderer = context.getRenderer();
injectDependenciesRecursively(parser, encodingDetector, renderer);
return parser;
}
/**
* Content enrichers are settled once, over the whole tree: the configured list, or
* with none, one engine per type resolved from the enrichers among the loaded parsers.
* Either way every {@link EnrichingParser} gets a composite (possibly empty), the
* effective engines are logged, and an enricher under {@code "parsers"} that nothing
* dispatches to is called out.
*/
@Override
protected Parser finish(Parser root, LoaderContext context) throws TikaConfigException {
CompositeContentEnricher configured = context.getContentEnrichers();
CompositeContentEnricher enrichers =
configured != null ? configured : ContentEnrichers.resolve(root);
logEnrichers(enrichers, configured != null);
for (Parser inert : undispatchedEnrichers(root)) {
logUndispatched(inert, enrichers, configured != null);
}
if (configured != null) {
for (Map.Entry<List<Parser>, Set<MediaType>> e
: overlappingTextRecognizers(configured).entrySet()) {
LOG.warn("Several text recognizers claim {}: {}; all of them run and their "
+ "text is concatenated. Keep one per type with _mime-include or "
+ "_mime-exclude on the entries.", e.getValue(), names(e.getKey()));
}
}
injectContentEnrichers(root, enrichers);
return root;
}
/**
* Configured text recognizers that share a media type, keyed by the recognizers and
* mapped to the types they share. Two transcriptions of one image is almost never
* intended; a recognizer beside an annotator is.
*/
static Map<List<Parser>, Set<MediaType>> overlappingTextRecognizers(
CompositeContentEnricher enrichers) {
Map<List<Parser>, Set<MediaType>> overlaps = new LinkedHashMap<>();
for (MediaType type : new TreeSet<>(enrichers.getSupportedTypes())) {
List<Parser> recognizers = new ArrayList<>();
for (Parser member : enrichers.getEnrichers(type)) {
if (ContentEnrichers.asTextRecognizer(member) != null
|| enrichers.isLegacyClaimant(member)) {
recognizers.add(member);
}
}
if (recognizers.size() > 1) {
overlaps.computeIfAbsent(recognizers, k -> new TreeSet<>()).add(type);
}
}
return overlaps;
}
private static String names(List<Parser> parsers) {
List<String> names = new ArrayList<>();
for (Parser p : parsers) {
names.add(ParserUtils.getParserClassname(p));
}
return names.toString();
}
/**
* Enrichers named directly under {@code "parsers"} that the composite never dispatches
* to: every type they advertise is claimed by another parser there, or they advertise
* none (engine unavailable, or told to skip). Both shapes look configured and do
* nothing as parsers.
*/
static List<Parser> undispatchedEnrichers(Parser root) {
List<Parser> inert = new ArrayList<>();
if (!(root instanceof CompositeParser composite) || root instanceof DefaultParser) {
return inert;
}
ParseContext empty = new ParseContext();
Map<MediaType, Parser> dispatch = composite.getParsers(empty);
MediaTypeRegistry registry = composite.getMediaTypeRegistry();
for (Parser member : composite.getAllComponentParsers()) {
if (!ContentEnrichers.isEnricher(member)) {
continue;
}
boolean dispatched = false;
for (MediaType type : member.getSupportedTypes(empty)) {
if (dispatch.get(registry.normalize(type)) == member) {
dispatched = true;
break;
}
}
if (!dispatched) {
inert.add(member);
}
}
return inert;
}
// the 4.0 shape still works, so it is INFO; an entry that never runs at all is a WARN
private static void logUndispatched(Parser inert, CompositeContentEnricher enrichers,
boolean listConfigured) {
String name = ParserUtils.getParserClassname(inert);
Set<MediaType> advertised = inert.getSupportedTypes(new ParseContext());
if (advertised.isEmpty()) {
LOG.info("{} under \"parsers\" advertises no media types (engine unavailable, or "
+ "configured to skip) and never runs. To turn enrichment off, set "
+ "\"text-recognizers\": [] instead.", name);
return;
}
Set<MediaType> enriching = new TreeSet<>();
for (MediaType type : enrichers.getSupportedTypes()) {
if (enrichers.getEnrichers(type).contains(inert)) {
enriching.add(type);
}
}
if (enriching.isEmpty()) {
LOG.warn("{} under \"parsers\" is never dispatched to (every type it advertises is "
+ "claimed by another parser) and {}, so it never runs. Name it under "
+ "\"text-recognizers\" to invoke it, or exclude the parser that "
+ "claims its types to dispatch to it.", name, listConfigured
? "\"text-recognizers\" does not name it"
: "another enricher is preferred for those types");
} else {
LOG.info("{} under \"parsers\" is never dispatched to (every type it advertises is "
+ "claimed by another parser); it acts only as the text recognizer for "
+ "{}. Name it under \"text-recognizers\" to say so.", name, enriching);
}
}
private static void logEnrichers(CompositeContentEnricher enrichers, boolean configured) {
if (enrichers.isEmpty()) {
LOG.info("text recognizers: none{}; images and rendered pages are not enriched",
configured ? " (\"text-recognizers\": [])"
: " found among the loaded parsers");
return;
}
Map<Parser, Set<MediaType>> byEngine = new IdentityHashMap<>();
for (MediaType type : enrichers.getSupportedTypes()) {
for (Parser member : enrichers.getEnrichers(type)) {
byEngine.computeIfAbsent(member, k -> new TreeSet<>()).add(type);
}
}
for (Map.Entry<Parser, Set<MediaType>> e : byEngine.entrySet()) {
LOG.info("text recognizer {} for {}{}", ParserUtils.getParserClassname(e.getKey()),
e.getValue(), configured ? ""
: " (found among the loaded parsers; name it under "
+ "\"text-recognizers\" to pin it)");
}
}
private static void injectContentEnrichers(Parser parser,
CompositeContentEnricher enrichers) {
if (parser instanceof EnrichingParser ep) {
ep.setContentEnrichers(enrichers);
}
if (parser instanceof CompositeParser cp) {
for (Parser child : cp.getAllComponentParsers()) {
injectContentEnrichers(child, enrichers);
}
} else if (parser instanceof ParserDecorator pd) {
injectContentEnrichers(pd.getWrappedParser(), enrichers);
}
}
/**
* Recursively inject dependencies into a parser and its children.
*/
private void injectDependenciesRecursively(Parser parser, EncodingDetector encodingDetector,
Renderer renderer) {
if (encodingDetector != null && parser instanceof AbstractEncodingDetectorParser aedp) {
aedp.setEncodingDetector(encodingDetector);
}
if (renderer != null && parser instanceof RenderingParser rp) {
rp.setRenderer(renderer);
}
if (parser instanceof CompositeParser cp) {
for (Parser child : cp.getAllComponentParsers()) {
injectDependenciesRecursively(child, encodingDetector, renderer);
}
} else if (parser instanceof ParserDecorator pd) {
injectDependenciesRecursively(pd.getWrappedParser(), encodingDetector, renderer);
}
}
/**
* Apply mime type filtering to a parser.
* Uses ParserDecorator.withMimeFilters() which creates a MimeFilteringDecorator
* that the serializer knows how to handle for round-trip support.
*/
private Parser applyMimeFiltering(Parser parser,
FrameworkConfig.ParserDecoration decoration)
throws TikaConfigException {
Set<MediaType> includeTypes = new HashSet<>();
Set<MediaType> excludeTypes = new HashSet<>();
for (String mimeStr : decoration.getMimeInclude()) {
includeTypes.add(ComponentInstantiator.parseFilterType(mimeStr));
}
for (String mimeStr : decoration.getMimeExclude()) {
excludeTypes.add(ComponentInstantiator.parseFilterType(mimeStr));
}
return ParserDecorator.withMimeFilters(parser, includeTypes, excludeTypes);
}
}