ComponentNameResolver.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.serialization;

import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.tika.config.loader.ComponentInfo;
import org.apache.tika.config.loader.ComponentRegistry;
import org.apache.tika.detect.Detector;
import org.apache.tika.detect.EncodingDetector;
import org.apache.tika.digest.DigesterFactory;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.extractor.EmbeddedDocumentExtractorFactory;
import org.apache.tika.extractor.UnpackSelector;
import org.apache.tika.language.translate.Translator;
import org.apache.tika.metadata.filter.MetadataFilter;
import org.apache.tika.metadata.writefilter.MetadataWriteLimiterFactory;
import org.apache.tika.parser.Parser;
import org.apache.tika.renderer.Renderer;
import org.apache.tika.sax.ContentHandlerDecoratorFactory;
import org.apache.tika.sax.ContentHandlerFactory;

/**
 * Utility class that resolves friendly component names to classes using ComponentRegistry.
 * <p>
 * Supports friendly names like "pdf-parser" as well as fully qualified class names.
 * Registries must be registered via {@link #registerRegistry(String, ComponentRegistry)}
 * before use.
 * <p>
 * Also stores {@link ComponentConfig} registrations for top-level component loading.
 */
public final class ComponentNameResolver {

    /**
     * Interfaces that use compact format serialization and serve as ParseContext keys.
     * Types implementing these interfaces will be serialized as:
     * - "type-name" for defaults
     * - {"type-name": {...}} for configured instances
     */
    private static final Set<Class<?>> CONTEXT_KEY_INTERFACES = new HashSet<>();

    static {
        CONTEXT_KEY_INTERFACES.add(Parser.class);
        CONTEXT_KEY_INTERFACES.add(Detector.class);
        CONTEXT_KEY_INTERFACES.add(EncodingDetector.class);
        CONTEXT_KEY_INTERFACES.add(MetadataFilter.class);
        CONTEXT_KEY_INTERFACES.add(Translator.class);
        CONTEXT_KEY_INTERFACES.add(Renderer.class);
        CONTEXT_KEY_INTERFACES.add(DigesterFactory.class);
        CONTEXT_KEY_INTERFACES.add(EmbeddedDocumentExtractorFactory.class);
        CONTEXT_KEY_INTERFACES.add(MetadataWriteLimiterFactory.class);
        CONTEXT_KEY_INTERFACES.add(ContentHandlerDecoratorFactory.class);
        CONTEXT_KEY_INTERFACES.add(ContentHandlerFactory.class);
        CONTEXT_KEY_INTERFACES.add(UnpackSelector.class);
    }

    /**
     * Subset of {@link #CONTEXT_KEY_INTERFACES} a request-supplied (wire) ParseContext may
     * instantiate and bind. Default-deny: a type is listed only if every registered implementation
     * is confined to transforming this request's metadata or shaping its output -- no exec, IO,
     * network, or control over which other components run. Enforced only on untrusted wire input;
     * trusted load-time config via TikaLoader is unrestricted.
     */
    private static final Set<Class<?>> WIRE_INSTANTIABLE_CONTEXT_KEYS = new HashSet<>();

    /**
     * Complement of {@link #WIRE_INSTANTIABLE_CONTEXT_KEYS}: context keys a wire ParseContext may
     * NOT instantiate (exec/IO/network or parse-graph control). Explicit so the exhaustiveness test
     * can assert every context-key interface is classified as exactly one of allowed/blocked.
     */
    private static final Set<Class<?>> WIRE_BLOCKED_CONTEXT_KEYS = new HashSet<>();

    static {
        // Allowed: bounded to this request's metadata/output; no exec or IO.
        WIRE_INSTANTIABLE_CONTEXT_KEYS.add(MetadataFilter.class);
        WIRE_INSTANTIABLE_CONTEXT_KEYS.add(ContentHandlerFactory.class);
        WIRE_INSTANTIABLE_CONTEXT_KEYS.add(ContentHandlerDecoratorFactory.class);
        WIRE_INSTANTIABLE_CONTEXT_KEYS.add(DigesterFactory.class);
        WIRE_INSTANTIABLE_CONTEXT_KEYS.add(MetadataWriteLimiterFactory.class);
        WIRE_INSTANTIABLE_CONTEXT_KEYS.add(UnpackSelector.class);

        // Blocked: exec / IO / network / parse-graph control.
        WIRE_BLOCKED_CONTEXT_KEYS.add(Parser.class);
        WIRE_BLOCKED_CONTEXT_KEYS.add(Detector.class);
        WIRE_BLOCKED_CONTEXT_KEYS.add(EncodingDetector.class);
        WIRE_BLOCKED_CONTEXT_KEYS.add(Renderer.class);
        WIRE_BLOCKED_CONTEXT_KEYS.add(Translator.class);
        WIRE_BLOCKED_CONTEXT_KEYS.add(EmbeddedDocumentExtractorFactory.class);
    }

    private static final Map<String, ComponentRegistry> REGISTRIES = new ConcurrentHashMap<>();

    // Component configuration storage (keyed by component class)
    private static final Map<Class<?>, ComponentConfig<?>> CLASS_TO_CONFIG = new ConcurrentHashMap<>();

    private ComponentNameResolver() {
        // Utility class
    }

    /**
     * Registers a ComponentRegistry for name resolution.
     *
     * @param indexName the index file name (e.g., "parsers", "detectors")
     * @param registry the registry to register
     */
    public static void registerRegistry(String indexName, ComponentRegistry registry) {
        REGISTRIES.put(indexName, registry);
    }

    /**
     * Resolves a friendly name (or registered FQCN) to a Class by searching the
     * registered component registries only. There is deliberately no
     * Class.forName fallback: unregistered names are rejected for security.
     *
     * @param name friendly name or registered fully qualified class name
     * @param classLoader unused for resolution; retained for API stability
     * @return the resolved class
     * @throws ClassNotFoundException if the name is not in any registry
     */
    public static Class<?> resolveClass(String name, ClassLoader classLoader)
            throws ClassNotFoundException {
        for (ComponentRegistry registry : REGISTRIES.values()) {
            if (registry.hasComponent(name)) {
                try {
                    return registry.getComponentClass(name);
                } catch (TikaConfigException e) {
                    // continue to next registry
                }
            }
        }
        throw new ClassNotFoundException(unregisteredMessage(name));
    }

    /**
     * Builds a diagnostic message for an unregistered component name. It calls out the
     * two usual causes -- the name is misspelled, or the module that provides it is not
     * on the classpath (optional components such as the Tess4J OCR parser ship in
     * separate jars that must be added explicitly) -- and lists the names that
     * <em>are</em> registered so the caller can find the right one (or notice that
     * nothing registered, which means no {@code .idx} files were on the classpath).
     */
    private static String unregisteredMessage(String name) {
        TreeSet<String> known = new TreeSet<>();
        for (ComponentRegistry registry : REGISTRIES.values()) {
            known.addAll(registry.getAllComponents().keySet());
        }
        StringBuilder sb = new StringBuilder()
                .append("Component '").append(name).append("' is not registered. ")
                .append("Either the name is misspelled, or the module that provides it is ")
                .append("not on the classpath -- optional components (for example the Tess4J ")
                .append("OCR parser in tika-parser-tess4j-module) ship as separate jars that ")
                .append("must be added explicitly. ");
        if (known.isEmpty()) {
            sb.append("No components are currently registered "
                    + "(no META-INF/tika/*.idx files were found on the classpath). ");
        } else {
            sb.append(known.size()).append(" registered component(s): ");
            int shown = 0;
            int cap = 50;
            for (String registered : known) {
                if (shown == cap) {
                    sb.append(", ...");
                    break;
                }
                if (shown > 0) {
                    sb.append(", ");
                }
                sb.append(registered);
                shown++;
            }
            sb.append(". ");
        }
        sb.append("Arbitrary class names are not allowed for security reasons.");
        return sb.toString();
    }

    /**
     * Gets the friendly name for a class, or null if not registered.
     *
     * @param clazz the class to look up
     * @return the friendly name, or null if not found
     */
    public static String getFriendlyName(Class<?> clazz) {
        for (ComponentRegistry registry : REGISTRIES.values()) {
            String friendlyName = registry.getFriendlyName(clazz);
            if (friendlyName != null) {
                return friendlyName;
            }
        }
        return null;
    }

    /**
     * Checks if a component with the given name is registered in any registry.
     *
     * @param name the component name to check
     * @return true if the component is registered
     */
    public static boolean hasComponent(String name) {
        for (ComponentRegistry registry : REGISTRIES.values()) {
            if (registry.hasComponent(name)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Gets the component info for a given friendly name.
     *
     * @param name the friendly name to look up
     * @return Optional containing the ComponentInfo, or empty if not found
     */
    public static Optional<ComponentInfo> getComponentInfo(String name) {
        for (ComponentRegistry registry : REGISTRIES.values()) {
            if (registry.hasComponent(name)) {
                try {
                    return Optional.of(registry.getComponentInfo(name));
                } catch (TikaConfigException e) {
                    // continue to next registry
                }
            }
        }
        return Optional.empty();
    }

    /**
     * Checks if any registered component implements or extends the given abstract type.
     * <p>
     * This is used by TikaModule to determine if an abstract type (interface or abstract class)
     * should use compact component serialization.
     *
     * @param abstractType the abstract type to check
     * @return true if at least one registered component is assignable to this type
     */
    public static boolean hasImplementationsOf(Class<?> abstractType) {
        for (ComponentRegistry registry : REGISTRIES.values()) {
            for (ComponentInfo info : registry.getAllComponents().values()) {
                if (abstractType.isAssignableFrom(info.componentClass())) {
                    return true;
                }
            }
        }
        return false;
    }

    // ==================== Component Config Methods ====================

    /**
     * Registers a ComponentConfig for top-level component loading.
     *
     * @param config the component configuration
     */
    public static <T> void registerComponentConfig(ComponentConfig<T> config) {
        CLASS_TO_CONFIG.put(config.getComponentClass(), config);
    }

    /**
     * Gets component configuration by component class.
     *
     * @param componentClass the component class (e.g., Parser.class)
     * @return the component config, or null if not registered
     */
    @SuppressWarnings("unchecked")
    public static <T> ComponentConfig<T> getComponentConfig(Class<T> componentClass) {
        return (ComponentConfig<T>) CLASS_TO_CONFIG.get(componentClass);
    }

    // ==================== Context Key Resolution Methods ====================

    /**
     * Returns the set of interfaces that use compact format serialization.
     *
     * @return unmodifiable set of context key interfaces
     */
    public static Set<Class<?>> getContextKeyInterfaces() {
        return Collections.unmodifiableSet(CONTEXT_KEY_INTERFACES);
    }

    /** Wire-instantiable context-key interfaces; see {@link #WIRE_INSTANTIABLE_CONTEXT_KEYS}. */
    public static Set<Class<?>> getWireInstantiableContextKeys() {
        return Collections.unmodifiableSet(WIRE_INSTANTIABLE_CONTEXT_KEYS);
    }

    /** True if a wire ParseContext may bind this context-key type (default-deny). */
    public static boolean isWireInstantiable(Class<?> contextKey) {
        return WIRE_INSTANTIABLE_CONTEXT_KEYS.contains(contextKey);
    }

    /**
     * True if a wire ParseContext must NOT instantiate this context-key type. Fail-closed: any
     * context-key interface not on the wire allowlist is blocked, so a newly-added one is refused
     * until consciously allow-listed. Non-component keys (plain config DTOs) are never blocked.
     */
    public static boolean isWireBlocked(Class<?> contextKey) {
        return CONTEXT_KEY_INTERFACES.contains(contextKey)
                && !WIRE_INSTANTIABLE_CONTEXT_KEYS.contains(contextKey);
    }

    /** Explicitly wire-blocked context-key interfaces; complement of the wire allowlist. */
    public static Set<Class<?>> getWireBlockedContextKeys() {
        return Collections.unmodifiableSet(WIRE_BLOCKED_CONTEXT_KEYS);
    }

    /**
     * Finds the appropriate context key interface for a given type.
     * This is used to determine which interface should be used as the ParseContext key
     * when storing instances of this type.
     *
     * @param type the type to find the context key for
     * @return the interface to use as context key, or null if none found
     */
    public static Class<?> findContextKeyInterface(Class<?> type) {
        for (Class<?> iface : CONTEXT_KEY_INTERFACES) {
            if (iface.isAssignableFrom(type)) {
                return iface;
            }
        }
        return null;
    }

    /**
     * Checks if a type should use compact format serialization.
     * Returns true if the type implements any of the registered context key interfaces.
     *
     * @param type the type to check
     * @return true if the type uses compact format
     */
    public static boolean usesCompactFormat(Class<?> type) {
        return findContextKeyInterface(type) != null;
    }

    /**
     * Determines the ParseContext key for a component.
     * <p>
     * Resolution order:
     * <ol>
     *   <li>Explicit contextKey from .idx file (via @TikaComponent annotation)</li>
     *   <li>Auto-detect from implemented interfaces (using CONTEXT_KEY_INTERFACES)</li>
     *   <li>Fall back to the component class itself</li>
     * </ol>
     *
     * @param info the component info
     * @return the class to use as ParseContext key
     */
    public static Class<?> determineContextKey(ComponentInfo info) {
        if (info.contextKey() != null) {
            return info.contextKey();
        }
        Class<?> interfaceKey = findContextKeyInterface(info.componentClass());
        if (interfaceKey != null) {
            return interfaceKey;
        }
        return info.componentClass();
    }

    /**
     * Gets the contextKey for a class from the component registry.
     * The contextKey is recorded in the .idx file by the annotation processor.
     *
     * @param clazz the class to check
     * @return the contextKey class if specified, or null if not registered or no contextKey
     */
    public static Class<?> getContextKey(Class<?> clazz) {
        for (ComponentRegistry registry : REGISTRIES.values()) {
            String friendlyName = registry.getFriendlyName(clazz);
            if (friendlyName != null) {
                try {
                    ComponentInfo info = registry.getComponentInfo(friendlyName);
                    return info.contextKey();
                } catch (TikaConfigException e) {
                    // continue to next registry
                }
            }
        }
        return null;
    }
}