This is an automated email from the ASF dual-hosted git repository.

tallison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git


The following commit(s) were added to refs/heads/main by this push:
     new c29b4810b1 TIKA-4564 -- replace string.replace with actual json 
interpolation in unit tests (#2439)
c29b4810b1 is described below

commit c29b4810b1011e734200bf84dcc65bca5bc9d44e
Author: Tim Allison <[email protected]>
AuthorDate: Thu Dec 11 08:56:24 2025 -0500

    TIKA-4564 -- replace string.replace with actual json interpolation in unit 
tests (#2439)
---
 .../java/org/apache/tika/cli/TikaCLIAsyncTest.java |  27 +-
 .../tika/pipes/kafka/tests/TikaPipesKafkaTest.java |  29 +-
 .../pipes/opensearch/tests/OpenSearchTest.java     |  47 ++-
 .../resources/opensearch/plugins-template.json     |   2 +-
 .../tika/pipes/s3/tests/S3PipeIntegrationTest.java |  45 ++-
 .../src/test/resources/s3/plugins-template.json    |  34 +--
 .../pipes/solr/tests/TikaPipesSolrTestBase.java    |  49 ++-
 .../src/test/resources/solr/plugins-template.json  |   8 +-
 .../apache/tika/async/cli/AsyncProcessorTest.java  |  24 +-
 .../apache/tika/pipes/core/PluginsTestHelper.java  |  39 +--
 .../pipes/core/emitter/EmitterManagerTest.java     |  23 +-
 .../pipes/core/fetcher/FetcherManagerTest.java     |  23 +-
 .../test/resources/configs/tika-config-basic.json  |   2 +-
 .../resources/configs/tika-config-passback.json    |   2 +-
 .../org/apache/tika/config/JsonConfigHelper.java   | 288 ++++++++++++++++++
 .../apache/tika/config/JsonConfigHelperTest.java   | 336 +++++++++++++++++++++
 .../src/test/resources/configs/template-test.json  |  20 ++
 .../org/apache/tika/server/core/CXFTestBase.java   |  62 ++--
 .../tika/server/core/TikaResourceFetcherTest.java  |  27 +-
 .../resources/configs/cxf-test-base-template.json  |   4 +-
 .../tika-config-server-fetcher-template.json       |   2 +-
 .../resources/configs/cxf-test-base-template.json  |   4 +-
 22 files changed, 849 insertions(+), 248 deletions(-)

diff --git a/tika-app/src/test/java/org/apache/tika/cli/TikaCLIAsyncTest.java 
b/tika-app/src/test/java/org/apache/tika/cli/TikaCLIAsyncTest.java
index 5351078e93..697cc1e7e5 100644
--- a/tika-app/src/test/java/org/apache/tika/cli/TikaCLIAsyncTest.java
+++ b/tika-app/src/test/java/org/apache/tika/cli/TikaCLIAsyncTest.java
@@ -24,10 +24,11 @@ import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.PrintStream;
-import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
 
 import org.apache.commons.io.FileUtils;
 import org.junit.jupiter.api.AfterEach;
@@ -38,6 +39,8 @@ import org.junit.jupiter.api.io.TempDir;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.tika.config.JsonConfigHelper;
+
 public class TikaCLIAsyncTest {
 
     private static final Logger LOG = LoggerFactory.getLogger(TikaCLI.class);
@@ -60,21 +63,17 @@ public class TikaCLIAsyncTest {
         TIKA_CONFIG = Files.createTempFile(ASYNC_OUTPUT_DIR, "plugins-", 
".json");
 
         Path pluginsDir = Paths.get("target/plugins");
-        if (! Files.isDirectory(pluginsDir)) {
+        if (!Files.isDirectory(pluginsDir)) {
             LOG.warn("CAN'T FIND PLUGINS DIR. pwd={}", 
Paths.get("").toAbsolutePath().toString());
         }
-        String jsonTemplate = 
Files.readString(Paths.get(TikaCLIAsyncTest.class.getResource("/configs/config-template.json").toURI()),
-                StandardCharsets.UTF_8);
-
-        String json = jsonTemplate.replace("FETCHER_BASE_PATH", 
TEST_DATA_FILE.getAbsolutePath().toString())
-                                   .replace("EMITTER_BASE_PATH", 
ASYNC_OUTPUT_DIR.toAbsolutePath().toString())
-                                   .replace("PLUGIN_ROOTS", 
pluginsDir.toAbsolutePath().toString())
-                .replace("TIKA_CONFIG", TIKA_CONFIG
-                        .toAbsolutePath().toString());
-
-                ;
-        json = json.replace("\\", "/");
-        Files.writeString(TIKA_CONFIG, json, UTF_8);
+
+        Map<String, Object> replacements = new HashMap<>();
+        replacements.put("FETCHER_BASE_PATH", TEST_DATA_FILE.toPath());
+        replacements.put("EMITTER_BASE_PATH", ASYNC_OUTPUT_DIR);
+        replacements.put("PLUGIN_ROOTS", pluginsDir);
+
+        
JsonConfigHelper.writeConfigFromResource("/configs/config-template.json",
+                TikaCLIAsyncTest.class, replacements, TIKA_CONFIG);
     }
 
     /**
diff --git 
a/tika-integration-tests/tika-pipes-kafka-integration-tests/src/test/java/org/apache/tika/pipes/kafka/tests/TikaPipesKafkaTest.java
 
b/tika-integration-tests/tika-pipes-kafka-integration-tests/src/test/java/org/apache/tika/pipes/kafka/tests/TikaPipesKafkaTest.java
index 4884d82c9e..e42b4fb8a0 100644
--- 
a/tika-integration-tests/tika-pipes-kafka-integration-tests/src/test/java/org/apache/tika/pipes/kafka/tests/TikaPipesKafkaTest.java
+++ 
b/tika-integration-tests/tika-pipes-kafka-integration-tests/src/test/java/org/apache/tika/pipes/kafka/tests/TikaPipesKafkaTest.java
@@ -36,13 +36,11 @@ import java.util.UUID;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
-import java.util.regex.Matcher;
 
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.base.Stopwatch;
 import org.apache.commons.io.FilenameUtils;
-import org.apache.commons.io.IOUtils;
 import org.apache.kafka.clients.consumer.ConsumerConfig;
 import org.apache.kafka.clients.consumer.ConsumerRecord;
 import org.apache.kafka.clients.consumer.ConsumerRecords;
@@ -65,6 +63,7 @@ import org.testcontainers.kafka.ConfluentKafkaContainer;
 import org.testcontainers.utility.DockerImageName;
 
 import org.apache.tika.cli.TikaCLI;
+import org.apache.tika.config.JsonConfigHelper;
 import org.apache.tika.pipes.api.HandlerConfig;
 import org.apache.tika.utils.SystemUtils;
 
@@ -208,30 +207,24 @@ public class TikaPipesKafkaTest {
 
     @NotNull
     private Path getTikaConfig(Path pipesDirectory, Path testFileFolderPath) 
throws Exception {
-        String json;
-        try (InputStream is = 
this.getClass().getResourceAsStream("/kafka/plugins-template.json")) {
-            assert is != null;
-            json = IOUtils.toString(is, StandardCharsets.UTF_8);
-        }
-
-        String res = json.replace("PIPE_ITERATOR_TOPIC", PIPE_ITERATOR_TOPIC)
-                .replace("EMITTER_TOPIC", EMITTER_TOPIC)
-                .replace("BOOTSTRAP_SERVERS", kafka.getBootstrapServers())
-                .replaceAll("FETCHER_BASE_PATH",
-                        
Matcher.quoteReplacement(testFileFolderPath.toAbsolutePath().toString()))
-                .replace("PARSE_MODE", HandlerConfig.PARSE_MODE.RMETA.name());
         Path tikaConfig = pipesDirectory.resolve("tika-config.json");
 
-        res = res.replace("TIKA_CONFIG", 
tikaConfig.toAbsolutePath().toString());
-
         Path log4jPropFile = pipesDirectory.resolve("log4j2.xml");
         try (InputStream is = 
this.getClass().getResourceAsStream("/pipes-fork-server-custom-log4j2.xml")) {
             assert is != null;
             Files.copy(is, log4jPropFile);
         }
-        res = res.replace("LOG4J_PROPERTIES_FILE", 
log4jPropFile.toAbsolutePath().toString());
 
-        Files.writeString(tikaConfig, res, StandardCharsets.UTF_8);
+        Map<String, Object> replacements = new HashMap<>();
+        replacements.put("PIPE_ITERATOR_TOPIC", PIPE_ITERATOR_TOPIC);
+        replacements.put("EMITTER_TOPIC", EMITTER_TOPIC);
+        replacements.put("BOOTSTRAP_SERVERS", kafka.getBootstrapServers());
+        replacements.put("FETCHER_BASE_PATH", testFileFolderPath);
+        replacements.put("PARSE_MODE", HandlerConfig.PARSE_MODE.RMETA.name());
+        replacements.put("LOG4J_PROPERTIES_FILE", log4jPropFile);
+
+        
JsonConfigHelper.writeConfigFromResource("/kafka/plugins-template.json",
+                TikaPipesKafkaTest.class, replacements, tikaConfig);
         return tikaConfig;
     }
 }
diff --git 
a/tika-integration-tests/tika-pipes-opensearch-integration-tests/src/test/java/org/apache/tika/pipes/opensearch/tests/OpenSearchTest.java
 
b/tika-integration-tests/tika-pipes-opensearch-integration-tests/src/test/java/org/apache/tika/pipes/opensearch/tests/OpenSearchTest.java
index 59c41c078f..c284f96c7c 100644
--- 
a/tika-integration-tests/tika-pipes-opensearch-integration-tests/src/test/java/org/apache/tika/pipes/opensearch/tests/OpenSearchTest.java
+++ 
b/tika-integration-tests/tika-pipes-opensearch-integration-tests/src/test/java/org/apache/tika/pipes/opensearch/tests/OpenSearchTest.java
@@ -48,6 +48,7 @@ import org.testcontainers.utility.DockerImageName;
 
 import org.apache.tika.cli.TikaCLI;
 import org.apache.tika.client.HttpClientFactory;
+import org.apache.tika.config.JsonConfigHelper;
 import org.apache.tika.config.loader.TikaJsonConfig;
 import org.apache.tika.exception.TikaConfigException;
 import org.apache.tika.metadata.Metadata;
@@ -453,43 +454,29 @@ public class OpenSearchTest {
                                        HandlerConfig.PARSE_MODE parseMode, 
String endpoint, Path testDocDirectory) throws IOException {
         Path tikaConfig = pipesDirectory.resolve("plugins-config.json");
 
-
-        String json = new 
String(OpenSearchTest.class.getResourceAsStream("/opensearch/plugins-template.json").readAllBytes(),
 StandardCharsets.UTF_8);
-        String res =
-                json.replace("ATTACHMENT_STRATEGY", 
attachmentStrategy.toString())
-                           .replace("UPDATE_STRATEGY", 
updateStrategy.toString())
-                           .replace("USER_NAME", CONTAINER.getUsername())
-                           .replace("PASSWORD", CONTAINER.getPassword())
-                           .replaceAll("FETCHER_BASE_PATH",
-                                             
Matcher.quoteReplacement(testDocDirectory.toAbsolutePath().toString()))
-                           .replace("PARSE_MODE", parseMode.name());
-
-        if (attachmentStrategy == 
OpenSearchEmitterConfig.AttachmentStrategy.PARENT_CHILD) {
-            res = res.replace("INCLUDE_ROUTING", "true");
-        } else {
-            res = res.replace("INCLUDE_ROUTING", "false");
-        }
-        res = res.replace("OPEN_SEARCH_URL", endpoint);
-
-        res = res.replace("TIKA_CONFIG", tikaConfig
-                    .toAbsolutePath()
-                    .toString());
-
         Path log4jPropFile = pipesDirectory.resolve("log4j2.xml");
         try (InputStream is = OpenSearchTest.class
                 .getResourceAsStream("/pipes-fork-server-custom-log4j2.xml")) {
             Files.copy(is, log4jPropFile);
         }
 
-        res = res.replace("LOG4J_PROPERTIES_FILE", 
log4jPropFile.toAbsolutePath().toString());
-        res = res.replace("\\", "/");
-        Files.writeString(tikaConfig, res, StandardCharsets.UTF_8);
-        return tikaConfig;
-    }
+        boolean includeRouting = (attachmentStrategy == 
OpenSearchEmitterConfig.AttachmentStrategy.PARENT_CHILD);
+
+        Map<String, Object> replacements = new HashMap<>();
+        replacements.put("ATTACHMENT_STRATEGY", attachmentStrategy.toString());
+        replacements.put("UPDATE_STRATEGY", updateStrategy.toString());
+        replacements.put("USER_NAME", CONTAINER.getUsername());
+        replacements.put("PASSWORD", CONTAINER.getPassword());
+        replacements.put("FETCHER_BASE_PATH", testDocDirectory);
+        replacements.put("PARSE_MODE", parseMode.name());
+        replacements.put("INCLUDE_ROUTING", includeRouting);
+        replacements.put("OPEN_SEARCH_URL", endpoint);
+        replacements.put("LOG4J_PROPERTIES_FILE", log4jPropFile);
 
-    private String createTikaConfigXml(Path tikaConfigFile, String xml) {
-        xml = xml.replace("TIKA_CONFIG", 
tikaConfigFile.toAbsolutePath().toString());
-        return xml;
+        
JsonConfigHelper.writeConfigFromResource("/opensearch/plugins-template.json",
+                OpenSearchTest.class, replacements, tikaConfig);
+
+        return tikaConfig;
     }
 
     private void createTestHtmlFiles(String bodyContent, int numHtmlDocs, Path 
testDocDirectory) throws Exception {
diff --git 
a/tika-integration-tests/tika-pipes-opensearch-integration-tests/src/test/resources/opensearch/plugins-template.json
 
b/tika-integration-tests/tika-pipes-opensearch-integration-tests/src/test/resources/opensearch/plugins-template.json
index 3a3286039b..7c3663a156 100644
--- 
a/tika-integration-tests/tika-pipes-opensearch-integration-tests/src/test/resources/opensearch/plugins-template.json
+++ 
b/tika-integration-tests/tika-pipes-opensearch-integration-tests/src/test/resources/opensearch/plugins-template.json
@@ -49,7 +49,7 @@
     "opensearch-pipes-reporter": {
       "openSearchUrl": "OPEN_SEARCH_URL",
       "keyPrefix": "my_test_",
-      "includeRouting": INCLUDE_ROUTING,
+      "includeRouting": "INCLUDE_ROUTING",
       "httpClientConfig": {
         "userName": "USER_NAME",
         "password": "PASSWORD",
diff --git 
a/tika-integration-tests/tika-pipes-s3-integration-tests/src/test/java/org/apache/tika/pipes/s3/tests/S3PipeIntegrationTest.java
 
b/tika-integration-tests/tika-pipes-s3-integration-tests/src/test/java/org/apache/tika/pipes/s3/tests/S3PipeIntegrationTest.java
index 7fe0c9370b..b33d77c672 100644
--- 
a/tika-integration-tests/tika-pipes-s3-integration-tests/src/test/java/org/apache/tika/pipes/s3/tests/S3PipeIntegrationTest.java
+++ 
b/tika-integration-tests/tika-pipes-s3-integration-tests/src/test/java/org/apache/tika/pipes/s3/tests/S3PipeIntegrationTest.java
@@ -21,14 +21,16 @@ import java.io.InputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
 import java.security.NoSuchAlgorithmException;
 import java.time.Duration;
 import java.time.temporal.ChronoUnit;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 
 import org.apache.commons.io.FileUtils;
-import org.apache.commons.io.IOUtils;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
@@ -52,6 +54,7 @@ import 
software.amazon.awssdk.services.s3.model.GetObjectResponse;
 import software.amazon.awssdk.services.s3.model.PutObjectRequest;
 
 import org.apache.tika.cli.TikaCLI;
+import org.apache.tika.config.JsonConfigHelper;
 
 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
 @Testcontainers(disabledWithoutDocker = true)
@@ -125,38 +128,32 @@ class S3PipeIntegrationTest {
         createTestFiles();
 
         // Setup config files
-        File log4jPropFile = new File("target", "tmp-log4j2.xml");
-        File tikaConfigFile = new File("target", "plugins-config-s3.json");
+        Path log4jPropFile = Path.of("target", "tmp-log4j2.xml");
+        Path tikaConfigFile = Path.of("target", "plugins-config-s3.json");
 
         try (InputStream is = this.getClass()
                 .getResourceAsStream("/pipes-fork-server-custom-log4j2.xml")) {
             Assertions.assertNotNull(is);
-            FileUtils.copyInputStreamToFile(is, log4jPropFile);
+            FileUtils.copyInputStreamToFile(is, log4jPropFile.toFile());
         }
 
         // Create plugins config JSON
-        String pluginsTemplate;
-        try (InputStream is = 
this.getClass().getResourceAsStream("/s3/plugins-template.json")) {
-            assert is != null;
-            pluginsTemplate = IOUtils.toString(is, StandardCharsets.UTF_8);
-        }
-
-        String pluginsConfig = pluginsTemplate
-                .replace("{TIKA_CONFIG}", tikaConfigFile.getAbsolutePath())
-                .replace("{LOG4J_PROPERTIES_FILE}", 
log4jPropFile.getAbsolutePath())
-                .replace("{PARSE_MODE}", 
org.apache.tika.pipes.api.HandlerConfig.PARSE_MODE.RMETA.name())
-                .replace("{PIPE_ITERATOR_BUCKET}", FETCH_BUCKET)
-                .replace("{EMIT_BUCKET}", EMIT_BUCKET)
-                .replace("{FETCH_BUCKET}", FETCH_BUCKET)
-                .replace("{ACCESS_KEY}", ACCESS_KEY)
-                .replace("{SECRET_KEY}", SECRET_KEY)
-                .replace("{ENDPOINT_CONFIGURATION_SERVICE}", MINIO_ENDPOINT)
-                .replace("{REGION}", REGION.id());
-
-        FileUtils.writeStringToFile(tikaConfigFile, pluginsConfig, 
StandardCharsets.UTF_8);
+        Map<String, Object> replacements = new HashMap<>();
+        replacements.put("LOG4J_PROPERTIES_FILE", log4jPropFile);
+        replacements.put("PARSE_MODE", 
org.apache.tika.pipes.api.HandlerConfig.PARSE_MODE.RMETA.name());
+        replacements.put("PIPE_ITERATOR_BUCKET", FETCH_BUCKET);
+        replacements.put("EMIT_BUCKET", EMIT_BUCKET);
+        replacements.put("FETCH_BUCKET", FETCH_BUCKET);
+        replacements.put("ACCESS_KEY", ACCESS_KEY);
+        replacements.put("SECRET_KEY", SECRET_KEY);
+        replacements.put("ENDPOINT_CONFIGURATION_SERVICE", MINIO_ENDPOINT);
+        replacements.put("REGION", REGION.id());
+
+        JsonConfigHelper.writeConfigFromResource("/s3/plugins-template.json",
+                S3PipeIntegrationTest.class, replacements, tikaConfigFile);
 
         try {
-            TikaCLI.main(new String[]{"-a", "-c", 
tikaConfigFile.getAbsolutePath()});
+            TikaCLI.main(new String[]{"-a", "-c", 
tikaConfigFile.toAbsolutePath().toString()});
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
diff --git 
a/tika-integration-tests/tika-pipes-s3-integration-tests/src/test/resources/s3/plugins-template.json
 
b/tika-integration-tests/tika-pipes-s3-integration-tests/src/test/resources/s3/plugins-template.json
index d0a61ae18a..a39705a2d4 100644
--- 
a/tika-integration-tests/tika-pipes-s3-integration-tests/src/test/resources/s3/plugins-template.json
+++ 
b/tika-integration-tests/tika-pipes-s3-integration-tests/src/test/resources/s3/plugins-template.json
@@ -2,12 +2,12 @@
   "fetchers": {
     "s3f": {
       "s3-fetcher": {
-        "region": "{REGION}",
-        "bucket": "{FETCH_BUCKET}",
+        "region": "REGION",
+        "bucket": "FETCH_BUCKET",
         "credentialsProvider": "key_secret",
-        "accessKey": "{ACCESS_KEY}",
-        "secretKey": "{SECRET_KEY}",
-        "endpointConfigurationService": "{ENDPOINT_CONFIGURATION_SERVICE}",
+        "accessKey": "ACCESS_KEY",
+        "secretKey": "SECRET_KEY",
+        "endpointConfigurationService": "ENDPOINT_CONFIGURATION_SERVICE",
         "pathStyleAccessEnabled": true,
         "maxConnections": 50,
         "throttleSeconds": [
@@ -22,12 +22,12 @@
   "emitters": {
     "s3e": {
       "s3-emitter": {
-        "region": "{REGION}",
-        "bucket": "{EMIT_BUCKET}",
+        "region": "REGION",
+        "bucket": "EMIT_BUCKET",
         "credentialsProvider": "key_secret",
-        "accessKey": "{ACCESS_KEY}",
-        "secretKey": "{SECRET_KEY}",
-        "endpointConfigurationService": "{ENDPOINT_CONFIGURATION_SERVICE}",
+        "accessKey": "ACCESS_KEY",
+        "secretKey": "SECRET_KEY",
+        "endpointConfigurationService": "ENDPOINT_CONFIGURATION_SERVICE",
         "pathStyleAccessEnabled": true,
         "maxConnections": 50,
         "fileExtension": "json",
@@ -37,19 +37,19 @@
   },
   "pipes-iterator": {
     "s3-pipes-iterator": {
-      "region": "{REGION}",
-      "bucket": "{PIPE_ITERATOR_BUCKET}",
+      "region": "REGION",
+      "bucket": "PIPE_ITERATOR_BUCKET",
       "credentialsProvider": "key_secret",
-      "accessKey": "{ACCESS_KEY}",
-      "secretKey": "{SECRET_KEY}",
-      "endpointConfigurationService": "{ENDPOINT_CONFIGURATION_SERVICE}",
+      "accessKey": "ACCESS_KEY",
+      "secretKey": "SECRET_KEY",
+      "endpointConfigurationService": "ENDPOINT_CONFIGURATION_SERVICE",
       "pathStyleAccessEnabled": true,
       "baseConfig": {
         "fetcherId": "s3f",
         "emitterId": "s3e",
         "handlerConfig": {
           "type": "TEXT",
-          "parseMode": "{PARSE_MODE}",
+          "parseMode": "PARSE_MODE",
           "writeLimit": -1,
           "maxEmbeddedResources": -1,
           "throwOnWriteLimitReached": true
@@ -69,7 +69,7 @@
       "-Xmx1g",
       "-XX:ParallelGCThreads=2",
       "-XX:+ExitOnOutOfMemoryError",
-      "-Dlog4j.configurationFile={LOG4J_PROPERTIES_FILE}"
+      "-Dlog4j.configurationFile=LOG4J_PROPERTIES_FILE"
     ],
     "timeoutMillis": 60000,
     "emitStrategy": {
diff --git 
a/tika-integration-tests/tika-pipes-solr-integration-tests/src/test/java/org/apache/tika/pipes/solr/tests/TikaPipesSolrTestBase.java
 
b/tika-integration-tests/tika-pipes-solr-integration-tests/src/test/java/org/apache/tika/pipes/solr/tests/TikaPipesSolrTestBase.java
index 942716be45..622243870d 100644
--- 
a/tika-integration-tests/tika-pipes-solr-integration-tests/src/test/java/org/apache/tika/pipes/solr/tests/TikaPipesSolrTestBase.java
+++ 
b/tika-integration-tests/tika-pipes-solr-integration-tests/src/test/java/org/apache/tika/pipes/solr/tests/TikaPipesSolrTestBase.java
@@ -23,10 +23,11 @@ import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.regex.Matcher;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 import org.apache.commons.io.FileUtils;
-import org.apache.commons.io.IOUtils;
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.entity.StringEntity;
@@ -46,6 +47,7 @@ import org.testcontainers.junit.jupiter.Container;
 import org.testcontainers.utility.DockerImageName;
 
 import org.apache.tika.cli.TikaCLI;
+import org.apache.tika.config.JsonConfigHelper;
 import org.apache.tika.pipes.api.HandlerConfig;
 import org.apache.tika.pipes.emitter.solr.SolrEmitterConfig;
 import org.apache.tika.utils.SystemUtils;
@@ -264,38 +266,33 @@ public abstract class TikaPipesSolrTestBase {
                                HandlerConfig.PARSE_MODE parseMode) throws 
IOException {
         Path tikaConfig = pipesDirectory.resolve("plugins-config.json");
 
-        String json;
-        try (InputStream is = 
this.getClass().getResourceAsStream("/solr/plugins-template.json")) {
-            json = IOUtils.toString(is, StandardCharsets.UTF_8);
+        Path log4jPropFile = pipesDirectory.resolve("log4j2.xml");
+        try (InputStream is = 
this.getClass().getResourceAsStream("/pipes-fork-server-custom-log4j2.xml")) {
+            Files.copy(is, log4jPropFile, 
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
         }
 
-        String solrUrls;
-        String solrZkHosts;
+        List<String> solrUrls;
+        List<String> solrZkHosts;
         if (useZk()) {
-            solrUrls = "[]";
-            solrZkHosts = "[\"" + solrHost + ":" + zkPort + "\"]";
+            solrUrls = List.of();
+            solrZkHosts = List.of(solrHost + ":" + zkPort);
         } else {
-            solrUrls = "[\"http://"; + solrHost + ":" + solrPort + "/solr\"]";
-            solrZkHosts = "[]";
+            solrUrls = List.of("http://"; + solrHost + ":" + solrPort + 
"/solr");
+            solrZkHosts = List.of();
         }
 
-        String res = json.replace("UPDATE_STRATEGY", updateStrategy.toString())
-                .replace("ATTACHMENT_STRATEGY", attachmentStrategy.toString())
-                .replaceAll("FETCHER_BASE_PATH",
-                        
Matcher.quoteReplacement(testFileFolder.toAbsolutePath().toString()))
-                .replace("PARSE_MODE", parseMode.name())
-                .replace("SOLR_URLS", solrUrls)
-                .replace("SOLR_ZK_HOSTS", solrZkHosts);
-
-        res = res.replace("TIKA_CONFIG", 
tikaConfig.toAbsolutePath().toString());
+        Map<String, Object> replacements = new HashMap<>();
+        replacements.put("UPDATE_STRATEGY", updateStrategy.toString());
+        replacements.put("ATTACHMENT_STRATEGY", attachmentStrategy.toString());
+        replacements.put("FETCHER_BASE_PATH", testFileFolder);
+        replacements.put("PARSE_MODE", parseMode.name());
+        replacements.put("SOLR_URLS", solrUrls);
+        replacements.put("SOLR_ZK_HOSTS", solrZkHosts);
+        replacements.put("LOG4J_PROPERTIES_FILE", log4jPropFile);
 
-        Path log4jPropFile = pipesDirectory.resolve("log4j2.xml");
-        try (InputStream is = 
this.getClass().getResourceAsStream("/pipes-fork-server-custom-log4j2.xml")) {
-            Files.copy(is, log4jPropFile, 
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
-        }
-        res = res.replace("LOG4J_PROPERTIES_FILE", 
log4jPropFile.toAbsolutePath().toString());
+        JsonConfigHelper.writeConfigFromResource("/solr/plugins-template.json",
+                TikaPipesSolrTestBase.class, replacements, tikaConfig);
 
-        Files.writeString(tikaConfig, res, StandardCharsets.UTF_8);
         return tikaConfig;
     }
 
diff --git 
a/tika-integration-tests/tika-pipes-solr-integration-tests/src/test/resources/solr/plugins-template.json
 
b/tika-integration-tests/tika-pipes-solr-integration-tests/src/test/resources/solr/plugins-template.json
index c448920871..3359a08b46 100644
--- 
a/tika-integration-tests/tika-pipes-solr-integration-tests/src/test/resources/solr/plugins-template.json
+++ 
b/tika-integration-tests/tika-pipes-solr-integration-tests/src/test/resources/solr/plugins-template.json
@@ -50,8 +50,8 @@
     "se": {
       "solr-emitter": {
         "solrCollection": "testcol",
-        "solrUrls": SOLR_URLS,
-        "solrZkHosts": SOLR_ZK_HOSTS,
+        "solrUrls": "SOLR_URLS",
+        "solrZkHosts": "SOLR_ZK_HOSTS",
         "updateStrategy": "UPDATE_STRATEGY",
         "attachmentStrategy": "ATTACHMENT_STRATEGY",
         "commitWithin": 1,
@@ -65,8 +65,8 @@
   "pipes-iterator": {
     "solr-pipes-iterator": {
       "solrCollection": "testcol",
-      "solrUrls": SOLR_URLS,
-      "solrZkHosts": SOLR_ZK_HOSTS,
+      "solrUrls": "SOLR_URLS",
+      "solrZkHosts": "SOLR_ZK_HOSTS",
       "idField": "id",
       "parsingIdField": "parsing_id_i",
       "failCountField": "fail_count_i",
diff --git 
a/tika-pipes/tika-async-cli/src/test/java/org/apache/tika/async/cli/AsyncProcessorTest.java
 
b/tika-pipes/tika-async-cli/src/test/java/org/apache/tika/async/cli/AsyncProcessorTest.java
index c3c5f100f5..51aff03a09 100644
--- 
a/tika-pipes/tika-async-cli/src/test/java/org/apache/tika/async/cli/AsyncProcessorTest.java
+++ 
b/tika-pipes/tika-async-cli/src/test/java/org/apache/tika/async/cli/AsyncProcessorTest.java
@@ -22,11 +22,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import java.io.BufferedReader;
 import java.io.OutputStream;
-import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import org.apache.commons.io.IOUtils;
 import org.junit.jupiter.api.BeforeEach;
@@ -36,6 +37,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import org.apache.tika.TikaTest;
+import org.apache.tika.config.JsonConfigHelper;
 import org.apache.tika.metadata.Metadata;
 import org.apache.tika.metadata.TikaCoreProperties;
 import org.apache.tika.parser.ParseContext;
@@ -84,20 +86,20 @@ public class AsyncProcessorTest extends TikaTest {
         Files.createDirectories(inputDir);
 
         Path pluginsDir = Paths.get("target/plugins");
-        if (! Files.isDirectory(pluginsDir)) {
+        if (!Files.isDirectory(pluginsDir)) {
             LOG.warn("CAN'T FIND PLUGINS DIR. pwd={}", 
Paths.get("").toAbsolutePath().toString());
         }
 
         tikaConfigPath = configDir.resolve("tika-config.json");
-        String json = 
Files.readString(Paths.get(AsyncProcessorTest.class.getResource("/configs/config-template.json").toURI()),
 StandardCharsets.UTF_8);
-        String jsonTemp = json
-                .replace("FETCHER_BASE_PATH", 
inputDir.toAbsolutePath().toString())
-                .replace("JSON_EMITTER_BASE_PATH", 
jsonOutputDir.toAbsolutePath().toString())
-                .replace("BYTES_EMITTER_BASE_PATH", 
bytesOutputDir.toAbsolutePath().toString())
-                .replace("PLUGIN_ROOTS", 
pluginsDir.toAbsolutePath().toString())
-                .replace("TIKA_CONFIG", 
tikaConfigPath.toAbsolutePath().toString());
-        jsonTemp = jsonTemp.replace("\\", "/");
-        Files.writeString(tikaConfigPath, jsonTemp, StandardCharsets.UTF_8);
+
+        Map<String, Object> replacements = new HashMap<>();
+        replacements.put("FETCHER_BASE_PATH", inputDir);
+        replacements.put("JSON_EMITTER_BASE_PATH", jsonOutputDir);
+        replacements.put("BYTES_EMITTER_BASE_PATH", bytesOutputDir);
+        replacements.put("PLUGIN_ROOTS", pluginsDir);
+
+        
JsonConfigHelper.writeConfigFromResource("/configs/config-template.json",
+                AsyncProcessorTest.class, replacements, tikaConfigPath);
 
         Path mock = inputDir.resolve("mock.xml");
         try (OutputStream os = Files.newOutputStream(mock)) {
diff --git 
a/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/pipes/core/PluginsTestHelper.java
 
b/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/pipes/core/PluginsTestHelper.java
index 5e8676094d..3ae317358e 100644
--- 
a/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/pipes/core/PluginsTestHelper.java
+++ 
b/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/pipes/core/PluginsTestHelper.java
@@ -17,14 +17,17 @@
 package org.apache.tika.pipes.core;
 
 import java.io.IOException;
-import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.tika.config.JsonConfigHelper;
+
 public class PluginsTestHelper {
     private static final Logger LOG = 
LoggerFactory.getLogger(PluginsTestHelper.class);
 
@@ -49,29 +52,25 @@ public class PluginsTestHelper {
     public static Path getFileSystemFetcherConfig(String templateName, Path 
configBase, Path fetcherBase, Path emitterBase, boolean 
emitIntermediateResults) throws Exception {
         Path pipesConfig = configBase.resolve("pipes-config.json");
 
-        Path tikaPluginsTemplate = 
Paths.get(PluginsTestHelper.class.getResource("/configs/" + 
templateName).toURI());
-        String json = Files.readString(tikaPluginsTemplate, 
StandardCharsets.UTF_8);
-
-        json = json.replace("FETCHER_BASE_PATH", fetcherBase
-                .toAbsolutePath()
-                .toString());
+        Map<String, Object> replacements = new HashMap<>();
+        replacements.put("FETCHER_BASE_PATH", fetcherBase);
 
         if (emitterBase != null) {
-            json = json.replace("EMITTER_BASE_PATH", emitterBase
-                    .toAbsolutePath()
-                    .toString());
+            replacements.put("EMITTER_BASE_PATH", emitterBase);
         }
+
         Path pwd = Paths.get("");
         Path plugins = pwd.resolve("target/plugins");
         if (Files.isDirectory(plugins)) {
-            json = json.replace("PLUGINS_PATHS", 
plugins.toAbsolutePath().toString());
+            replacements.put("PLUGINS_PATHS", plugins);
             LOG.info("found plugins path");
         } else {
-            LOG.warn("Couldn't find plugins from {}",  pwd.toAbsolutePath());
+            LOG.warn("Couldn't find plugins from {}", pwd.toAbsolutePath());
         }
-        json = json.replace("EMIT_INTERMEDIATE_RESULTS", 
String.valueOf(emitIntermediateResults));
-        json = json.replace("\\", "/");
-        Files.write(pipesConfig, json.getBytes(StandardCharsets.UTF_8));
+        replacements.put("EMIT_INTERMEDIATE_RESULTS", emitIntermediateResults);
+
+        JsonConfigHelper.writeConfigFromResource("/configs/" + templateName,
+                PluginsTestHelper.class, replacements, pipesConfig);
         return pipesConfig;
     }
 
@@ -85,14 +84,4 @@ public class PluginsTestHelper {
         }
     }
 
-    /**
-     * Converts a Path to a JSON-safe string with forward slashes.
-     * This ensures paths work correctly in JSON configs on both Windows and 
Unix systems.
-     *
-     * @param path the path to convert
-     * @return a string representation with forward slashes
-     */
-    public static String toJsonPath(Path path) {
-        return path.toString().replace("\\", "/");
-    }
 }
diff --git 
a/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/pipes/core/emitter/EmitterManagerTest.java
 
b/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/pipes/core/emitter/EmitterManagerTest.java
index 5b7ce9ced2..b2f7a9c62f 100644
--- 
a/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/pipes/core/emitter/EmitterManagerTest.java
+++ 
b/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/pipes/core/emitter/EmitterManagerTest.java
@@ -37,6 +37,7 @@ import java.util.concurrent.TimeUnit;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
+import org.apache.tika.config.JsonConfigHelper;
 import org.apache.tika.config.loader.TikaJsonConfig;
 import org.apache.tika.exception.TikaConfigException;
 import org.apache.tika.pipes.api.emitter.Emitter;
@@ -110,8 +111,8 @@ public class EmitterManagerTest {
                   },
                   "plugin-roots": "target/plugins"
                 }
-                """, PluginsTestHelper.toJsonPath(tmpDir.resolve("output1")),
-                     PluginsTestHelper.toJsonPath(tmpDir.resolve("output2")));
+                """, JsonConfigHelper.toJsonPath(tmpDir.resolve("output1")),
+                     JsonConfigHelper.toJsonPath(tmpDir.resolve("output2")));
 
         Path configPath = tmpDir.resolve("config.json");
         Files.writeString(configPath, configJson, StandardCharsets.UTF_8);
@@ -272,8 +273,8 @@ public class EmitterManagerTest {
                   },
                   "plugin-roots": "target/plugins"
                 }
-                """, PluginsTestHelper.toJsonPath(tmpDir.resolve("output1")),
-                     PluginsTestHelper.toJsonPath(tmpDir.resolve("output2")));
+                """, JsonConfigHelper.toJsonPath(tmpDir.resolve("output1")),
+                     JsonConfigHelper.toJsonPath(tmpDir.resolve("output2")));
 
         Path configPath = tmpDir.resolve("config.json");
         Files.writeString(configPath, configJson, StandardCharsets.UTF_8);
@@ -323,8 +324,8 @@ public class EmitterManagerTest {
                   },
                   "plugin-roots": "target/plugins"
                 }
-                """, PluginsTestHelper.toJsonPath(tmpDir.resolve("output1")),
-                     PluginsTestHelper.toJsonPath(tmpDir.resolve("output2")));
+                """, JsonConfigHelper.toJsonPath(tmpDir.resolve("output1")),
+                     JsonConfigHelper.toJsonPath(tmpDir.resolve("output2")));
 
         Path configPath = tmpDir.resolve("config.json");
         Files.writeString(configPath, configJson, StandardCharsets.UTF_8);
@@ -360,7 +361,7 @@ public class EmitterManagerTest {
                   "basePath": "%s",
                   "onExists": "REPLACE"
                 }
-                """, PluginsTestHelper.toJsonPath(tmpDir.resolve("output2")));
+                """, JsonConfigHelper.toJsonPath(tmpDir.resolve("output2")));
         ExtensionConfig newConfig = new ExtensionConfig("fse2", 
"file-system-emitter", newConfigJson);
 
         emitterManager.saveEmitter(newConfig);
@@ -390,7 +391,7 @@ public class EmitterManagerTest {
                   "basePath": "%s",
                   "onExists": "REPLACE"
                 }
-                """, PluginsTestHelper.toJsonPath(tmpDir.resolve("output2")));
+                """, JsonConfigHelper.toJsonPath(tmpDir.resolve("output2")));
         ExtensionConfig duplicateConfig = new ExtensionConfig("fse", 
"file-system-emitter", newConfigJson);
 
         TikaConfigException exception = 
assertThrows(TikaConfigException.class, () -> {
@@ -450,7 +451,7 @@ public class EmitterManagerTest {
                       "basePath": "%s",
                       "onExists": "REPLACE"
                     }
-                    """, PluginsTestHelper.toJsonPath(tmpDir.resolve("output" 
+ i)));
+                    """, JsonConfigHelper.toJsonPath(tmpDir.resolve("output" + 
i)));
             ExtensionConfig config2 = new ExtensionConfig("fse" + i, 
"file-system-emitter", configJson);
             emitterManager.saveEmitter(config2);
         }
@@ -484,7 +485,7 @@ public class EmitterManagerTest {
                   "basePath": "%s",
                   "onExists": "REPLACE"
                 }
-                """, PluginsTestHelper.toJsonPath(tmpDir.resolve("output2")));
+                """, JsonConfigHelper.toJsonPath(tmpDir.resolve("output2")));
         ExtensionConfig newConfig = new ExtensionConfig("fse2", 
"file-system-emitter", newConfigJson);
 
         TikaConfigException exception = 
assertThrows(TikaConfigException.class, () -> {
@@ -510,7 +511,7 @@ public class EmitterManagerTest {
                   "basePath": "%s",
                   "onExists": "REPLACE"
                 }
-                """, PluginsTestHelper.toJsonPath(tmpDir.resolve("output2")));
+                """, JsonConfigHelper.toJsonPath(tmpDir.resolve("output2")));
         ExtensionConfig newConfig = new ExtensionConfig("fse2", 
"file-system-emitter", newConfigJson);
 
         TikaConfigException exception = 
assertThrows(TikaConfigException.class, () -> {
diff --git 
a/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/pipes/core/fetcher/FetcherManagerTest.java
 
b/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/pipes/core/fetcher/FetcherManagerTest.java
index b526775478..192646f0d8 100644
--- 
a/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/pipes/core/fetcher/FetcherManagerTest.java
+++ 
b/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/pipes/core/fetcher/FetcherManagerTest.java
@@ -37,6 +37,7 @@ import java.util.concurrent.TimeUnit;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
+import org.apache.tika.config.JsonConfigHelper;
 import org.apache.tika.config.loader.TikaJsonConfig;
 import org.apache.tika.exception.TikaConfigException;
 import org.apache.tika.pipes.api.fetcher.Fetcher;
@@ -108,8 +109,8 @@ public class FetcherManagerTest {
                   },
                   "plugin-roots": "target/plugins"
                 }
-                """, PluginsTestHelper.toJsonPath(tmpDir.resolve("path1")),
-                     PluginsTestHelper.toJsonPath(tmpDir.resolve("path2")));
+                """, JsonConfigHelper.toJsonPath(tmpDir.resolve("path1")),
+                     JsonConfigHelper.toJsonPath(tmpDir.resolve("path2")));
 
         Path configPath = tmpDir.resolve("config.json");
         Files.writeString(configPath, configJson, StandardCharsets.UTF_8);
@@ -268,8 +269,8 @@ public class FetcherManagerTest {
                   },
                   "plugin-roots": "target/plugins"
                 }
-                """, PluginsTestHelper.toJsonPath(tmpDir.resolve("path1")),
-                     PluginsTestHelper.toJsonPath(tmpDir.resolve("path2")));
+                """, JsonConfigHelper.toJsonPath(tmpDir.resolve("path1")),
+                     JsonConfigHelper.toJsonPath(tmpDir.resolve("path2")));
 
         Path configPath = tmpDir.resolve("config.json");
         Files.writeString(configPath, configJson, StandardCharsets.UTF_8);
@@ -317,8 +318,8 @@ public class FetcherManagerTest {
                   },
                   "plugin-roots": "target/plugins"
                 }
-                """, PluginsTestHelper.toJsonPath(tmpDir.resolve("path1")),
-                     PluginsTestHelper.toJsonPath(tmpDir.resolve("path2")));
+                """, JsonConfigHelper.toJsonPath(tmpDir.resolve("path1")),
+                     JsonConfigHelper.toJsonPath(tmpDir.resolve("path2")));
 
         Path configPath = tmpDir.resolve("config.json");
         Files.writeString(configPath, configJson, StandardCharsets.UTF_8);
@@ -351,7 +352,7 @@ public class FetcherManagerTest {
         // Dynamically add a new fetcher configuration
         String newConfigJson = String.format(Locale.ROOT, """
                 {"basePath": "%s"}
-                """, PluginsTestHelper.toJsonPath(tmpDir.resolve("path2")));
+                """, JsonConfigHelper.toJsonPath(tmpDir.resolve("path2")));
         ExtensionConfig newConfig = new ExtensionConfig("fsf2", 
"file-system-fetcher", newConfigJson);
 
         fetcherManager.saveFetcher(newConfig);
@@ -378,7 +379,7 @@ public class FetcherManagerTest {
         // Try to add a fetcher with the same ID as existing one
         String newConfigJson = String.format(Locale.ROOT, """
                 {"basePath": "%s"}
-                """, PluginsTestHelper.toJsonPath(tmpDir.resolve("path2")));
+                """, JsonConfigHelper.toJsonPath(tmpDir.resolve("path2")));
         ExtensionConfig duplicateConfig = new ExtensionConfig("fsf", 
"file-system-fetcher", newConfigJson);
 
         TikaConfigException exception = 
assertThrows(TikaConfigException.class, () -> {
@@ -435,7 +436,7 @@ public class FetcherManagerTest {
         for (int i = 2; i <= 5; i++) {
             String configJson = String.format(Locale.ROOT, """
                     {"basePath": "%s"}
-                    """, PluginsTestHelper.toJsonPath(tmpDir.resolve("path" + 
i)));
+                    """, JsonConfigHelper.toJsonPath(tmpDir.resolve("path" + 
i)));
             ExtensionConfig config2 = new ExtensionConfig("fsf" + i, 
"file-system-fetcher", configJson);
             fetcherManager.saveFetcher(config2);
         }
@@ -466,7 +467,7 @@ public class FetcherManagerTest {
         // Try to add a fetcher - should fail
         String newConfigJson = String.format(Locale.ROOT, """
                 {"basePath": "%s"}
-                """, PluginsTestHelper.toJsonPath(tmpDir.resolve("path2")));
+                """, JsonConfigHelper.toJsonPath(tmpDir.resolve("path2")));
         ExtensionConfig newConfig = new ExtensionConfig("fsf2", 
"file-system-fetcher", newConfigJson);
 
         TikaConfigException exception = 
assertThrows(TikaConfigException.class, () -> {
@@ -489,7 +490,7 @@ public class FetcherManagerTest {
         // Try to add a fetcher - should fail
         String newConfigJson = String.format(Locale.ROOT, """
                 {"basePath": "%s"}
-                """, PluginsTestHelper.toJsonPath(tmpDir.resolve("path2")));
+                """, JsonConfigHelper.toJsonPath(tmpDir.resolve("path2")));
         ExtensionConfig newConfig = new ExtensionConfig("fsf2", 
"file-system-fetcher", newConfigJson);
 
         TikaConfigException exception = 
assertThrows(TikaConfigException.class, () -> {
diff --git 
a/tika-pipes/tika-pipes-integration-tests/src/test/resources/configs/tika-config-basic.json
 
b/tika-pipes/tika-pipes-integration-tests/src/test/resources/configs/tika-config-basic.json
index 06a0f71d0d..8ce84ffe72 100644
--- 
a/tika-pipes/tika-pipes-integration-tests/src/test/resources/configs/tika-config-basic.json
+++ 
b/tika-pipes/tika-pipes-integration-tests/src/test/resources/configs/tika-config-basic.json
@@ -41,7 +41,7 @@
   "pipes": {
     "numClients": 4,
     "timeoutMillis": 5000,
-    "emitIntermediateResults": EMIT_INTERMEDIATE_RESULTS,
+    "emitIntermediateResults": "EMIT_INTERMEDIATE_RESULTS",
     "forkedJvmArgs": ["-Xmx512m"],
     "emitStrategy": {
       "type": "DYNAMIC",
diff --git 
a/tika-pipes/tika-pipes-integration-tests/src/test/resources/configs/tika-config-passback.json
 
b/tika-pipes/tika-pipes-integration-tests/src/test/resources/configs/tika-config-passback.json
index 2473aa5572..db019f51f8 100644
--- 
a/tika-pipes/tika-pipes-integration-tests/src/test/resources/configs/tika-config-passback.json
+++ 
b/tika-pipes/tika-pipes-integration-tests/src/test/resources/configs/tika-config-passback.json
@@ -41,7 +41,7 @@
   "pipes": {
     "numClients": 4,
     "timeoutMillis": 5000,
-    "emitIntermediateResults": EMIT_INTERMEDIATE_RESULTS,
+    "emitIntermediateResults": "EMIT_INTERMEDIATE_RESULTS",
     "forkedJvmArgs": ["-Xmx512m"],
     "emitStrategy": {
       "type": "EMIT_ALL"
diff --git 
a/tika-serialization/src/main/java/org/apache/tika/config/JsonConfigHelper.java 
b/tika-serialization/src/main/java/org/apache/tika/config/JsonConfigHelper.java
new file mode 100644
index 0000000000..f73e920e1c
--- /dev/null
+++ 
b/tika-serialization/src/main/java/org/apache/tika/config/JsonConfigHelper.java
@@ -0,0 +1,288 @@
+/*
+ * 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 java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+
+/**
+ * Helper class for loading JSON config templates with placeholder replacement.
+ * <p>
+ * This provides a type-safe alternative to String.replace() for JSON configs,
+ * properly handling paths (converting backslashes to forward slashes on 
Windows)
+ * and different value types (strings, integers, doubles, booleans).
+ * <p>
+ * Example template JSON:
+ * <pre>
+ * {
+ *   "fetchers": {
+ *     "fs": {
+ *       "file-system-fetcher": {
+ *         "basePath": "FETCHER_BASE_PATH"
+ *       }
+ *     }
+ *   },
+ *   "pipes": {
+ *     "maxFiles": "MAX_FILES",
+ *     "enabled": "ENABLED"
+ *   }
+ * }
+ * </pre>
+ * <p>
+ * Usage:
+ * <pre>
+ * Map&lt;String, Object&gt; replacements = Map.of(
+ *     "FETCHER_BASE_PATH", tmpDir.resolve("input"),  // Path
+ *     "MAX_FILES", 100,                               // Integer
+ *     "ENABLED", true                                 // Boolean
+ * );
+ * JsonNode config = JsonConfigHelper.load(templatePath, replacements);
+ * </pre>
+ */
+public class JsonConfigHelper {
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    /**
+     * Loads a JSON config template from a resource path and applies 
replacements.
+     *
+     * @param resourcePath path to template resource (e.g., 
"/configs/template.json")
+     * @param clazz class to use for resource loading
+     * @param replacements map of placeholder names to replacement values
+     * @return the modified JsonNode tree
+     * @throws IOException if the template cannot be read or parsed
+     */
+    public static JsonNode loadFromResource(String resourcePath, Class<?> 
clazz,
+                                            Map<String, Object> replacements) 
throws IOException {
+        try (InputStream is = clazz.getResourceAsStream(resourcePath)) {
+            if (is == null) {
+                throw new IOException("Resource not found: " + resourcePath);
+            }
+            JsonNode root = MAPPER.readTree(is);
+            return applyReplacements(root, replacements);
+        }
+    }
+
+    /**
+     * Loads a JSON config template from a file path and applies replacements.
+     *
+     * @param templatePath path to the template file
+     * @param replacements map of placeholder names to replacement values
+     * @return the modified JsonNode tree
+     * @throws IOException if the template cannot be read or parsed
+     */
+    public static JsonNode load(Path templatePath, Map<String, Object> 
replacements)
+            throws IOException {
+        JsonNode root = MAPPER.readTree(templatePath.toFile());
+        return applyReplacements(root, replacements);
+    }
+
+    /**
+     * Loads a JSON config template from a string and applies replacements.
+     *
+     * @param jsonTemplate the JSON template string
+     * @param replacements map of placeholder names to replacement values
+     * @return the modified JsonNode tree
+     * @throws IOException if the template cannot be parsed
+     */
+    public static JsonNode loadFromString(String jsonTemplate, Map<String, 
Object> replacements)
+            throws IOException {
+        JsonNode root = MAPPER.readTree(jsonTemplate);
+        return applyReplacements(root, replacements);
+    }
+
+    /**
+     * Loads a template, applies replacements, and writes to an output file.
+     *
+     * @param templatePath path to the template file
+     * @param replacements map of placeholder names to replacement values
+     * @param outputPath path to write the result
+     * @return the output path
+     * @throws IOException if reading or writing fails
+     */
+    public static Path writeConfig(Path templatePath, Map<String, Object> 
replacements,
+                                   Path outputPath) throws IOException {
+        JsonNode config = load(templatePath, replacements);
+        String json = 
MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(config);
+        Files.writeString(outputPath, json, StandardCharsets.UTF_8);
+        return outputPath;
+    }
+
+    /**
+     * Loads a template from resources, applies replacements, and writes to an 
output file.
+     *
+     * @param resourcePath path to template resource
+     * @param clazz class to use for resource loading
+     * @param replacements map of placeholder names to replacement values
+     * @param outputPath path to write the result
+     * @return the output path
+     * @throws IOException if reading or writing fails
+     */
+    public static Path writeConfigFromResource(String resourcePath, Class<?> 
clazz,
+                                               Map<String, Object> 
replacements,
+                                               Path outputPath) throws 
IOException {
+        JsonNode config = loadFromResource(resourcePath, clazz, replacements);
+        String json = 
MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(config);
+        Files.writeString(outputPath, json, StandardCharsets.UTF_8);
+        return outputPath;
+    }
+
+    /**
+     * Applies replacements to a JsonNode tree, modifying it in place.
+     *
+     * @param root the root node to modify
+     * @param replacements map of placeholder names to replacement values
+     * @return the modified root node
+     */
+    public static JsonNode applyReplacements(JsonNode root, Map<String, 
Object> replacements) {
+        if (root.isObject()) {
+            applyToObject((ObjectNode) root, replacements);
+        } else if (root.isArray()) {
+            applyToArray((ArrayNode) root, replacements);
+        }
+        return root;
+    }
+
+    private static void applyToObject(ObjectNode node, Map<String, Object> 
replacements) {
+        Iterator<String> fieldNames = node.fieldNames();
+        while (fieldNames.hasNext()) {
+            String fieldName = fieldNames.next();
+            JsonNode child = node.get(fieldName);
+
+            if (child.isTextual()) {
+                String text = child.asText();
+                if (replacements.containsKey(text)) {
+                    node.set(fieldName, toJsonNode(replacements.get(text)));
+                }
+            } else if (child.isObject()) {
+                applyToObject((ObjectNode) child, replacements);
+            } else if (child.isArray()) {
+                applyToArray((ArrayNode) child, replacements);
+            }
+        }
+    }
+
+    private static void applyToArray(ArrayNode array, Map<String, Object> 
replacements) {
+        for (int i = 0; i < array.size(); i++) {
+            JsonNode child = array.get(i);
+
+            if (child.isTextual()) {
+                String text = child.asText();
+                if (replacements.containsKey(text)) {
+                    array.set(i, toJsonNode(replacements.get(text)));
+                }
+            } else if (child.isObject()) {
+                applyToObject((ObjectNode) child, replacements);
+            } else if (child.isArray()) {
+                applyToArray((ArrayNode) child, replacements);
+            }
+        }
+    }
+
+    /**
+     * Converts a Java object to the appropriate JsonNode type.
+     * <p>
+     * Supported types:
+     * <ul>
+     *   <li>JsonNode - used directly (for complex objects or arrays)</li>
+     *   <li>List - converted to ArrayNode</li>
+     *   <li>Path - converted to forward-slash string (Windows compatible)</li>
+     *   <li>String - TextNode</li>
+     *   <li>Integer, Long - numeric node</li>
+     *   <li>Float, Double - numeric node</li>
+     *   <li>Boolean - boolean node</li>
+     *   <li>null - null node</li>
+     * </ul>
+     *
+     * @param value the value to convert
+     * @return the appropriate JsonNode
+     */
+    private static JsonNode toJsonNode(Object value) {
+        if (value == null) {
+            return MAPPER.nullNode();
+        }
+        if (value instanceof JsonNode) {
+            // Already a JsonNode, use directly
+            return (JsonNode) value;
+        }
+        if (value instanceof List) {
+            // Convert List to ArrayNode
+            ArrayNode arrayNode = MAPPER.createArrayNode();
+            for (Object item : (List<?>) value) {
+                arrayNode.add(toJsonNode(item));
+            }
+            return arrayNode;
+        }
+        if (value instanceof Path) {
+            // Convert path to forward slashes for JSON (Windows compatibility)
+            return new TextNode(toJsonPath((Path) value));
+        }
+        if (value instanceof String) {
+            return new TextNode((String) value);
+        }
+        if (value instanceof Integer) {
+            return MAPPER.getNodeFactory().numberNode((Integer) value);
+        }
+        if (value instanceof Long) {
+            return MAPPER.getNodeFactory().numberNode((Long) value);
+        }
+        if (value instanceof Double) {
+            return MAPPER.getNodeFactory().numberNode((Double) value);
+        }
+        if (value instanceof Float) {
+            return MAPPER.getNodeFactory().numberNode((Float) value);
+        }
+        if (value instanceof Boolean) {
+            return MAPPER.getNodeFactory().booleanNode((Boolean) value);
+        }
+        // Fallback: convert to string
+        return new TextNode(value.toString());
+    }
+
+    /**
+     * Converts a Path to a JSON-safe string with forward slashes.
+     * This handles Windows paths correctly.
+     *
+     * @param path the path to convert
+     * @return the path string with forward slashes
+     */
+    public static String toJsonPath(Path path) {
+        return path.toAbsolutePath().toString().replace("\\", "/");
+    }
+
+    /**
+     * Returns the ObjectMapper used by this helper.
+     * Useful for additional JSON operations.
+     *
+     * @return the ObjectMapper instance
+     */
+    public static ObjectMapper getMapper() {
+        return MAPPER;
+    }
+}
diff --git 
a/tika-serialization/src/test/java/org/apache/tika/config/JsonConfigHelperTest.java
 
b/tika-serialization/src/test/java/org/apache/tika/config/JsonConfigHelperTest.java
new file mode 100644
index 0000000000..338f9ce594
--- /dev/null
+++ 
b/tika-serialization/src/test/java/org/apache/tika/config/JsonConfigHelperTest.java
@@ -0,0 +1,336 @@
+/*
+ * 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());
+    }
+}
diff --git a/tika-serialization/src/test/resources/configs/template-test.json 
b/tika-serialization/src/test/resources/configs/template-test.json
new file mode 100644
index 0000000000..de81f5b040
--- /dev/null
+++ b/tika-serialization/src/test/resources/configs/template-test.json
@@ -0,0 +1,20 @@
+{
+  "fetchers": {
+    "fs": {
+      "file-system-fetcher": {
+        "basePath": "FETCHER_BASE_PATH"
+      }
+    }
+  },
+  "emitters": {
+    "out": {
+      "file-system-emitter": {
+        "basePath": "EMITTER_BASE_PATH"
+      }
+    }
+  },
+  "pipes": {
+    "maxFilesWaitingInQueue": "MAX_FILES",
+    "emitIntermediateResults": "EMIT_INTERMEDIATE"
+  }
+}
diff --git 
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/CXFTestBase.java
 
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/CXFTestBase.java
index 0ecace00c0..7fc1588d0b 100644
--- 
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/CXFTestBase.java
+++ 
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/CXFTestBase.java
@@ -26,7 +26,6 @@ import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -36,6 +35,8 @@ import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.Map;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.commons.codec.digest.DigestUtils;
 import org.apache.commons.compress.archivers.ArchiveEntry;
 import org.apache.commons.compress.archivers.ArchiveInputStream;
@@ -43,7 +44,6 @@ import 
org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
 import org.apache.commons.compress.archivers.zip.ZipFile;
 import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
 import org.apache.commons.io.IOUtils;
-import org.apache.commons.io.input.UnsynchronizedByteArrayInputStream;
 import org.apache.cxf.binding.BindingFactoryManager;
 import org.apache.cxf.endpoint.Server;
 import org.apache.cxf.jaxrs.JAXRSBindingFactory;
@@ -55,6 +55,7 @@ import org.junit.jupiter.api.BeforeEach;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.tika.config.JsonConfigHelper;
 import org.apache.tika.config.loader.TikaLoader;
 import org.apache.tika.server.core.resource.TikaResource;
 import org.apache.tika.server.core.resource.UnpackerResource;
@@ -82,16 +83,8 @@ public abstract class CXFTestBase {
             }
             """;
 
-    public final static String JSON_TEMPLATE;
-
-    static {
-        try {
-            JSON_TEMPLATE = Files.readString(
-                    
Paths.get(CXFTestBase.class.getResource("/configs/cxf-test-base-template.json").toURI()),
 UTF_8);
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-    }
+    private static final String TEMPLATE_RESOURCE = 
"/configs/cxf-test-base-template.json";
+    private static final ObjectMapper MAPPER = new ObjectMapper();
 
     protected static final String endPoint = "http://localhost:"; + 
TikaServerConfig.DEFAULT_PORT;
     protected final static int DIGESTER_READ_LIMIT = 20 * 1024 * 1024;
@@ -101,23 +94,21 @@ public abstract class CXFTestBase {
     public static void createPluginsConfig(Path configPath, Path inputDir, 
Path jsonOutputDir, Path bytesOutputDir, Long timeoutMillis) throws IOException 
{
 
         Path pluginsDir = Paths.get("target/plugins");
-        if (! Files.isDirectory(pluginsDir)) {
+        if (!Files.isDirectory(pluginsDir)) {
             LOG.warn("CAN'T FIND PLUGINS DIR. pwd={}", 
Paths.get("").toAbsolutePath().toString());
         }
-        String json = CXFTestBase.JSON_TEMPLATE.replace("FETCHER_BASE_PATH", 
inputDir.toAbsolutePath().toString())
-                                   .replace("JSON_EMITTER_BASE_PATH", 
jsonOutputDir.toAbsolutePath().toString())
-                                   .replace("PLUGINS_PATHS", 
pluginsDir.toAbsolutePath().toString());
+
+        Map<String, Object> replacements = new HashMap<>();
+        replacements.put("FETCHER_BASE_PATH", inputDir);
+        replacements.put("JSON_EMITTER_BASE_PATH", jsonOutputDir);
+        replacements.put("PLUGINS_PATHS", pluginsDir);
         if (bytesOutputDir != null) {
-            json = json.replace("BYTES_EMITTER_BASE_PATH", 
bytesOutputDir.toAbsolutePath().toString());
-        }
-        json = json.replace("TIKA_CONFIG", 
configPath.toAbsolutePath().toString());
-        if (timeoutMillis != null) {
-            json = json.replace("TIMEOUT_MILLIS", timeoutMillis.toString());
-        } else {
-            json = json.replace("TIMEOUT_MILLIS", "10000");
+            replacements.put("BYTES_EMITTER_BASE_PATH", bytesOutputDir);
         }
-        json = json.replace("\\", "/");
-        Files.writeString(configPath, json, StandardCharsets.UTF_8);
+        replacements.put("TIMEOUT_MILLIS", timeoutMillis != null ? 
timeoutMillis : 10000L);
+
+        JsonConfigHelper.writeConfigFromResource(TEMPLATE_RESOURCE,
+                CXFTestBase.class, replacements, configPath);
     }
 
 
@@ -238,19 +229,16 @@ public abstract class CXFTestBase {
                     .toAbsolutePath()
                     .toString());
         }
-        String json = CXFTestBase.JSON_TEMPLATE
-                .replace("FETCHER_BASE_PATH", getPipesInputPath())
-                .replace("PLUGINS_PATHS", pluginsDir
-                        .toAbsolutePath()
-                        .toString())
-                .replace("TIMEOUT_MILLIS",  "10000");
-
-        json = json.replace("\\", "/");
-        return UnsynchronizedByteArrayInputStream
-                .builder()
-                .setByteArray(json.getBytes(UTF_8))
-                .get();
 
+        Map<String, Object> replacements = new HashMap<>();
+        replacements.put("FETCHER_BASE_PATH", getPipesInputPath());
+        replacements.put("PLUGINS_PATHS", pluginsDir);
+        replacements.put("TIMEOUT_MILLIS", 10000L);
+
+        JsonNode config = JsonConfigHelper.loadFromResource(TEMPLATE_RESOURCE,
+                CXFTestBase.class, replacements);
+        String json = MAPPER.writeValueAsString(config);
+        return new ByteArrayInputStream(json.getBytes(UTF_8));
     }
 
     protected String getPipesInputPath() {
diff --git 
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaResourceFetcherTest.java
 
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaResourceFetcherTest.java
index f76d14bb24..c4a631f978 100644
--- 
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaResourceFetcherTest.java
+++ 
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaResourceFetcherTest.java
@@ -24,8 +24,12 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import jakarta.ws.rs.core.MultivaluedHashMap;
 import jakarta.ws.rs.core.MultivaluedMap;
 import jakarta.ws.rs.core.Response;
@@ -35,6 +39,7 @@ import 
org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
+import org.apache.tika.config.JsonConfigHelper;
 import org.apache.tika.config.loader.TikaJsonConfig;
 import org.apache.tika.exception.TikaConfigException;
 import org.apache.tika.io.TikaInputStream;
@@ -62,20 +67,18 @@ public class TikaResourceFetcherTest extends CXFTestBase {
         sf.setProviders(providers);
     }
 
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
     @Override
     protected InputStream getTikaConfigInputStream() throws IOException {
-        Path inputDir = null;
-        try {
-            inputDir = Paths.get(TikaResourceFetcherTest.class
-                    .getResource("/test-documents/")
-                    .toURI());
-        } catch (URISyntaxException e) {
-            throw new RuntimeException(e);
-        }
-        String configXML = 
getStringFromInputStream(TikaResourceFetcherTest.class.getResourceAsStream("/configs/tika-config-server-fetcher-template.json"));
-
-        configXML = configXML.replace("{PORT}", "9998");
-        return new 
ByteArrayInputStream(configXML.getBytes(StandardCharsets.UTF_8));
+        Map<String, Object> replacements = new HashMap<>();
+        replacements.put("PORT", 9998);
+
+        JsonNode config = JsonConfigHelper.loadFromResource(
+                "/configs/tika-config-server-fetcher-template.json",
+                TikaResourceFetcherTest.class, replacements);
+        String json = MAPPER.writeValueAsString(config);
+        return new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
     }
 
     protected String getPipesInputPath() {
diff --git 
a/tika-server/tika-server-core/src/test/resources/configs/cxf-test-base-template.json
 
b/tika-server/tika-server-core/src/test/resources/configs/cxf-test-base-template.json
index 0efff460bd..358c023e60 100644
--- 
a/tika-server/tika-server-core/src/test/resources/configs/cxf-test-base-template.json
+++ 
b/tika-server/tika-server-core/src/test/resources/configs/cxf-test-base-template.json
@@ -25,7 +25,7 @@
   },
   "server": {
     "port": 9999,
-    "taskTimeoutMillis": TIMEOUT_MILLIS,
+    "taskTimeoutMillis": "TIMEOUT_MILLIS",
     "taskPulseMillis": 100,
     "enableUnsecureFeatures": true,
     "endpoints": [
@@ -37,7 +37,7 @@
   },
   "pipes": {
     "numClients": 2,
-    "timeoutMillis": TIMEOUT_MILLIS,
+    "timeoutMillis": "TIMEOUT_MILLIS",
     "emitIntermediateResults": false,
     "forkedJvmArgs": [
       "-Xmx512m"
diff --git 
a/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-fetcher-template.json
 
b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-fetcher-template.json
index 4dd1b826a2..9647bb1a4c 100644
--- 
a/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-fetcher-template.json
+++ 
b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-fetcher-template.json
@@ -1,6 +1,6 @@
 {
   "server": {
-    "port": "{PORT}",
+    "port": "PORT",
     "taskTimeoutMillis": 54321,
     "enableUnsecureFeatures": true,
     "taskPulseMillis": 100,
diff --git 
a/tika-server/tika-server-standard/src/test/resources/configs/cxf-test-base-template.json
 
b/tika-server/tika-server-standard/src/test/resources/configs/cxf-test-base-template.json
index 0efff460bd..358c023e60 100644
--- 
a/tika-server/tika-server-standard/src/test/resources/configs/cxf-test-base-template.json
+++ 
b/tika-server/tika-server-standard/src/test/resources/configs/cxf-test-base-template.json
@@ -25,7 +25,7 @@
   },
   "server": {
     "port": 9999,
-    "taskTimeoutMillis": TIMEOUT_MILLIS,
+    "taskTimeoutMillis": "TIMEOUT_MILLIS",
     "taskPulseMillis": 100,
     "enableUnsecureFeatures": true,
     "endpoints": [
@@ -37,7 +37,7 @@
   },
   "pipes": {
     "numClients": 2,
-    "timeoutMillis": TIMEOUT_MILLIS,
+    "timeoutMillis": "TIMEOUT_MILLIS",
     "emitIntermediateResults": false,
     "forkedJvmArgs": [
       "-Xmx512m"

Reply via email to