ConfigOverrides.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.config;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.tika.config.EmbeddedLimits;
import org.apache.tika.config.TimeoutLimits;
import org.apache.tika.pipes.core.EmitStrategy;

/**
 * Configuration overrides for merging with or creating Tika JSON configuration.
 * <p>
 * This class provides a fluent builder API to specify fetchers, emitters, pipes
 * configuration, and other settings that should be merged into an existing config
 * or used to create a new one.
 * <p>
 * Example usage:
 * <pre>
 * ConfigOverrides overrides = ConfigOverrides.builder()
 *     .addFetcher("my-fetcher", "file-system-fetcher",
 *         Map.of("basePath", "/tmp/input"))
 *     .setPipesConfig(4, null)
 *     .setEmitStrategy(EmitStrategy.PASSBACK_ALL)
 *     .setPluginRoots("plugins")
 *     .build();
 * </pre>
 */
public class ConfigOverrides {

    private final List<FetcherOverride> fetchers;
    private final List<EmitterOverride> emitters;
    private final PipesConfigOverride pipesConfig;
    private final String pluginRoots;
    private final EmitStrategy emitStrategy;
    private final TimeoutLimits timeoutLimits;
    private final EmbeddedLimits embeddedLimits;
    private final Integer writeLimit;
    private final boolean classpathPlugins;
    private final boolean trustedRequests;
    private final List<String> presets;

    private ConfigOverrides(Builder builder) {
        this.fetchers = Collections.unmodifiableList(new ArrayList<>(builder.fetchers));
        this.emitters = Collections.unmodifiableList(new ArrayList<>(builder.emitters));
        this.pipesConfig = builder.pipesConfig;
        this.pluginRoots = builder.pluginRoots;
        this.emitStrategy = builder.emitStrategy;
        this.timeoutLimits = builder.timeoutLimits;
        this.embeddedLimits = builder.embeddedLimits;
        this.writeLimit = builder.writeLimit;
        this.classpathPlugins = builder.classpathPlugins;
        this.trustedRequests = builder.trustedRequests;
        this.presets = Collections.unmodifiableList(new ArrayList<>(builder.presets));
    }

    public static Builder builder() {
        return new Builder();
    }

    public List<FetcherOverride> getFetchers() {
        return fetchers;
    }

    public List<EmitterOverride> getEmitters() {
        return emitters;
    }

    public PipesConfigOverride getPipesConfig() {
        return pipesConfig;
    }

    public String getPluginRoots() {
        return pluginRoots;
    }

    public EmitStrategy getEmitStrategy() {
        return emitStrategy;
    }

    public TimeoutLimits getTimeoutLimits() {
        return timeoutLimits;
    }

    public EmbeddedLimits getEmbeddedLimits() {
        return embeddedLimits;
    }

    public Integer getWriteLimit() {
        return writeLimit;
    }

    /** Whether the fork is told to load plugins from its classpath; see ConfigMerger. */
    public boolean isClasspathPlugins() {
        return classpathPlugins;
    }

    /** Whether the fork is told its requests are the operator's own; see ConfigMerger. */
    public boolean isTrustedRequests() {
        return trustedRequests;
    }

    /** Catalog presets to activate, as {@code "name": true} would; see ConfigMerger. */
    public List<String> getPresets() {
        return presets;
    }

    /**
     * Represents a fetcher configuration override.
     */
    public static class FetcherOverride {
        private final String id;
        private final String type;
        private final Map<String, Object> config;

        public FetcherOverride(String id, String type, Map<String, Object> config) {
            this.id = id;
            this.type = type;
            this.config = config != null ? new HashMap<>(config) : new HashMap<>();
        }

        public String getId() {
            return id;
        }

        public String getType() {
            return type;
        }

        public Map<String, Object> getConfig() {
            return config;
        }
    }

    /**
     * Represents an emitter configuration override.
     */
    public static class EmitterOverride {
        private final String id;
        private final String type;
        private final Map<String, Object> config;

        public EmitterOverride(String id, String type, Map<String, Object> config) {
            this.id = id;
            this.type = type;
            this.config = config != null ? new HashMap<>(config) : new HashMap<>();
        }

        public String getId() {
            return id;
        }

        public String getType() {
            return type;
        }

        public Map<String, Object> getConfig() {
            return config;
        }
    }

    /**
     * Represents pipes configuration overrides.
     */
    public static class PipesConfigOverride {
        private final int numClients;
        private final int maxFilesProcessedPerProcess;
        private final List<String> forkedJvmArgs;
        private final long socketTimeoutMillis;
        private final String javaPath;

        public PipesConfigOverride(int numClients,
                                   int maxFilesProcessedPerProcess,
                                   List<String> forkedJvmArgs) {
            this(numClients, maxFilesProcessedPerProcess, forkedJvmArgs, -1, null);
        }

        /**
         * @param socketTimeoutMillis written when positive; -1 leaves the existing config's value
         * @param javaPath written when non-null; null leaves the existing config's value
         */
        public PipesConfigOverride(int numClients,
                                   int maxFilesProcessedPerProcess,
                                   List<String> forkedJvmArgs,
                                   long socketTimeoutMillis,
                                   String javaPath) {
            this.numClients = numClients;
            this.maxFilesProcessedPerProcess = maxFilesProcessedPerProcess;
            this.forkedJvmArgs = forkedJvmArgs != null ?
                    new ArrayList<>(forkedJvmArgs) : new ArrayList<>();
            this.socketTimeoutMillis = socketTimeoutMillis;
            this.javaPath = javaPath;
        }

        public int getNumClients() {
            return numClients;
        }

        public int getMaxFilesProcessedPerProcess() {
            return maxFilesProcessedPerProcess;
        }

        public List<String> getForkedJvmArgs() {
            return forkedJvmArgs;
        }

        public long getSocketTimeoutMillis() {
            return socketTimeoutMillis;
        }

        public String getJavaPath() {
            return javaPath;
        }
    }

    /**
     * Builder for ConfigOverrides.
     */
    public static class Builder {
        private final List<FetcherOverride> fetchers = new ArrayList<>();
        private final List<EmitterOverride> emitters = new ArrayList<>();
        private PipesConfigOverride pipesConfig;
        private String pluginRoots;
        private EmitStrategy emitStrategy;
        private TimeoutLimits timeoutLimits;
        private EmbeddedLimits embeddedLimits;
        private Integer writeLimit;
        private boolean classpathPlugins;
        private boolean trustedRequests;
        private final List<String> presets = new ArrayList<>();

        private Builder() {
        }

        /**
         * Activates a catalog preset, as {@code "presets": {"name": true}} in the config would,
         * replacing whatever value the existing config has for the name.
         */
        public Builder addPreset(String name) {
            this.presets.add(name);
            return this;
        }

        /**
         * Sets {@code pipes.trustedRequests}: the requests come from the operator, so their
         * parseContext is trusted and their limits and timeouts are used as given.
         */
        public Builder setTrustedRequests(boolean trustedRequests) {
            this.trustedRequests = trustedRequests;
            return this;
        }

        /**
         * Appends {@code -Dtika.plugins.classpath=true} to the fork's JVM args, keeping whatever
         * forkedJvmArgs the existing config or {@link #setPipesConfig} already carry.
         */
        public Builder setClasspathPlugins(boolean classpathPlugins) {
            this.classpathPlugins = classpathPlugins;
            return this;
        }

        /**
         * Add a fetcher configuration.
         *
         * @param id the fetcher ID
         * @param type the fetcher type (e.g., "file-system-fetcher")
         * @param config the fetcher configuration properties
         * @return this builder
         */
        public Builder addFetcher(String id, String type, Map<String, Object> config) {
            fetchers.add(new FetcherOverride(id, type, config));
            return this;
        }

        /**
         * Add an emitter configuration.
         *
         * @param id the emitter ID
         * @param type the emitter type (e.g., "file-system-emitter")
         * @param config the emitter configuration properties
         * @return this builder
         */
        public Builder addEmitter(String id, String type, Map<String, Object> config) {
            emitters.add(new EmitterOverride(id, type, config));
            return this;
        }

        /**
         * Set pipes configuration with basic options.
         *
         * @param numClients number of forked JVM clients
         * @param forkedJvmArgs JVM arguments for forked processes (may be null)
         * @return this builder
         */
        public Builder setPipesConfig(int numClients,
                                      List<String> forkedJvmArgs) {
            return setPipesConfig(numClients,
                    org.apache.tika.pipes.core.PipesConfig.DEFAULT_MAX_FILES_PROCESSED_PER_PROCESS,
                    forkedJvmArgs);
        }

        /**
         * Set pipes configuration with all options.
         *
         * @param numClients number of forked JVM clients
         * @param maxFilesProcessedPerProcess max files before process restart
         * @param forkedJvmArgs JVM arguments for forked processes (may be null)
         * @return this builder
         */
        public Builder setPipesConfig(int numClients,
                                      int maxFilesProcessedPerProcess,
                                      List<String> forkedJvmArgs) {
            return setPipesConfig(numClients, maxFilesProcessedPerProcess, forkedJvmArgs,
                    -1, null);
        }

        /**
         * Set pipes configuration including the parent-side process settings.
         *
         * @param socketTimeoutMillis socket read timeout for the forked process, -1 to leave
         *                            the existing config's value
         * @param javaPath java executable for the forked process, null to leave the existing
         *                 config's value
         * @return this builder
         */
        public Builder setPipesConfig(int numClients,
                                      int maxFilesProcessedPerProcess,
                                      List<String> forkedJvmArgs,
                                      long socketTimeoutMillis,
                                      String javaPath) {
            this.pipesConfig = new PipesConfigOverride(numClients,
                    maxFilesProcessedPerProcess, forkedJvmArgs, socketTimeoutMillis, javaPath);
            return this;
        }

        /**
         * Set the timeout limits to write to the parse-context section.
         *
         * @param timeoutLimits the timeout limits (may be null to use defaults)
         * @return this builder
         */
        public Builder setTimeoutLimits(TimeoutLimits timeoutLimits) {
            this.timeoutLimits = timeoutLimits;
            return this;
        }

        /**
         * Set the embedded limits to write to the parse-context section. Config-tier limits
         * are the ceiling a request's own limits are clamped to.
         *
         * @param embeddedLimits the embedded limits (may be null to use defaults)
         * @return this builder
         */
        public Builder setEmbeddedLimits(EmbeddedLimits embeddedLimits) {
            this.embeddedLimits = embeddedLimits;
            return this;
        }

        /**
         * Set {@code output-limits.writeLimit} in the parse-context section.
         *
         * @param writeLimit the write limit, negative for unlimited (may be null to leave unset)
         * @return this builder
         */
        public Builder setWriteLimit(Integer writeLimit) {
            this.writeLimit = writeLimit;
            return this;
        }

        /**
         * Set the plugin roots path.
         *
         * @param pluginRoots path to the plugins directory
         * @return this builder
         */
        public Builder setPluginRoots(String pluginRoots) {
            this.pluginRoots = pluginRoots;
            return this;
        }

        /**
         * Set the emit strategy.
         *
         * @param emitStrategy the emit strategy
         * @return this builder
         */
        public Builder setEmitStrategy(EmitStrategy emitStrategy) {
            this.emitStrategy = emitStrategy;
            return this;
        }

        /**
         * Build the ConfigOverrides instance.
         *
         * @return the ConfigOverrides
         */
        public ConfigOverrides build() {
            return new ConfigOverrides(this);
        }
    }
}