ParseContextConfig.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;

import java.io.IOException;
import java.util.Locale;

import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.parser.ParseContext;

/**
 * Facade for accessing runtime configuration from ParseContext's jsonConfigs.
 * <p>
 * This wrapper provides a safe way for parsers to access runtime configuration
 * without directly depending on tika-serialization. It performs these critical checks:
 * <ul>
 *   <li>If ParseContext has JSON config for the requested key but ConfigDeserializer
 *       is not on the classpath, throws TikaConfigException with a clear error message</li>
 *   <li>If ConfigDeserializer is available, delegates to it for deserialization</li>
 *   <li>If no config is present, returns the default config</li>
 * </ul>
 * <p>
 * Usage in parsers:
 * <pre>
 * PDFParserConfig localConfig = ParseContextConfig.getConfig(
 *     context, "pdf-parser", PDFParserConfig.class, defaultConfig);
 * </pre>
 *
 * @since Apache Tika 4.0
 */
public class ParseContextConfig {

    /**
     * Retrieves runtime configuration from ParseContext.
     * <p>
     * Resolution is per config key and config class: the resolved-config cache, then
     * JSON config (deserialized and cached). A class-keyed entry set programmatically via
     * {@code context.set(configClass, config)} is honored only when no JSON config
     * exists for the key, since several components may share one config class.
     * <p>
     * This method performs defensive checking: if the ParseContext has JSON configuration
     * for the requested key but the ConfigDeserializer is not available on the classpath,
     * it throws TikaConfigException to prevent silent failures.
     *
     * @param context the parse context (may be null)
     * @param configKey the configuration key (e.g., "pdf-parser", "html-parser")
     * @param configClass the configuration class
     * @param defaultConfig the default configuration to use if no runtime config exists
     * @param <T> the configuration type
     * @return the runtime config merged with defaults, or the default config if no runtime config
     * @throws TikaConfigException if ParseContext has JSON config but ConfigDeserializer is not on classpath
     * @throws IOException if deserialization fails
     */
    public static <T> T getConfig(ParseContext context, String configKey,
                                   Class<T> configClass, T defaultConfig)
            throws TikaConfigException, IOException {
        if (context == null) {
            return defaultConfig;
        }

        // Per-key resolution first: a class-keyed entry may belong to a different
        // component that happens to share this config class. The cache is also keyed
        // by class: one key may be resolved into a validation-only subclass and then
        // into the real config class, and each must get its own instance.
        T resolved = context.getResolvedConfig(configKey, configClass);
        if (resolved != null) {
            return resolved;
        }

        if (!context.hasJsonConfig(configKey)) {
            // no JSON for this key: honor a programmatic, class-keyed override
            T existingConfig = context.get(configClass);
            return existingConfig != null ? existingConfig : defaultConfig;
        }

        ConfigReader reader = ConfigReader.find();
        if (reader == null) {
            throw new TikaConfigException(String.format(Locale.ROOT,
                "ParseContext contains JSON configuration for '%s' " +
                "but tika-serialization is missing or failed to load. " +
                "This means your runtime configuration will be ignored. " +
                "To fix: add tika-serialization as a dependency.",
                configKey));
        }
        try {
            return reader.getConfig(context, configKey, configClass, defaultConfig);
        } catch (RuntimeException e) {
            throw new IOException("Failed to deserialize config for '" + configKey + "': " +
                e.getMessage(), e);
        }
    }

    /**
     * Checks if runtime configuration exists for the given key.
     * <p>
     * Unlike {@link #getConfig}, this method does NOT throw if ConfigDeserializer
     * is missing - it only checks for the presence of config.
     *
     * @param context the parse context
     * @param configKey the configuration key
     * @return true if JSON config exists for this key
     */
    public static boolean hasConfig(ParseContext context, String configKey) {
        if (context == null) {
            return false;
        }
        return context.hasJsonConfig(configKey);
    }

    /**
     * Checks if ConfigDeserializer is available on the classpath.
     *
     * @return true if tika-serialization is available
     */
    public static boolean isConfigDeserializerAvailable() {
        return ConfigReader.find() != null;
    }
}