EmitterManager.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.pipes.core.emitter;

import java.io.IOException;
import java.util.Map;

import com.fasterxml.jackson.databind.JsonNode;
import org.pf4j.PluginManager;

import org.apache.tika.config.loader.TikaJsonConfig;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.exception.TikaException;
import org.apache.tika.pipes.api.emitter.Emitter;
import org.apache.tika.pipes.api.emitter.EmitterFactory;
import org.apache.tika.pipes.api.emitter.EmitterNotFoundException;
import org.apache.tika.pipes.core.AbstractComponentManager;
import org.apache.tika.plugins.ExtensionConfig;

/**
 * Utility class that will apply the appropriate emitter
 * to the emitterString based on the prefix.
 * <p>
 * This does not allow multiple emitters supporting the same prefix.
 * Emitters are instantiated lazily on first use.
 */
public class EmitterManager extends AbstractComponentManager<Emitter, EmitterFactory> {

    private static final String CONFIG_KEY = "emitters";

    /**
     * Loads an EmitterManager without allowing runtime modifications.
     * Use {@link #load(PluginManager, TikaJsonConfig, boolean)} to enable runtime emitter additions.
     *
     * @param pluginManager the plugin manager
     * @param tikaJsonConfig the configuration
     * @return an EmitterManager that does not allow runtime modifications
     */
    public static EmitterManager load(PluginManager pluginManager, TikaJsonConfig tikaJsonConfig)
            throws IOException, TikaConfigException {
        return load(pluginManager, tikaJsonConfig, false);
    }

    /**
     * Loads an EmitterManager with optional support for runtime modifications.
     *
     * @param pluginManager the plugin manager
     * @param tikaJsonConfig the configuration
     * @param allowRuntimeModifications if true, allows calling {@link #saveEmitter(ExtensionConfig)}
     *                                  to add emitters at runtime
     * @return an EmitterManager
     */
    public static EmitterManager load(PluginManager pluginManager, TikaJsonConfig tikaJsonConfig,
                                     boolean allowRuntimeModifications)
            throws IOException, TikaConfigException {
        return load(pluginManager, tikaJsonConfig, allowRuntimeModifications, null);
    }

    /**
     * Loads an EmitterManager with optional support for runtime modifications and a custom config store.
     *
     * @param pluginManager the plugin manager
     * @param tikaJsonConfig the configuration
     * @param allowRuntimeModifications if true, allows calling {@link #saveEmitter(ExtensionConfig)}
     *                                  to add emitters at runtime
     * @param configStore custom config store implementation, or null to use default in-memory store
     * @return an EmitterManager
     */
    public static EmitterManager load(PluginManager pluginManager, TikaJsonConfig tikaJsonConfig,
                                     boolean allowRuntimeModifications,
                                     org.apache.tika.pipes.core.config.ConfigStore configStore)
            throws IOException, TikaConfigException {
        EmitterManager manager = new EmitterManager(pluginManager, allowRuntimeModifications);
        JsonNode emittersNode = tikaJsonConfig.getRootNode().get(CONFIG_KEY);

        // Validate configuration and collect emitter configs without instantiating
        Map<String, ExtensionConfig> configs = manager.validateAndCollectConfigs(pluginManager, emittersNode);

        if (configStore != null) {
            return new EmitterManager(pluginManager, configs, allowRuntimeModifications, configStore);
        }
        return new EmitterManager(pluginManager, configs, allowRuntimeModifications);
    }

    private EmitterManager(PluginManager pluginManager, boolean allowRuntimeModifications) {
        super(pluginManager, Map.of(), allowRuntimeModifications);
    }

    private EmitterManager(PluginManager pluginManager, Map<String, ExtensionConfig> emitterConfigs,
                          boolean allowRuntimeModifications) {
        super(pluginManager, emitterConfigs, allowRuntimeModifications);
    }

    private EmitterManager(PluginManager pluginManager, Map<String, ExtensionConfig> emitterConfigs,
                          boolean allowRuntimeModifications,
                          org.apache.tika.pipes.core.config.ConfigStore configStore) {
        super(pluginManager, emitterConfigs, allowRuntimeModifications, configStore);
    }

    @Override
    protected String getConfigKey() {
        return CONFIG_KEY;
    }

    @Override
    protected Class<EmitterFactory> getFactoryClass() {
        return EmitterFactory.class;
    }

    @Override
    protected String getComponentName() {
        return "emitter";
    }

    @Override
    protected TikaException createNotFoundException(String message) {
        return new EmitterNotFoundException(message);
    }

    /**
     * Gets an emitter by ID, lazily instantiating it if needed.
     *
     * @param emitterName the emitter ID
     * @return the emitter
     * @throws EmitterNotFoundException if no emitter with the given ID exists
     * @throws IOException if there's an error building the emitter
     * @throws TikaException if there's a configuration error
     */
    public Emitter getEmitter(String emitterName) throws IOException, TikaException {
        return getComponent(emitterName);
    }

    /**
     * Convenience method that returns an emitter if only one emitter
     * is configured. If 0 or > 1 emitters are configured, this throws an IllegalArgumentException.
     *
     * @return the single configured emitter
     * @throws IOException if there's an error building the emitter
     * @throws TikaException if there's a configuration error
     */
    public Emitter getEmitter() throws IOException, TikaException {
        return getComponent();
    }

    /**
     * Dynamically adds or updates an emitter configuration at runtime.
     * The emitter will not be instantiated until it is first requested via {@link #getEmitter(String)}.
     * This allows for dynamic configuration without the overhead of immediate instantiation.
     * If an emitter with the same ID already exists, it will be replaced and the cached instance cleared.
     * <p>
     * This method is only available if the EmitterManager was loaded with
     * {@link #load(PluginManager, TikaJsonConfig, boolean)} with allowRuntimeModifications=true.
     * <p>
     * Only authorized/authenticated users should be allowed to modify emitters. BE CAREFUL.
     *
     * @param config the extension configuration for the emitter
     * @throws TikaConfigException if the emitter type is unknown or if runtime modifications are not allowed
     * @throws IOException if there is an error accessing the plugin manager
     */
    public void saveEmitter(ExtensionConfig config) throws TikaConfigException, IOException {
        saveComponent(config);
    }

    /**
     * Deletes an emitter configuration by ID.
     *
     * @param emitterId the emitter ID to delete
     * @throws TikaConfigException if runtime modifications are not allowed or emitter not found
     */
    public void deleteEmitter(String emitterId) throws TikaConfigException {
        deleteComponent(emitterId);
    }

    /**
     * Gets the configuration for a specific emitter by ID.
     *
     * @param emitterId the emitter ID
     * @return the emitter configuration, or null if not found
     */
    public ExtensionConfig getConfig(String emitterId) {
        return getComponentConfig(emitterId);
    }
}