JsonConfigHelperTest.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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;

import com.fasterxml.jackson.databind.JsonNode;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

public class JsonConfigHelperTest {

    @Test
    public void testStringReplacement() throws Exception {
        String template = """
                {
                  "name": "COMPONENT_NAME",
                  "description": "A test component"
                }
                """;

        JsonNode result = JsonConfigHelper.loadFromString(template,
                Map.of("COMPONENT_NAME", "my-fetcher"));

        assertEquals("my-fetcher", result.get("name").asText());
        assertEquals("A test component", result.get("description").asText());
    }

    @Test
    public void testIntegerReplacement() throws Exception {
        String template = """
                {
                  "maxFiles": "MAX_FILES",
                  "timeout": "TIMEOUT_MS"
                }
                """;

        JsonNode result = JsonConfigHelper.loadFromString(template,
                Map.of("MAX_FILES", 100, "TIMEOUT_MS", 5000L));

        assertTrue(result.get("maxFiles").isNumber());
        assertEquals(100, result.get("maxFiles").asInt());
        assertTrue(result.get("timeout").isNumber());
        assertEquals(5000L, result.get("timeout").asLong());
    }

    @Test
    public void testDoubleReplacement() throws Exception {
        String template = """
                {
                  "threshold": "THRESHOLD",
                  "rate": "RATE"
                }
                """;

        JsonNode result = JsonConfigHelper.loadFromString(template,
                Map.of("THRESHOLD", 0.95, "RATE", 1.5f));

        assertTrue(result.get("threshold").isNumber());
        assertEquals(0.95, result.get("threshold").asDouble(), 0.001);
        assertTrue(result.get("rate").isNumber());
        assertEquals(1.5, result.get("rate").asDouble(), 0.001);
    }

    @Test
    public void testBooleanReplacement() throws Exception {
        String template = """
                {
                  "enabled": "ENABLED",
                  "debug": "DEBUG_MODE"
                }
                """;

        JsonNode result = JsonConfigHelper.loadFromString(template,
                Map.of("ENABLED", true, "DEBUG_MODE", false));

        assertTrue(result.get("enabled").isBoolean());
        assertTrue(result.get("enabled").asBoolean());
        assertTrue(result.get("debug").isBoolean());
        assertFalse(result.get("debug").asBoolean());
    }

    @Test
    public void testPathReplacement(@TempDir Path tmpDir) throws Exception {
        String template = """
                {
                  "basePath": "BASE_PATH",
                  "outputDir": "OUTPUT_DIR"
                }
                """;

        Path inputPath = tmpDir.resolve("input");
        Path outputPath = tmpDir.resolve("output");

        JsonNode result = JsonConfigHelper.loadFromString(template,
                Map.of("BASE_PATH", inputPath, "OUTPUT_DIR", outputPath));

        // Should be strings with forward slashes (even on Windows)
        assertTrue(result.get("basePath").isTextual());
        assertFalse(result.get("basePath").asText().contains("\\"),
                "Path should use forward slashes");
        assertTrue(result.get("basePath").asText().endsWith("input"));

        assertTrue(result.get("outputDir").isTextual());
        assertFalse(result.get("outputDir").asText().contains("\\"),
                "Path should use forward slashes");
        assertTrue(result.get("outputDir").asText().endsWith("output"));
    }

    @Test
    public void testNestedReplacement() throws Exception {
        String template = """
                {
                  "fetchers": {
                    "fs": {
                      "file-system-fetcher": {
                        "basePath": "FETCHER_PATH",
                        "maxSize": "MAX_SIZE"
                      }
                    }
                  },
                  "emitters": {
                    "out": {
                      "file-system-emitter": {
                        "basePath": "EMITTER_PATH"
                      }
                    }
                  }
                }
                """;

        JsonNode result = JsonConfigHelper.loadFromString(template,
                Map.of(
                        "FETCHER_PATH", "/tmp/input",
                        "EMITTER_PATH", "/tmp/output",
                        "MAX_SIZE", 1000
                ));

        assertEquals("/tmp/input",
                result.at("/fetchers/fs/file-system-fetcher/basePath").asText());
        assertEquals(1000,
                result.at("/fetchers/fs/file-system-fetcher/maxSize").asInt());
        assertEquals("/tmp/output",
                result.at("/emitters/out/file-system-emitter/basePath").asText());
    }

    @Test
    public void testArrayReplacement() throws Exception {
        String template = """
                {
                  "items": ["ITEM_1", "ITEM_2", "static-item"],
                  "counts": ["COUNT_1", "COUNT_2"]
                }
                """;

        JsonNode result = JsonConfigHelper.loadFromString(template,
                Map.of(
                        "ITEM_1", "first",
                        "ITEM_2", "second",
                        "COUNT_1", 10,
                        "COUNT_2", 20
                ));

        assertEquals("first", result.get("items").get(0).asText());
        assertEquals("second", result.get("items").get(1).asText());
        assertEquals("static-item", result.get("items").get(2).asText());
        assertEquals(10, result.get("counts").get(0).asInt());
        assertEquals(20, result.get("counts").get(1).asInt());
    }

    @Test
    public void testUnmatchedPlaceholdersLeftAlone() throws Exception {
        String template = """
                {
                  "matched": "WILL_MATCH",
                  "unmatched": "NOT_IN_MAP"
                }
                """;

        JsonNode result = JsonConfigHelper.loadFromString(template,
                Map.of("WILL_MATCH", "replaced-value"));

        assertEquals("replaced-value", result.get("matched").asText());
        assertEquals("NOT_IN_MAP", result.get("unmatched").asText());
    }

    @Test
    public void testWriteConfig(@TempDir Path tmpDir) throws Exception {
        String template = """
                {
                  "path": "THE_PATH",
                  "count": "THE_COUNT"
                }
                """;

        Path templateFile = tmpDir.resolve("template.json");
        Files.writeString(templateFile, template);

        Path outputFile = tmpDir.resolve("output.json");
        Path inputDir = tmpDir.resolve("input");

        JsonConfigHelper.writeConfig(templateFile,
                Map.of("THE_PATH", inputDir, "THE_COUNT", 42),
                outputFile);

        // Read back and verify
        JsonNode result = JsonConfigHelper.getMapper().readTree(outputFile.toFile());
        assertTrue(result.get("path").asText().endsWith("input"));
        assertFalse(result.get("path").asText().contains("\\"));
        assertEquals(42, result.get("count").asInt());
    }

    @Test
    public void testMixedTypes() throws Exception {
        String template = """
                {
                  "config": {
                    "stringVal": "STRING_VAL",
                    "intVal": "INT_VAL",
                    "longVal": "LONG_VAL",
                    "doubleVal": "DOUBLE_VAL",
                    "floatVal": "FLOAT_VAL",
                    "boolVal": "BOOL_VAL",
                    "pathVal": "PATH_VAL"
                  }
                }
                """;

        JsonNode result = JsonConfigHelper.loadFromString(template,
                Map.of(
                        "STRING_VAL", "hello",
                        "INT_VAL", 42,
                        "LONG_VAL", 9999999999L,
                        "DOUBLE_VAL", 3.14159,
                        "FLOAT_VAL", 2.5f,
                        "BOOL_VAL", true,
                        "PATH_VAL", Path.of("/tmp/test")
                ));

        JsonNode config = result.get("config");
        assertEquals("hello", config.get("stringVal").asText());
        assertEquals(42, config.get("intVal").asInt());
        assertEquals(9999999999L, config.get("longVal").asLong());
        assertEquals(3.14159, config.get("doubleVal").asDouble(), 0.00001);
        assertEquals(2.5, config.get("floatVal").asDouble(), 0.01);
        assertTrue(config.get("boolVal").asBoolean());
        String pathVal = config.get("pathVal").asText();
        assertTrue(pathVal.endsWith("tmp/test"), "Path should end with tmp/test but was: " + pathVal);
        assertFalse(pathVal.contains("\\"), "Path should use forward slashes");
    }

    @Test
    public void testToJsonPath() {
        // Test that backslashes are converted to forward slashes
        Path path = Path.of("/some/path/to/file");
        String jsonPath = JsonConfigHelper.toJsonPath(path);
        assertFalse(jsonPath.contains("\\"), "Should not contain backslashes");
        assertTrue(jsonPath.contains("/"), "Should contain forward slashes");
    }

    @Test
    public void testLoadFromResource(@TempDir Path tmpDir) throws Exception {
        Path fetcherPath = tmpDir.resolve("fetcher-base");
        Path emitterPath = tmpDir.resolve("emitter-base");

        JsonNode result = JsonConfigHelper.loadFromResource(
                "/configs/template-test.json",
                JsonConfigHelperTest.class,
                Map.of(
                        "FETCHER_BASE_PATH", fetcherPath,
                        "EMITTER_BASE_PATH", emitterPath,
                        "MAX_FILES", 500,
                        "EMIT_INTERMEDIATE", true
                ));

        // Verify nested paths were replaced
        String fetcherBasePath = result.at("/fetchers/fs/file-system-fetcher/basePath").asText();
        assertTrue(fetcherBasePath.endsWith("fetcher-base"));
        assertFalse(fetcherBasePath.contains("\\"));

        String emitterBasePath = result.at("/emitters/out/file-system-emitter/basePath").asText();
        assertTrue(emitterBasePath.endsWith("emitter-base"));
        assertFalse(emitterBasePath.contains("\\"));

        // Verify numeric and boolean replacements
        assertEquals(500, result.at("/pipes/maxFilesWaitingInQueue").asInt());
        assertTrue(result.at("/pipes/emitIntermediateResults").asBoolean());
    }

    @Test
    public void testWriteConfigFromResource(@TempDir Path tmpDir) throws Exception {
        Path fetcherPath = tmpDir.resolve("input");
        Path emitterPath = tmpDir.resolve("output");
        Path outputFile = tmpDir.resolve("generated-config.json");

        JsonConfigHelper.writeConfigFromResource(
                "/configs/template-test.json",
                JsonConfigHelperTest.class,
                Map.of(
                        "FETCHER_BASE_PATH", fetcherPath,
                        "EMITTER_BASE_PATH", emitterPath,
                        "MAX_FILES", 1000,
                        "EMIT_INTERMEDIATE", false
                ),
                outputFile);

        // Verify file was written
        assertTrue(Files.exists(outputFile));

        // Read back and verify
        JsonNode result = JsonConfigHelper.getMapper().readTree(outputFile.toFile());
        assertTrue(result.at("/fetchers/fs/file-system-fetcher/basePath").asText().endsWith("input"));
        assertEquals(1000, result.at("/pipes/maxFilesWaitingInQueue").asInt());
        assertFalse(result.at("/pipes/emitIntermediateResults").asBoolean());
    }
}