11/**
2- * (C) Copyright IBM Corporation 2017, 2025 .
2+ * (C) Copyright IBM Corporation 2017, 2026 .
33 *
44 * Licensed under the Apache License, Version 2.0 (the "License");
55 * you may not use this file except in compliance with the License.
3434import java .util .Set ;
3535import java .util .Map ;
3636import java .util .Properties ;
37+ import java .util .regex .Matcher ;
38+ import java .util .regex .Pattern ;
3739
3840import javax .xml .XMLConstants ;
3941import javax .xml .parsers .DocumentBuilder ;
4648import javax .xml .xpath .XPathFactory ;
4749
4850import io .openliberty .tools .common .plugins .util .LibertyPropFilesUtility ;
51+ import io .openliberty .tools .common .plugins .util .OSUtil ;
4952import io .openliberty .tools .common .plugins .util .PluginExecutionException ;
5053import org .apache .commons .io .comparator .NameFileComparator ;
5154import org .w3c .dom .Document ;
5255import org .w3c .dom .Element ;
5356import org .w3c .dom .NodeList ;
54- import org .w3c .dom .Node ;
55- import org .w3c .dom .NamedNodeMap ;
5657import org .xml .sax .SAXException ;
5758
5859import io .openliberty .tools .common .CommonLoggerI ;
@@ -88,6 +89,10 @@ public class ServerConfigDocument {
8889 private static final XPathExpression XPATH_SERVER_INCLUDE ;
8990 public static final XPathExpression XPATH_SERVER_VARIABLE ;
9091 private static final XPathExpression XPATH_ALL_SERVER_APPLICATIONS ;
92+ // Windows style: !VAR!
93+ private static final Pattern WINDOWS_EXPANSION_VAR_PATTERN ;
94+ // Linux style: ${VAR}
95+ private static final Pattern LINUX_EXPANSION_VAR_PATTERN ;
9196
9297
9398 static {
@@ -106,6 +111,8 @@ public class ServerConfigDocument {
106111 // correct
107112 throw new RuntimeException (ex );
108113 }
114+ WINDOWS_EXPANSION_VAR_PATTERN = Pattern .compile ("!(\\ w+)!" );
115+ LINUX_EXPANSION_VAR_PATTERN = Pattern .compile ("\\ $\\ {(\\ w+)\\ }" );
109116 }
110117
111118 public Set <String > getLocations () {
@@ -321,6 +328,64 @@ public void processServerEnv() throws Exception, FileNotFoundException {
321328 parsePropertiesFromFile (new File (libertyDirectoryPropertyToFile .get (ServerFeatureUtil .WLP_USER_DIR ),
322329 "shared" + File .separator + serverEnvString ));
323330 parsePropertiesFromFile (getFileFromConfigDirectory (serverEnvString ));
331+ Map <String , String > resolvedMap = new HashMap <>();
332+
333+ props .forEach ((k , v ) -> {
334+ String key = (String ) k ;
335+ String value = (String ) v ;
336+ Set <String > resolveInProgressProps = new HashSet <>();
337+ resolveInProgressProps .add (key );
338+ resolvedMap .put (key , resolveExpansionProperties (props , value , key , resolveInProgressProps ));
339+ });
340+
341+ // After all resolutions are calculated, update the original props
342+ props .putAll (resolvedMap );
343+ }
344+
345+ /**
346+ * Resolves property placeholders recursively with safety guards.
347+ * Uses appendReplacement to ensure a single-pass scan and strict depth control.
348+ *
349+ * @param props The properties source.
350+ * @param value The string currently being processed.
351+ * @param key key of property being processed.
352+ * @param resolveInProgressProps The set of variables in the current stack to detect loops.
353+ * @return The resolved string or raw text if depth/circularity limits are hit.
354+ */
355+ private String resolveExpansionProperties (Properties props , String value , String key , Set <String > resolveInProgressProps ) {
356+ if (value == null ) return null ;
357+ Pattern pattern = OSUtil .isWindows () ? WINDOWS_EXPANSION_VAR_PATTERN : LINUX_EXPANSION_VAR_PATTERN ;
358+ Matcher matcher = pattern .matcher (value );
359+ StringBuffer sb = new StringBuffer ();
360+ while (matcher .find ()) {
361+ String finalReplacement ;
362+ String varName = matcher .group (1 );
363+ // 2. Circular Reference Guard
364+ if (resolveInProgressProps .contains (varName )) {
365+ log .warn ("Circular reference detected: " + varName + " depends on itself in value " + value + ". Skipping expansion." );
366+ break ;
367+ }
368+ String replacement = props .getProperty (varName );
369+ if (replacement != null ) {
370+ // 3. Recursive call
371+ // Add to stack before recursing
372+ resolveInProgressProps .add (varName );
373+ try {
374+ finalReplacement = resolveExpansionProperties (props , replacement , key , resolveInProgressProps );
375+ } finally {
376+ // Remove from stack after finishing this branch (backtracking)
377+ resolveInProgressProps .remove (varName );
378+ }
379+ } else {
380+ // Variable not found in Properties; leave the original ${VAR} or !VAR!
381+ finalReplacement = matcher .group (0 ); // Keep original
382+ }
383+ matcher .appendReplacement (sb , Matcher .quoteReplacement (finalReplacement ));
384+ log .debug (String .format ("Resolving Property %s for %s. Resolved value is %s" , varName , value , sb ));
385+ }
386+ // 4. Finalize the string
387+ matcher .appendTail (sb );
388+ return sb .toString ();
324389 }
325390
326391 /**
0 commit comments