TikaObjectMapperFactory.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.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.parser.pages.PagesConfig;
import org.apache.tika.renderer.RenderSettings;
import org.apache.tika.serialization.ComponentNameResolver;
import org.apache.tika.serialization.OperatorOnlyGuard;
import org.apache.tika.serialization.TikaModule;
/**
* Factory for creating ObjectMappers configured for Tika serialization.
* <p>
* Configures strict validation settings and loads component registries
* for friendly name resolution.
*/
public class TikaObjectMapperFactory {
private static final Logger LOG = LoggerFactory.getLogger(TikaObjectMapperFactory.class);
/**
* Index file names for component registries.
*/
private static final String[] REGISTRY_INDEX_FILES = {
"parsers",
"detectors",
"encoding-detectors",
"metadata-filters",
"image-checks",
"renderers",
"translators",
"digester-factories",
"content-handler-factories",
"parse-context"
};
private static ObjectMapper MAPPER = null;
// Shared plain ObjectMapper (no TikaModule) for converting JsonNodes to JSON strings.
// Needed because the main mapper may use a binary format (e.g., Smile)
// which doesn't support writeValueAsString().
private static final ObjectMapper PLAIN_MAPPER = new ObjectMapper();
static {
// Components with no bean properties (e.g., parsers with no configuration)
// need to serialize as empty objects rather than throwing.
PLAIN_MAPPER.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
sparseOverlays(PLAIN_MAPPER);
}
@JsonInclude(JsonInclude.Include.NON_NULL)
private abstract static class Sparse {
}
/** An overlay config is mostly null: a dump writes what is set. Those classes only. */
private static void sparseOverlays(ObjectMapper mapper) {
for (Class<?> overlay : List.of(PagesConfig.class, PagesConfig.Ocr.class,
PagesConfig.Auto.class, PagesConfig.Emit.class, RenderSettings.class)) {
mapper.addMixIn(overlay, Sparse.class);
}
}
/**
* Returns a shared plain ObjectMapper without TikaModule registration.
* <p>
* This mapper is suitable for:
* <ul>
* <li>Converting JsonNodes to JSON strings</li>
* <li>Serializing component properties without compact format wrapping</li>
* <li>Avoiding infinite recursion when serializing inside TikaModule</li>
* </ul>
* <p>
* Has {@code FAIL_ON_EMPTY_BEANS} disabled to allow serialization of classes with no properties.
*
* @return the shared plain ObjectMapper
*/
public static ObjectMapper getPlainMapper() {
return PLAIN_MAPPER;
}
/**
* Reads a component's JSON config; called by tika-core's ConfigDeserializer by reflection.
* Untrusted JSON may not set an {@link org.apache.tika.config.OperatorOnly} property.
*/
public static <T> T readConfig(String json, Class<T> configClass, boolean trusted)
throws Exception {
ObjectMapper mapper = getMapper();
if (!trusted) {
OperatorOnlyGuard.check(mapper, nestedName(configClass), json, configClass);
}
return ComponentInstantiator.withTrust(mapper.readerFor(configClass), trusted).readValue(json);
}
/** {@code PDFParser$Config}, not {@code Config}: names the component that owns the config. */
private static String nestedName(Class<?> configClass) {
return configClass.getName().substring(configClass.getPackageName().length() + 1);
}
public static synchronized ObjectMapper getMapper() {
if (MAPPER == null) {
MAPPER = createMapper();
}
return MAPPER;
}
/**
* Creates an ObjectMapper configured for Tika serialization.
*
* @return configured ObjectMapper
*/
public static ObjectMapper createMapper() {
return createMapper(null);
}
/**
* Creates an ObjectMapper configured for Tika serialization with a custom JsonFactory.
* <p>
* This can be used to create mappers for binary formats like Smile:
* <pre>
* ObjectMapper smileMapper = TikaObjectMapperFactory.createMapper(new SmileFactory());
* </pre>
*
* @param factory the JsonFactory to use, or null for default JSON
* @return configured ObjectMapper
*/
public static ObjectMapper createMapper(JsonFactory factory) {
ObjectMapper mapper = strict((factory != null) ? new ObjectMapper(factory) : new ObjectMapper());
// Prevent null values being assigned to primitive fields (int, boolean, etc.)
mapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, true);
sparseOverlays(mapper);
// Load component registries for name resolution
loadComponentRegistries();
// Register TikaModule for compact serialization/deserialization of registered components
TikaModule tikaModule = new TikaModule();
mapper.registerModule(tikaModule);
// Set the shared mapper for TikaModule's deserializers
TikaModule.setSharedMapper(mapper);
return mapper;
}
/**
* A strict mapper without Tika's component serializers, for plugin configuration
* ({@code PluginJson}). Same checks as {@link #getMapper()} except
* {@code FAIL_ON_NULL_FOR_PRIMITIVES}, which also rejects an absent primitive record
* component, and optional primitives with defaults are the norm in plugin configs.
*/
public static ObjectMapper createStrictPlainMapper() {
return strict(new ObjectMapper());
}
private static ObjectMapper strict(ObjectMapper mapper) {
// Allow comments in JSON config files (// and /* */ style)
mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
// Duplicate keys: the parser feature covers readValue; the tree feature only readTree
mapper.configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, true);
mapper.configure(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY, true);
// Fail on unknown properties to catch configuration errors early
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
// Ensure enums are properly validated (not just numeric values)
mapper.configure(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS, true);
// Accept enum values in any case ("no_ocr" as well as "NO_OCR"): hand-written
// requests and older examples spell them either way
mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true);
// Need to allow creation of classes without setters/getters -- we may want to revisit this
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
return mapper;
}
/**
* Loads component registries for name resolution.
* Registries are loaded from index files and registered with the ComponentNameResolver.
* Missing registries are silently ignored (may not be on classpath).
*/
private static void loadComponentRegistries() {
ClassLoader classLoader = TikaObjectMapperFactory.class.getClassLoader();
for (String indexFile : REGISTRY_INDEX_FILES) {
try {
ComponentRegistry registry = new ComponentRegistry(indexFile, classLoader);
ComponentNameResolver.registerRegistry(indexFile, registry);
LOG.debug("Loaded component registry: {}", indexFile);
} catch (TikaConfigException e) {
// Registry not available - this is expected if the module isn't on classpath
LOG.debug("Component registry not available: {} - {}", indexFile, e.getMessage());
}
}
}
}