Add support for escaping declarative config env var substitution (#7033)
This commit is contained in:
		
							parent
							
								
									2d1c14ee56
								
							
						
					
					
						commit
						dc0b4acd86
					
				|  | @ -29,6 +29,7 @@ import java.util.logging.Logger; | ||||||
| import java.util.regex.MatchResult; | import java.util.regex.MatchResult; | ||||||
| import java.util.regex.Matcher; | import java.util.regex.Matcher; | ||||||
| import java.util.regex.Pattern; | import java.util.regex.Pattern; | ||||||
|  | import javax.annotation.Nullable; | ||||||
| import org.snakeyaml.engine.v2.api.Load; | import org.snakeyaml.engine.v2.api.Load; | ||||||
| import org.snakeyaml.engine.v2.api.LoadSettings; | import org.snakeyaml.engine.v2.api.LoadSettings; | ||||||
| import org.snakeyaml.engine.v2.common.ScalarStyle; | import org.snakeyaml.engine.v2.common.ScalarStyle; | ||||||
|  | @ -308,6 +309,10 @@ public final class DeclarativeConfiguration { | ||||||
|       return mapping; |       return mapping; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private static final String ESCAPE_SEQUENCE = "$$"; | ||||||
|  |     private static final int ESCAPE_SEQUENCE_LENGTH = ESCAPE_SEQUENCE.length(); | ||||||
|  |     private static final char ESCAPE_SEQUENCE_REPLACEMENT = '$'; | ||||||
|  | 
 | ||||||
|     private Object constructValueObject(Node node) { |     private Object constructValueObject(Node node) { | ||||||
|       Object value = constructObject(node); |       Object value = constructObject(node); | ||||||
|       if (!(node instanceof ScalarNode)) { |       if (!(node instanceof ScalarNode)) { | ||||||
|  | @ -318,14 +323,53 @@ public final class DeclarativeConfiguration { | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       String val = (String) value; |       String val = (String) value; | ||||||
|  |       ScalarStyle scalarStyle = ((ScalarNode) node).getScalarStyle(); | ||||||
|  | 
 | ||||||
|  |       // Iterate through val left to right, search for escape sequence "$$" | ||||||
|  |       // For the substring of val between the last escape sequence and the next found, perform | ||||||
|  |       // environment variable substitution | ||||||
|  |       // Add the escape replacement character '$' in place of each escape sequence found | ||||||
|  | 
 | ||||||
|  |       int lastEscapeIndexEnd = 0; | ||||||
|  |       StringBuilder newVal = null; | ||||||
|  |       while (true) { | ||||||
|  |         int escapeIndex = val.indexOf(ESCAPE_SEQUENCE, lastEscapeIndexEnd); | ||||||
|  |         int substitutionEndIndex = escapeIndex == -1 ? val.length() : escapeIndex; | ||||||
|  |         newVal = envVarSubstitution(newVal, val, lastEscapeIndexEnd, substitutionEndIndex); | ||||||
|  |         if (escapeIndex == -1) { | ||||||
|  |           break; | ||||||
|  |         } else { | ||||||
|  |           newVal.append(ESCAPE_SEQUENCE_REPLACEMENT); | ||||||
|  |         } | ||||||
|  |         lastEscapeIndexEnd = escapeIndex + ESCAPE_SEQUENCE_LENGTH; | ||||||
|  |         if (lastEscapeIndexEnd >= val.length()) { | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // If the value was double quoted, retain the double quotes so we don't change a value | ||||||
|  |       // intended to be a string to a different type after environment variable substitution | ||||||
|  |       if (scalarStyle == ScalarStyle.DOUBLE_QUOTED) { | ||||||
|  |         newVal.insert(0, "\""); | ||||||
|  |         newVal.append("\""); | ||||||
|  |       } | ||||||
|  |       return load.loadFromString(newVal.toString()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private StringBuilder envVarSubstitution( | ||||||
|  |         @Nullable StringBuilder newVal, String source, int startIndex, int endIndex) { | ||||||
|  |       String val = source.substring(startIndex, endIndex); | ||||||
|       Matcher matcher = ENV_VARIABLE_REFERENCE.matcher(val); |       Matcher matcher = ENV_VARIABLE_REFERENCE.matcher(val); | ||||||
|  | 
 | ||||||
|       if (!matcher.find()) { |       if (!matcher.find()) { | ||||||
|         return value; |         return newVal == null ? new StringBuilder(val) : newVal.append(val); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (newVal == null) { | ||||||
|  |         newVal = new StringBuilder(); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       int offset = 0; |       int offset = 0; | ||||||
|       StringBuilder newVal = new StringBuilder(); |  | ||||||
|       ScalarStyle scalarStyle = ((ScalarNode) node).getScalarStyle(); |  | ||||||
|       do { |       do { | ||||||
|         MatchResult matchResult = matcher.toMatchResult(); |         MatchResult matchResult = matcher.toMatchResult(); | ||||||
|         String envVarKey = matcher.group(1); |         String envVarKey = matcher.group(1); | ||||||
|  | @ -340,13 +384,8 @@ public final class DeclarativeConfiguration { | ||||||
|       if (offset != val.length()) { |       if (offset != val.length()) { | ||||||
|         newVal.append(val, offset, val.length()); |         newVal.append(val, offset, val.length()); | ||||||
|       } |       } | ||||||
|       // If the value was double quoted, retain the double quotes so we don't change a value | 
 | ||||||
|       // intended to be a string to a different type after environment variable substitution |       return newVal; | ||||||
|       if (scalarStyle == ScalarStyle.DOUBLE_QUOTED) { |  | ||||||
|         newVal.insert(0, "\""); |  | ||||||
|         newVal.append("\""); |  | ||||||
|       } |  | ||||||
|       return load.loadFromString(newVal.toString()); |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -593,8 +593,10 @@ class DeclarativeConfigurationParseTest { | ||||||
|   @MethodSource("envVarSubstitutionArgs") |   @MethodSource("envVarSubstitutionArgs") | ||||||
|   void envSubstituteAndLoadYaml(String rawYaml, Object expectedYamlResult) { |   void envSubstituteAndLoadYaml(String rawYaml, Object expectedYamlResult) { | ||||||
|     Map<String, String> environmentVariables = new HashMap<>(); |     Map<String, String> environmentVariables = new HashMap<>(); | ||||||
|  |     environmentVariables.put("FOO", "BAR"); | ||||||
|     environmentVariables.put("STR_1", "value1"); |     environmentVariables.put("STR_1", "value1"); | ||||||
|     environmentVariables.put("STR_2", "value2"); |     environmentVariables.put("STR_2", "value2"); | ||||||
|  |     environmentVariables.put("VALUE_WITH_ESCAPE", "value$$"); | ||||||
|     environmentVariables.put("EMPTY_STR", ""); |     environmentVariables.put("EMPTY_STR", ""); | ||||||
|     environmentVariables.put("BOOL", "true"); |     environmentVariables.put("BOOL", "true"); | ||||||
|     environmentVariables.put("INT", "1"); |     environmentVariables.put("INT", "1"); | ||||||
|  | @ -657,7 +659,30 @@ class DeclarativeConfigurationParseTest { | ||||||
|         Arguments.of("key1: \"${EMPTY_STR}\"\n", mapOf(entry("key1", ""))), |         Arguments.of("key1: \"${EMPTY_STR}\"\n", mapOf(entry("key1", ""))), | ||||||
|         Arguments.of("key1: \"${BOOL}\"\n", mapOf(entry("key1", "true"))), |         Arguments.of("key1: \"${BOOL}\"\n", mapOf(entry("key1", "true"))), | ||||||
|         Arguments.of("key1: \"${INT}\"\n", mapOf(entry("key1", "1"))), |         Arguments.of("key1: \"${INT}\"\n", mapOf(entry("key1", "1"))), | ||||||
|         Arguments.of("key1: \"${FLOAT}\"\n", mapOf(entry("key1", "1.1")))); |         Arguments.of("key1: \"${FLOAT}\"\n", mapOf(entry("key1", "1.1"))), | ||||||
|  |         // Escaped | ||||||
|  |         Arguments.of("key1: ${FOO}\n", mapOf(entry("key1", "BAR"))), | ||||||
|  |         Arguments.of("key1: $${FOO}\n", mapOf(entry("key1", "${FOO}"))), | ||||||
|  |         Arguments.of("key1: $$${FOO}\n", mapOf(entry("key1", "$BAR"))), | ||||||
|  |         Arguments.of("key1: $$$${FOO}\n", mapOf(entry("key1", "$${FOO}"))), | ||||||
|  |         Arguments.of("key1: a $$ b\n", mapOf(entry("key1", "a $ b"))), | ||||||
|  |         Arguments.of("key1: $$ b\n", mapOf(entry("key1", "$ b"))), | ||||||
|  |         Arguments.of("key1: a $$\n", mapOf(entry("key1", "a $"))), | ||||||
|  |         Arguments.of("key1: a $ b\n", mapOf(entry("key1", "a $ b"))), | ||||||
|  |         Arguments.of("key1: $${STR_1}\n", mapOf(entry("key1", "${STR_1}"))), | ||||||
|  |         Arguments.of("key1: $${STR_1}$${STR_1}\n", mapOf(entry("key1", "${STR_1}${STR_1}"))), | ||||||
|  |         Arguments.of("key1: $${STR_1}$$\n", mapOf(entry("key1", "${STR_1}$"))), | ||||||
|  |         Arguments.of("key1: $$${STR_1}\n", mapOf(entry("key1", "$value1"))), | ||||||
|  |         Arguments.of("key1: \"$${STR_1}\"\n", mapOf(entry("key1", "${STR_1}"))), | ||||||
|  |         Arguments.of("key1: $${STR_1} ${STR_2}\n", mapOf(entry("key1", "${STR_1} value2"))), | ||||||
|  |         Arguments.of("key1: $${STR_1} $${STR_2}\n", mapOf(entry("key1", "${STR_1} ${STR_2}"))), | ||||||
|  |         Arguments.of("key1: $${NOT_SET:-value1}\n", mapOf(entry("key1", "${NOT_SET:-value1}"))), | ||||||
|  |         Arguments.of("key1: $${STR_1:-fallback}\n", mapOf(entry("key1", "${STR_1:-fallback}"))), | ||||||
|  |         Arguments.of("key1: $${STR_1:-${STR_1}}\n", mapOf(entry("key1", "${STR_1:-value1}"))), | ||||||
|  |         Arguments.of("key1: ${NOT_SET:-${FALLBACK}}\n", mapOf(entry("key1", "${FALLBACK}"))), | ||||||
|  |         Arguments.of( | ||||||
|  |             "key1: ${NOT_SET:-$${FALLBACK}}\n", mapOf(entry("key1", "${NOT_SET:-${FALLBACK}}"))), | ||||||
|  |         Arguments.of("key1: ${VALUE_WITH_ESCAPE}\n", mapOf(entry("key1", "value$$")))); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private static <K, V> Map.Entry<K, V> entry(K key, @Nullable V value) { |   private static <K, V> Map.Entry<K, V> entry(K key, @Nullable V value) { | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue