2222import java .io .IOException ;
2323import java .io .InputStream ;
2424import java .util .Collections ;
25- import java .util .List ;
2625import java .util .Map ;
26+ import java .util .Objects ;
2727import java .util .logging .Logger ;
2828import java .util .regex .MatchResult ;
2929import java .util .regex .Matcher ;
3030import java .util .regex .Pattern ;
3131import javax .annotation .Nullable ;
3232import org .snakeyaml .engine .v2 .api .Load ;
3333import org .snakeyaml .engine .v2 .api .LoadSettings ;
34+ import org .snakeyaml .engine .v2 .api .YamlUnicodeReader ;
3435import org .snakeyaml .engine .v2 .common .ScalarStyle ;
35- import org .snakeyaml .engine .v2 .constructor .StandardConstructor ;
36- import org .snakeyaml .engine .v2 .exceptions .ConstructorException ;
37- import org .snakeyaml .engine .v2 .exceptions .YamlEngineException ;
36+ import org .snakeyaml .engine .v2 .composer .Composer ;
3837import org .snakeyaml .engine .v2 .nodes .MappingNode ;
3938import org .snakeyaml .engine .v2 .nodes .Node ;
40- import org .snakeyaml .engine .v2 .nodes .NodeTuple ;
4139import org .snakeyaml .engine .v2 .nodes .ScalarNode ;
40+ import org .snakeyaml .engine .v2 .nodes .Tag ;
41+ import org .snakeyaml .engine .v2 .parser .ParserImpl ;
42+ import org .snakeyaml .engine .v2 .resolver .ScalarResolver ;
43+ import org .snakeyaml .engine .v2 .scanner .StreamReader ;
4244import org .snakeyaml .engine .v2 .schema .CoreSchema ;
4345
4446/**
@@ -126,8 +128,9 @@ public static OpenTelemetrySdk create(
126128 /**
127129 * Parse the {@code configuration} YAML and return the {@link OpenTelemetryConfigurationModel}.
128130 *
129- * <p>Before parsing, environment variable substitution is performed as described in {@link
130- * EnvSubstitutionConstructor}.
131+ * <p>During parsing, environment variable substitution is performed as defined in the <a
132+ * href="https://opentelemetry.io/docs/specs/otel/configuration/data-model/#environment-variable-substitution">
133+ * OpenTelemetry Configuration Data Model specification</a>.
131134 *
132135 * @throws DeclarativeConfigException if unable to parse
133136 */
@@ -149,7 +152,7 @@ static OpenTelemetryConfigurationModel parse(
149152 // Visible for testing
150153 static Object loadYaml (InputStream inputStream , Map <String , String > environmentVariables ) {
151154 LoadSettings settings = LoadSettings .builder ().setSchema (new CoreSchema ()).build ();
152- Load yaml = new Load (settings , new EnvSubstitutionConstructor ( settings , environmentVariables ) );
155+ Load yaml = new EnvLoad (settings , environmentVariables );
153156 return yaml .loadFromInputStream (inputStream );
154157 }
155158
@@ -241,89 +244,109 @@ static <M, R> R createAndMaybeCleanup(Factory<M, R> factory, SpiHelper spiHelper
241244 }
242245 }
243246
247+ private static final class EnvLoad extends Load {
248+
249+ private final LoadSettings settings ;
250+ private final Map <String , String > environmentVariables ;
251+
252+ public EnvLoad (LoadSettings settings , Map <String , String > environmentVariables ) {
253+ super (settings );
254+ this .settings = settings ;
255+ this .environmentVariables = environmentVariables ;
256+ }
257+
258+ @ Override
259+ public Object loadFromInputStream (InputStream yamlStream ) {
260+ Objects .requireNonNull (yamlStream , "InputStream cannot be null" );
261+ return loadOne (
262+ new EnvComposer (
263+ settings ,
264+ new ParserImpl (
265+ settings , new StreamReader (settings , new YamlUnicodeReader (yamlStream ))),
266+ environmentVariables ));
267+ }
268+ }
269+
244270 /**
245- * {@link StandardConstructor} which substitutes environment variables.
271+ * A YAML Composer that performs environment variable substitution according to the <a
272+ * href="https://opentelemetry.io/docs/specs/otel/configuration/data-model/#environment-variable-substitution">
273+ * OpenTelemetry Configuration Data Model specification</a>.
274+ *
275+ * <p>This composer supports:
246276 *
247- * <p>Environment variables follow the syntax {@code ${VARIABLE}}, where {@code VARIABLE} is an
248- * environment variable matching the regular expression {@code [a-zA-Z_]+[a-zA-Z0-9_]*}.
277+ * <ul>
278+ * <li>Environment variable references: {@code ${ENV_VAR}} or {@code ${env:ENV_VAR}}
279+ * <li>Default values: {@code ${ENV_VAR:-default_value}}
280+ * <li>Escape sequences: {@code $$} is replaced with a single {@code $}
281+ * </ul>
249282 *
250- * <p>Environment variable substitution only takes place on scalar values of maps. References to
251- * environment variables in keys or sets are ignored.
283+ * <p>Environment variable substitution only applies to scalar values. Mapping keys are not
284+ * candidates for substitution. Referenced environment variables that are undefined, null, or
285+ * empty are replaced with empty values unless a default value is provided.
252286 *
253- * <p>If a referenced environment variable is not defined, it is replaced with {@code ""}.
287+ * <p>The {@code $} character serves as an escape sequence where {@code $$} in the input is
288+ * translated to a single {@code $} in the output. This prevents environment variable substitution
289+ * for the escaped content.
254290 */
255- private static final class EnvSubstitutionConstructor extends StandardConstructor {
291+ private static final class EnvComposer extends Composer {
256292
257- // Load is not thread safe but this instance is always used on the same thread
258293 private final Load load ;
259294 private final Map <String , String > environmentVariables ;
295+ private final ScalarResolver scalarResolver ;
260296
261- private EnvSubstitutionConstructor (
262- LoadSettings loadSettings , Map <String , String > environmentVariables ) {
263- super (loadSettings );
264- load = new Load (loadSettings );
297+ private static final String ESCAPE_SEQUENCE = "$$" ;
298+ private static final int ESCAPE_SEQUENCE_LENGTH = ESCAPE_SEQUENCE .length ();
299+ private static final char ESCAPE_SEQUENCE_REPLACEMENT = '$' ;
300+
301+ public EnvComposer (
302+ LoadSettings settings , ParserImpl parser , Map <String , String > environmentVariables ) {
303+ super (settings , parser );
304+ this .load = new Load (settings );
265305 this .environmentVariables = environmentVariables ;
306+ this .scalarResolver = settings .getSchema ().getScalarResolver ();
266307 }
267308
268- /**
269- * Implementation is same as {@link
270- * org.snakeyaml.engine.v2.constructor.BaseConstructor#constructMapping(MappingNode)} except we
271- * override the resolution of values with our custom {@link #constructValueObject(Node)}, which
272- * performs environment variable substitution.
273- */
274309 @ Override
275- @ SuppressWarnings ({"ReturnValueIgnored" , "CatchingUnchecked" })
276- protected Map <Object , Object > constructMapping (MappingNode node ) {
277- Map <Object , Object > mapping = settings .getDefaultMap ().apply (node .getValue ().size ());
278- List <NodeTuple > nodeValue = node .getValue ();
279- for (NodeTuple tuple : nodeValue ) {
280- Node keyNode = tuple .getKeyNode ();
281- Object key = constructObject (keyNode );
282- if (key != null ) {
283- try {
284- key .hashCode (); // check circular dependencies
285- } catch (Exception e ) {
286- throw new ConstructorException (
287- "while constructing a mapping" ,
288- node .getStartMark (),
289- "found unacceptable key " + key ,
290- tuple .getKeyNode ().getStartMark (),
291- e );
292- }
293- }
294- Node valueNode = tuple .getValueNode ();
295- Object value = constructValueObject (valueNode );
296- if (keyNode .isRecursive ()) {
297- if (settings .getAllowRecursiveKeys ()) {
298- postponeMapFilling (mapping , key , value );
299- } else {
300- throw new YamlEngineException (
301- "Recursive key for mapping is detected but it is not configured to be allowed." );
302- }
303- } else {
304- mapping .put (key , value );
305- }
310+ protected Node composeValueNode (MappingNode node ) {
311+ Node itemValue = super .composeValueNode (node );
312+ if (!(itemValue instanceof ScalarNode )) {
313+ // Only apply environment variable substitution to ScalarNodes
314+ return itemValue ;
306315 }
316+ ScalarNode scalarNode = (ScalarNode ) itemValue ;
317+ String envSubstitution = envSubstitution (scalarNode .getValue ());
307318
308- return mapping ;
309- }
319+ // If the environment variable substitution does not change the value, do not modify the node
320+ if (envSubstitution .equals (scalarNode .getValue ())) {
321+ return itemValue ;
322+ }
310323
311- private static final String ESCAPE_SEQUENCE = "$$" ;
312- private static final int ESCAPE_SEQUENCE_LENGTH = ESCAPE_SEQUENCE . length ();
313- private static final char ESCAPE_SEQUENCE_REPLACEMENT = '$' ;
324+ Object envSubstitutionObj = load . loadFromString ( envSubstitution ) ;
325+ Tag tag = itemValue . getTag ();
326+ ScalarStyle scalarStyle = scalarNode . getScalarStyle () ;
314327
315- private Object constructValueObject (Node node ) {
316- Object value = constructObject (node );
317- if (!(node instanceof ScalarNode )) {
318- return value ;
319- }
320- if (!(value instanceof String )) {
321- return value ;
328+ Tag resolvedTag =
329+ envSubstitutionObj == null
330+ ? Tag .NULL
331+ : scalarResolver .resolve (envSubstitutionObj .toString (), true );
332+
333+ if ((!scalarStyle .equals (ScalarStyle .SINGLE_QUOTED )
334+ && !scalarStyle .equals (ScalarStyle .DOUBLE_QUOTED ))
335+ || itemValue .getTag ().equals (resolvedTag )) {
336+ tag = resolvedTag ;
322337 }
323338
324- String val = (String ) value ;
325- ScalarStyle scalarStyle = ((ScalarNode ) node ).getScalarStyle ();
339+ boolean resolved = true ;
340+ return new ScalarNode (
341+ tag ,
342+ resolved ,
343+ envSubstitution ,
344+ scalarStyle ,
345+ itemValue .getStartMark (),
346+ itemValue .getEndMark ());
347+ }
326348
349+ private String envSubstitution (String val ) {
327350 // Iterate through val left to right, search for escape sequence "$$"
328351 // For the substring of val between the last escape sequence and the next found, perform
329352 // environment variable substitution
@@ -346,13 +369,7 @@ private Object constructValueObject(Node node) {
346369 }
347370 }
348371
349- // If the value was double quoted, retain the double quotes so we don't change a value
350- // intended to be a string to a different type after environment variable substitution
351- if (scalarStyle == ScalarStyle .DOUBLE_QUOTED ) {
352- newVal .insert (0 , "\" " );
353- newVal .append ("\" " );
354- }
355- return load .loadFromString (newVal .toString ());
372+ return newVal .toString ();
356373 }
357374
358375 private StringBuilder envVarSubstitution (
0 commit comments