ConfigMergerTest.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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.apache.tika.pipes.core.EmitStrategy;
public class ConfigMergerTest {
@TempDir
Path tempDir;
@Test
public void testCreateNewConfig() throws IOException {
ConfigOverrides overrides = ConfigOverrides.builder()
.addFetcher("my-fetcher", "file-system-fetcher",
Map.of("basePath", "/tmp/input", "allowAbsolutePaths", true))
.setPipesConfig(4, null)
.setEmitStrategy(EmitStrategy.PASSBACK_ALL)
.setPluginRoots("plugins")
.build();
ConfigMerger.MergeResult result = ConfigMerger.mergeOrCreate(null, overrides);
assertNotNull(result);
assertNotNull(result.configPath());
assertTrue(Files.exists(result.configPath()));
// Verify config contents
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(result.configPath().toFile());
// Check fetcher
assertTrue(root.has("fetchers"));
assertTrue(root.get("fetchers").has("my-fetcher"));
JsonNode fetcherConfig = root.get("fetchers").get("my-fetcher").get("file-system-fetcher");
assertEquals("/tmp/input", fetcherConfig.get("basePath").asText());
assertTrue(fetcherConfig.get("allowAbsolutePaths").asBoolean());
// Check pipes config
assertTrue(root.has("pipes"));
assertEquals(4, root.get("pipes").get("numClients").asInt());
// Check emit strategy
assertEquals("PASSBACK_ALL", root.get("pipes").get("emitStrategy").get("type").asText());
// Check plugin roots
assertEquals("plugins", root.get("plugin-roots").asText());
// Clean up
Files.deleteIfExists(result.configPath());
}
@Test
public void testMergeWithExistingConfig() throws IOException {
// Create existing config
String existingConfig = """
{
"fetchers": {
"existing-fetcher": {
"file-system-fetcher": {
"basePath": "/existing/path"
}
}
},
"pipes": {
"numClients": 2
},
"plugin-roots": "existing-plugins"
}
""";
Path existingPath = tempDir.resolve("existing-config.json");
Files.writeString(existingPath, existingConfig);
// Apply overrides
ConfigOverrides overrides = ConfigOverrides.builder()
.addFetcher("new-fetcher", "file-system-fetcher",
Map.of("basePath", "/new/path"))
.setPipesConfig(8, null)
.build();
ConfigMerger.MergeResult result = ConfigMerger.mergeOrCreate(existingPath, overrides);
assertNotNull(result);
assertTrue(Files.exists(result.configPath()));
// Verify merged config
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(result.configPath().toFile());
// Existing fetcher should be preserved
assertTrue(root.get("fetchers").has("existing-fetcher"));
assertEquals("/existing/path",
root.get("fetchers").get("existing-fetcher")
.get("file-system-fetcher").get("basePath").asText());
// New fetcher should be added
assertTrue(root.get("fetchers").has("new-fetcher"));
assertEquals("/new/path",
root.get("fetchers").get("new-fetcher")
.get("file-system-fetcher").get("basePath").asText());
// Pipes config should be overridden
assertEquals(8, root.get("pipes").get("numClients").asInt());
// Existing plugin-roots should be preserved (not overridden)
assertEquals("existing-plugins", root.get("plugin-roots").asText());
// Clean up
Files.deleteIfExists(result.configPath());
}
@Test
public void testGeneratedUuidFetcherId() throws IOException {
ConfigOverrides overrides = ConfigOverrides.builder()
.addFetcher(null, "file-system-fetcher", // null ID triggers UUID generation
Map.of("allowAbsolutePaths", true))
.build();
ConfigMerger.MergeResult result = ConfigMerger.mergeOrCreate(null, overrides);
assertNotNull(result.fetcherId());
assertTrue(result.fetcherId().startsWith("tika-internal-fetcher-"));
// Verify fetcher exists with generated ID
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(result.configPath().toFile());
assertTrue(root.get("fetchers").has(result.fetcherId()));
Files.deleteIfExists(result.configPath());
}
@Test
public void testEmitterConfig() throws IOException {
ConfigOverrides overrides = ConfigOverrides.builder()
.addEmitter("my-emitter", "file-system-emitter",
Map.of("basePath", "/tmp/output", "onExists", "REPLACE"))
.build();
ConfigMerger.MergeResult result = ConfigMerger.mergeOrCreate(null, overrides);
assertNotNull(result);
assertNull(result.fetcherId()); // No fetcher was added
assertEquals("my-emitter", result.emitterId()); // Should be the explicit ID
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(result.configPath().toFile());
assertTrue(root.has("emitters"));
assertTrue(root.get("emitters").has("my-emitter"));
JsonNode emitterConfig = root.get("emitters").get("my-emitter").get("file-system-emitter");
assertEquals("/tmp/output", emitterConfig.get("basePath").asText());
assertEquals("REPLACE", emitterConfig.get("onExists").asText());
Files.deleteIfExists(result.configPath());
}
@Test
public void testJvmArgs() throws IOException {
ConfigOverrides overrides = ConfigOverrides.builder()
.setPipesConfig(4, List.of("-Xmx512m", "-Dsome.prop=value"))
.build();
ConfigMerger.MergeResult result = ConfigMerger.mergeOrCreate(null, overrides);
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(result.configPath().toFile());
assertTrue(root.get("pipes").has("forkedJvmArgs"));
JsonNode jvmArgs = root.get("pipes").get("forkedJvmArgs");
assertTrue(jvmArgs.isArray());
assertEquals(2, jvmArgs.size());
assertEquals("-Xmx512m", jvmArgs.get(0).asText());
assertEquals("-Dsome.prop=value", jvmArgs.get(1).asText());
Files.deleteIfExists(result.configPath());
}
@Test
public void testFullPipesConfig() throws IOException {
ConfigOverrides overrides = ConfigOverrides.builder()
.setPipesConfig(8, 300000, 5000, List.of("-Xmx1g"))
.build();
ConfigMerger.MergeResult result = ConfigMerger.mergeOrCreate(null, overrides);
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(result.configPath().toFile());
JsonNode pipes = root.get("pipes");
assertEquals(8, pipes.get("numClients").asInt());
assertEquals(300000, pipes.get("startupTimeoutMillis").asLong());
assertEquals(5000, pipes.get("maxFilesProcessedPerProcess").asInt());
Files.deleteIfExists(result.configPath());
}
@Test
public void testPluginRootsNotOverriddenIfExists() throws IOException {
// Create config with existing plugin-roots
String existingConfig = """
{
"plugin-roots": "user-plugins"
}
""";
Path existingPath = tempDir.resolve("config-with-plugins.json");
Files.writeString(existingPath, existingConfig);
// Try to set different plugin-roots
ConfigOverrides overrides = ConfigOverrides.builder()
.setPluginRoots("default-plugins")
.build();
ConfigMerger.MergeResult result = ConfigMerger.mergeOrCreate(existingPath, overrides);
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(result.configPath().toFile());
// Should keep the existing value, not override
assertEquals("user-plugins", root.get("plugin-roots").asText());
Files.deleteIfExists(result.configPath());
}
@Test
public void testMultipleFetchers() throws IOException {
ConfigOverrides overrides = ConfigOverrides.builder()
.addFetcher("fetcher1", "file-system-fetcher",
Map.of("basePath", "/path1"))
.addFetcher("fetcher2", "file-system-fetcher",
Map.of("basePath", "/path2"))
.build();
ConfigMerger.MergeResult result = ConfigMerger.mergeOrCreate(null, overrides);
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(result.configPath().toFile());
assertTrue(root.get("fetchers").has("fetcher1"));
assertTrue(root.get("fetchers").has("fetcher2"));
assertEquals("/path1",
root.get("fetchers").get("fetcher1")
.get("file-system-fetcher").get("basePath").asText());
assertEquals("/path2",
root.get("fetchers").get("fetcher2")
.get("file-system-fetcher").get("basePath").asText());
// Result should have first fetcher ID
assertEquals("fetcher1", result.fetcherId());
Files.deleteIfExists(result.configPath());
}
@Test
public void testEmptyOverrides() throws IOException {
ConfigOverrides overrides = ConfigOverrides.builder().build();
ConfigMerger.MergeResult result = ConfigMerger.mergeOrCreate(null, overrides);
assertNotNull(result);
assertTrue(Files.exists(result.configPath()));
assertNull(result.fetcherId());
assertNull(result.emitterId());
// Config should be basically empty
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(result.configPath().toFile());
assertFalse(root.has("fetchers"));
assertFalse(root.has("emitters"));
assertFalse(root.has("pipes"));
Files.deleteIfExists(result.configPath());
}
@Test
public void testNonExistentConfigPath() throws IOException {
Path nonExistent = tempDir.resolve("does-not-exist.json");
ConfigOverrides overrides = ConfigOverrides.builder()
.addFetcher("test", "file-system-fetcher", Map.of("basePath", "/test"))
.build();
// Should create new config, not fail
ConfigMerger.MergeResult result = ConfigMerger.mergeOrCreate(nonExistent, overrides);
assertNotNull(result);
assertTrue(Files.exists(result.configPath()));
Files.deleteIfExists(result.configPath());
}
}