diff --git a/src/main/java/io/openliberty/tools/common/plugins/config/ServerConfigDocument.java b/src/main/java/io/openliberty/tools/common/plugins/config/ServerConfigDocument.java index 89a04f30d..fd6e80aaf 100644 --- a/src/main/java/io/openliberty/tools/common/plugins/config/ServerConfigDocument.java +++ b/src/main/java/io/openliberty/tools/common/plugins/config/ServerConfigDocument.java @@ -1,5 +1,5 @@ /** - * (C) Copyright IBM Corporation 2017, 2025. + * (C) Copyright IBM Corporation 2017, 2026. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,8 @@ import java.util.Set; import java.util.Map; import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; @@ -46,13 +48,12 @@ import javax.xml.xpath.XPathFactory; import io.openliberty.tools.common.plugins.util.LibertyPropFilesUtility; +import io.openliberty.tools.common.plugins.util.OSUtil; import io.openliberty.tools.common.plugins.util.PluginExecutionException; import org.apache.commons.io.comparator.NameFileComparator; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; -import org.w3c.dom.Node; -import org.w3c.dom.NamedNodeMap; import org.xml.sax.SAXException; import io.openliberty.tools.common.CommonLoggerI; @@ -88,6 +89,10 @@ public class ServerConfigDocument { private static final XPathExpression XPATH_SERVER_INCLUDE; public static final XPathExpression XPATH_SERVER_VARIABLE; private static final XPathExpression XPATH_ALL_SERVER_APPLICATIONS; + // Windows style: !VAR! + private static final Pattern WINDOWS_EXPANSION_VAR_PATTERN; + // Linux style: ${VAR} + private static final Pattern LINUX_EXPANSION_VAR_PATTERN; static { @@ -106,6 +111,8 @@ public class ServerConfigDocument { // correct throw new RuntimeException(ex); } + WINDOWS_EXPANSION_VAR_PATTERN = Pattern.compile("!(\\w+)!"); + LINUX_EXPANSION_VAR_PATTERN = Pattern.compile("\\$\\{(\\w+)\\}"); } public Set getLocations() { @@ -321,6 +328,64 @@ public void processServerEnv() throws Exception, FileNotFoundException { parsePropertiesFromFile(new File(libertyDirectoryPropertyToFile.get(ServerFeatureUtil.WLP_USER_DIR), "shared" + File.separator + serverEnvString)); parsePropertiesFromFile(getFileFromConfigDirectory(serverEnvString)); + Map resolvedMap = new HashMap<>(); + + props.forEach((k, v) -> { + String key = (String) k; + String value = (String) v; + Set resolveInProgressProps = new HashSet<>(); + resolveInProgressProps.add(key); + resolvedMap.put(key, resolveExpansionProperties(props, value, key, resolveInProgressProps)); + }); + + // After all resolutions are calculated, update the original props + props.putAll(resolvedMap); + } + + /** + * Resolves property placeholders recursively with safety guards. + * Uses appendReplacement to ensure a single-pass scan and strict depth control. + * + * @param props The properties source. + * @param value The string currently being processed. + * @param key key of property being processed. + * @param resolveInProgressProps The set of variables in the current stack to detect loops. + * @return The resolved string or raw text if depth/circularity limits are hit. + */ + private String resolveExpansionProperties(Properties props, String value, String key, Set resolveInProgressProps) { + if (value == null) return null; + Pattern pattern = OSUtil.isWindows() ? WINDOWS_EXPANSION_VAR_PATTERN : LINUX_EXPANSION_VAR_PATTERN; + Matcher matcher = pattern.matcher(value); + StringBuffer sb = new StringBuffer(); + while (matcher.find()) { + String finalReplacement; + String varName = matcher.group(1); + // 2. Circular Reference Guard + if (resolveInProgressProps.contains(varName)) { + log.warn("Circular reference detected: " + varName + " depends on itself in value " + value + ". Skipping expansion."); + break; + } + String replacement = props.getProperty(varName); + if (replacement != null) { + // 3. Recursive call + // Add to stack before recursing + resolveInProgressProps.add(varName); + try { + finalReplacement = resolveExpansionProperties(props, replacement, key, resolveInProgressProps); + } finally { + // Remove from stack after finishing this branch (backtracking) + resolveInProgressProps.remove(varName); + } + } else { + // Variable not found in Properties; leave the original ${VAR} or !VAR! + finalReplacement = matcher.group(0); // Keep original + } + matcher.appendReplacement(sb, Matcher.quoteReplacement(finalReplacement)); + log.debug(String.format("Resolving Property %s for %s. Resolved value is %s", varName , value , sb)); + } + // 4. Finalize the string + matcher.appendTail(sb); + return sb.toString(); } /** diff --git a/src/test/java/io/openliberty/tools/common/plugins/util/ServerConfigDocumentTest.java b/src/test/java/io/openliberty/tools/common/plugins/util/ServerConfigDocumentTest.java index 686044f17..a3cbacbc7 100644 --- a/src/test/java/io/openliberty/tools/common/plugins/util/ServerConfigDocumentTest.java +++ b/src/test/java/io/openliberty/tools/common/plugins/util/ServerConfigDocumentTest.java @@ -1,5 +1,5 @@ /** - * (C) Copyright IBM Corporation 2023, 2024. + * (C) Copyright IBM Corporation 2023, 2026. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ package io.openliberty.tools.common.plugins.util; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import org.junit.Test; @@ -91,7 +92,20 @@ public void testAppLocationUsesLibertyProperty() throws Exception { assertTrue("App location four not found.", locFourFound); assertTrue("App location five not found.", locFiveFound); assertTrue("App location six not found.", locSixFound); - + if (OSUtil.isWindows()) { + assertEquals("Variable Expanded for !VAR!", "DEFINED_VAL", scd.getProperties().getProperty("this2_value")); + assertEquals("Variable Expanded for ${VAR}", "DEFINED\\old_value\\dir", scd.getProperties().getProperty("this5_value")); + assertEquals("Variable Expanded for recursive this8_value=!this5_value!\\!overriden_value!\\dir", "DEFINED\\old_value\\dir\\old_value\\dir", scd.getProperties().getProperty("this8_value")); + assertEquals("circular or self reference value is not resolved", "var_var_!circ_v1_win!", scd.getProperties().getProperty("circ_v1_win")); + } else { + assertEquals("Variable Expanded for ${VAR}", "DEFINED_VAL", scd.getProperties().getProperty("this3_value")); + assertEquals("Variable Expanded for ${VAR}", "DEFINED/old_value/dir", scd.getProperties().getProperty("this4_value")); + assertEquals("Variable Expanded for recursive this7_value=${this3_value}/${overriden_value}/dir", "DEFINED_VAL/old_value/dir", scd.getProperties().getProperty("this7_value")); + assertEquals("circular reference value is not resolved", "DEFINED_VAL/${self_ref_value}", scd.getProperties().getProperty("self_ref_value")); + assertEquals("recursive reference resolved", "v7_v6_v5_v4_v3_v2_1", scd.getProperties().getProperty("depth_max")); + assertEquals("recursive reference resolved", "v7_v6_v5_v4_v3_v2_1", scd.getProperties().getProperty("depth_v7")); + } + assertEquals("Variable not Expanded for !this_val", "!this_val", scd.getProperties().getProperty("this6_value")); } /** diff --git a/src/test/resources/serverConfig/liberty/wlp/usr/servers/defaultServer/server.env b/src/test/resources/serverConfig/liberty/wlp/usr/servers/defaultServer/server.env index 9caccadeb..f729de54f 100644 --- a/src/test/resources/serverConfig/liberty/wlp/usr/servers/defaultServer/server.env +++ b/src/test/resources/serverConfig/liberty/wlp/usr/servers/defaultServer/server.env @@ -1,5 +1,37 @@ keystore_password=C7ANPlAi0MQD154BJ5ZOURn http.port=1111 +# --- Base Definitions --- overriden_value=old_value this_value=DEFINED -bootstrap.properties.override=false \ No newline at end of file +bootstrap.properties.override=false + +# Combines multiple recursive lookups with forward property substitution linux +this7_value=${this3_value}/${overriden_value}/dir +# Combines multiple recursive lookups linux +this4_value=${this_value}/${overriden_value}/dir +this3_value=${this_value}_VAL +# self reference -> will throw warning but no infinite loop +self_ref_value=${this3_value}/${self_ref_value} +# circular reference -> will throw warning but no infinite loop +circ_v1=var_${circ_v2} +circ_v2=var_${circ_v1} + +# Combines multiple recursive lookups with forward property substitution windows +this8_value=!this5_value!\\!overriden_value!\\dir +this2_value=!this_value!_VAL +# Combines multiple recursive lookups windows +this5_value=!this_value!\\!overriden_value!\\dir +this6_value=!this_val +# circular reference -> will throw warning but no infinite loop +circ_v1_win=var_!circ_v2_win! +circ_v2_win=var_!circ_v1_win! + +# testing max recursion level, here since max level is 5, depth_max, depth_v7 will not be resolved +depth_max=${depth_v7} +depth_v7=v7_${depth_v6} +depth_v6=v6_${depth_v5} +depth_v5=v5_${depth_v4} +depth_v4=v4_${depth_v3} +depth_v3=v3_${depth_v2} +depth_v2=v2_${depth_v1} +depth_v1=1 \ No newline at end of file