Skip to content

Commit 73bc32e

Browse files
committed
Add better support for environment variables
1 parent 3346f34 commit 73bc32e

File tree

10 files changed

+1445
-382
lines changed

10 files changed

+1445
-382
lines changed

README.md

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ wiki!
3030
* Option to customize null handling
3131
* Option to customize serialization by providing your own serializers
3232
* Option to add headers and footers to configuration files
33+
* Option to overwrite configuration values with environment variables
3334
* ...and a few more!
3435

3536
## Usage example
@@ -502,7 +503,7 @@ serializing and deserializing a configuration:
502503
are treated as missing and are, therefore, handled as described in the section
503504
above.
504505
* By setting `inputNulls` to true, null values read from the configuration file
505-
override the corresponding default values of a configuration class with null
506+
overwrite the corresponding default values of a configuration class with null
506507
or set the component value of a record type to null. If the configuration
507508
element type is primitive, an exception is thrown.
508509

@@ -943,6 +944,123 @@ ConfigurationProperties.newBuilder()
943944
.build();
944945
```
945946

947+
### Overwriting configuration values with environment variables
948+
949+
You can allow users of your configuration to overwrite values that come from
950+
configuration files with values that are provided by environment variables.
951+
When a configuration is loaded, boolean values, numbers, and strings that appear
952+
in that configuration files can be overwritten with values that come from
953+
environment variables.
954+
955+
To enable support for overwriting config values with environment variables
956+
first create a `ConfigurationProperties.EnvVarResolutionConfiguration` object
957+
and then configure your `ConfigurationProperties` instance with it:
958+
959+
```java
960+
final var envVarConf = ConfigurationProperties.EnvVarResolutionConfiguration
961+
.resolveEnvVarsWithPrefix(prefix, caseSensitiveResolution);
962+
final var properties = ConfigurationProperties.newBuilder()
963+
.setEnvVarResolutionConfiguration(envVarConf)
964+
// ... build
965+
```
966+
967+
The `resolveEnvVarsWithPrefix` method returns a configuration object that
968+
resolves environment variables that start with the given prefix.
969+
970+
If you specify a non-empty prefix, then only variables that start with that
971+
prefix will be resolved when loading your configuration. For example, if you
972+
choose `MY_PREFIX_` as your prefix, only environment variables that start
973+
with `MY_PREFIX_` will be able to overwrite configuration values. Specifying a
974+
non-empty prefix that is specific to your project is highly recommended.
975+
976+
#### Environment variable names
977+
978+
To be able to overwrite configuration values, you need to name your environment
979+
variables in a specific way (see example below):
980+
981+
- To overwrite a boolean, number, or string value of a field, use an environment
982+
variable whose name matches the name of the field. The environment variable must
983+
be uppercase if case-sensitive resolution is disabled.
984+
- You can target nested elements by joining all field names that lead to that
985+
element with an underscore `_`.
986+
- You can target list elements by using the index of an element as the field name.
987+
988+
For example, in the following configuration file
989+
990+
```yaml
991+
a: 0
992+
b:
993+
c: 1
994+
d:
995+
- 2
996+
- 3
997+
e:
998+
- f: 4
999+
g: 5
1000+
```
1001+
1002+
`a` is a simple value, `b` is a map, `d` is a list, and `e` is a list of
1003+
maps. Then, under the assumption that you aren't using a prefix and that
1004+
case-sensitive resolution is disabled, you can overwrite the values from 0 to 5
1005+
using the following environment variables, respectively:
1006+
`A`, `B_C`, `D_0`, `D_1`, `E_0_F`, and `E_0_G`.
1007+
1008+
#### Case-sensitivity
1009+
1010+
**NOTE:** Environment variables are case-sensitive on UNIX systems and
1011+
case-insensitive on Windows.
1012+
1013+
If you want to (or have to, because you are on Windows) target the values of
1014+
your configuration using uppercase environment variables (irrespective of the
1015+
actual casing of the name of the fields), then you have to disable
1016+
case-sensitive resolution. For example, if you have a configuration file with
1017+
the following content
1018+
```yaml
1019+
alPHA:
1020+
be_ta: 1
1021+
ga_mma:
1022+
dELTa: 2
1023+
```
1024+
then, with `caseSensitiveResolution` set to `false`, you can overwrite the values
1025+
`1` and `2` using the environment variables `ALPHA_BE_TA` and `GA_MMA_DELTA`,
1026+
respectively.
1027+
1028+
If your configuration contains paths that differ only in their case but are
1029+
otherwise the same, and if you want to target these paths individually, then
1030+
you have to enable case-sensitive resolution. For example, if you have a
1031+
configuration file with the following content
1032+
```yaml
1033+
alpha:
1034+
beta: 1
1035+
ALPHA:
1036+
BETA: 2
1037+
```
1038+
and you want to target the values of `beta` and `BETA` individually, then you
1039+
have to set `caseSensitiveResolution` to `true`. After doing so you can
1040+
overwrite the values `1` and `2` by using the environment variables `alpha_beta`
1041+
and `ALPHA_BETA`, respectively.
1042+
1043+
#### Restrictions
1044+
1045+
The values provided by environment variables come as strings and need to be
1046+
converted to the correct target type. Because the resolution of environment
1047+
variables happens very early in the process (before the type information of the
1048+
configuration type is available) there are a few restrictions on how
1049+
environment variables are resolved and which values can be overwritten:
1050+
1051+
- Environment variables can only be used to overwrite simple values.
1052+
Trying to overwrite a collection or map will cause an exception.
1053+
- Environment variables cannot be used to insert new values into collections
1054+
or maps.
1055+
- Overwriting `null` values is not possible because `null`s don't carry any type
1056+
information.
1057+
- To overwrite a boolean value, an environment variable must be assigned the
1058+
string `true` or `false` (using any casing).
1059+
- Numbers that, when a configuration file is read, are interpreted as integers
1060+
(because they don't contain a decimal point), (currently) cannot be overwritten
1061+
by floating point numbers. This applies even if the configuration type actually
1062+
expects a floating point number.
1063+
9461064
### Changing the type of configuration elements
9471065

9481066
Changing the type of configuration elements is not supported. If you change the

configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java

Lines changed: 179 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package de.exlll.configlib;
22

33
import java.lang.reflect.Type;
4-
import java.util.Collections;
5-
import java.util.HashMap;
6-
import java.util.LinkedHashMap;
7-
import java.util.Map;
4+
import java.util.*;
85
import java.util.function.Function;
96
import java.util.function.Predicate;
107
import java.util.function.UnaryOperator;
@@ -28,6 +25,7 @@ public class ConfigurationProperties {
2825
private final boolean outputNulls;
2926
private final boolean inputNulls;
3027
private final boolean serializeSetsAsLists;
28+
private final EnvVarResolutionConfiguration envVarResolutionConfiguration;
3129

3230
/**
3331
* Constructs a new instance of this class with values taken from the given builder.
@@ -49,6 +47,10 @@ protected ConfigurationProperties(Builder<?> builder) {
4947
this.outputNulls = builder.outputNulls;
5048
this.inputNulls = builder.inputNulls;
5149
this.serializeSetsAsLists = builder.serializeSetsAsLists;
50+
this.envVarResolutionConfiguration = requireNonNull(
51+
builder.envVarResolutionConfiguration,
52+
"environment variable resolution configuration"
53+
);
5254
}
5355

5456
/**
@@ -100,9 +102,18 @@ public static abstract class Builder<B extends Builder<B>> {
100102
private boolean outputNulls = false;
101103
private boolean inputNulls = false;
102104
private boolean serializeSetsAsLists = true;
105+
private EnvVarResolutionConfiguration envVarResolutionConfiguration
106+
= EnvVarResolutionConfiguration.disabled();
103107

108+
/** Default constructor */
104109
protected Builder() {}
105110

111+
/**
112+
* A constructor that initializes the values of this builder with the values
113+
* of the given {@code ConfigurationProperties} object.
114+
*
115+
* @param properties the properties used to initialize the values of this builder
116+
*/
106117
protected Builder(ConfigurationProperties properties) {
107118
this.serializersByType.putAll(properties.serializersByType);
108119
this.serializerFactoriesByType.putAll(properties.serializerFactoriesByType);
@@ -113,6 +124,7 @@ protected Builder(ConfigurationProperties properties) {
113124
this.outputNulls = properties.outputNulls;
114125
this.inputNulls = properties.inputNulls;
115126
this.serializeSetsAsLists = properties.serializeSetsAsLists;
127+
this.envVarResolutionConfiguration = properties.envVarResolutionConfiguration;
116128
}
117129

118130
/**
@@ -289,6 +301,26 @@ final B serializeSetsAsLists(boolean serializeSetsAsLists) {
289301
return getThis();
290302
}
291303

304+
/**
305+
* Configures whether and how environment variables should be resolved.
306+
* <p>
307+
* The default is {@link EnvVarResolutionConfiguration#disabled()} which
308+
* is a configuration that disables the resolution of environment variables.
309+
*
310+
* @param configuration the {@code EnvVarResolutionConfiguration}
311+
* @return this builder
312+
* @throws NullPointerException if {@code configuration} is null
313+
*/
314+
public final B setEnvVarResolutionConfiguration(
315+
EnvVarResolutionConfiguration configuration
316+
) {
317+
this.envVarResolutionConfiguration = requireNonNull(
318+
configuration,
319+
"environment variable resolution configuration"
320+
);
321+
return getThis();
322+
}
323+
292324
/**
293325
* Builds a {@code ConfigurationProperties} instance.
294326
*
@@ -304,6 +336,139 @@ final B serializeSetsAsLists(boolean serializeSetsAsLists) {
304336
protected abstract B getThis();
305337
}
306338

339+
/**
340+
* A collection of values used to configure the resolution of environment variables.
341+
*/
342+
public static final class EnvVarResolutionConfiguration {
343+
private static final EnvVarResolutionConfiguration DISABLED =
344+
new EnvVarResolutionConfiguration();
345+
private final boolean resolveEnvVars;
346+
private final String prefix;
347+
private final boolean caseSensitive;
348+
349+
/**
350+
* Returns a configuration object that disables the resolution of
351+
* environment variables.
352+
*
353+
* @return configuration object that disables the resolution of
354+
* environment variables.
355+
*/
356+
public static EnvVarResolutionConfiguration disabled() {
357+
return DISABLED;
358+
}
359+
360+
/**
361+
* Returns a configuration object that resolves environment variables
362+
* that start with the given prefix. Specifying a non-empty prefix that
363+
* is specific to your project is highly recommended.
364+
* <p>
365+
* On UNIX systems environment variables are case-sensitive.
366+
* On Windows they are case-insensitive.
367+
* <ul>
368+
* <li>
369+
* If you want to (or have to, because you are on Windows) target the
370+
* values of your configuration using uppercase environment variables,
371+
* then you have to disable case-sensitive resolution.
372+
* <p>
373+
* For example, if you have a YAML configuration with the following content
374+
* <pre>
375+
* alPHA:
376+
* be_ta: 1
377+
* ga_mma:
378+
* dELTa: 2
379+
* </pre>
380+
* then, with {@code caseSensitive} set to {@code false}, you can overwrite
381+
* the values 1 and 2 using the environment variables
382+
* {@code ALPHA_BE_TA} and {@code GA_MMA_DELTA}, respectively.
383+
* </li>
384+
* <li>
385+
* If your configuration contains paths that differ only in their case,
386+
* but are otherwise the same, and if you want to target these paths
387+
* individually, then you have to enable case-sensitive resolution.
388+
* <p>
389+
* For example, if you have a YAML configuration with the following content
390+
* <pre>
391+
* alpha:
392+
* beta: 1
393+
* ALPHA:
394+
* BETA: 2
395+
* </pre>
396+
* and you want to target the values of {@code beta} and {@code BETA}
397+
* individually, then you have to set {@code caseSensitive} to {@code true}.
398+
* After doing so you can overwrite the values 1 and 2 by using the environment
399+
* variables {@code alpha_beta} and {@code ALPHA_BETA}, respectively.
400+
* </li>
401+
* <li>
402+
* If you specify a non-empty prefix, then all variables that you want to
403+
* be resolved have to start with that prefix. For example, if you choose
404+
* {@code MY_PREFIX_} as your prefix, then the four variables listed
405+
* above have to be named {@code MY_PREFIX_ALPHA_BE_TA},
406+
* {@code MY_PREFIX_GA_MMA_DELTA}, {@code MY_PREFIX_alpha_beta}, and
407+
* {@code MY_PREFIX_ALPHA_BETA}, respectively.
408+
* </li>
409+
* </ul>
410+
*
411+
* @param prefix string the environment variables have to be prefixed with
412+
* @param caseSensitive specifies whether the resolution should be case-sensitive
413+
* @return configuration object that resolves environment variables
414+
* that start with the given prefix
415+
* @throws NullPointerException if {@code prefix} is null
416+
*/
417+
public static EnvVarResolutionConfiguration resolveEnvVarsWithPrefix(
418+
String prefix,
419+
boolean caseSensitive
420+
) {
421+
return new EnvVarResolutionConfiguration(true, prefix, caseSensitive);
422+
}
423+
424+
private EnvVarResolutionConfiguration() {
425+
this(false, "", false);
426+
}
427+
428+
// create 'Builder' class if more configuration options are added
429+
private EnvVarResolutionConfiguration(
430+
boolean resolveEnvVars,
431+
String prefix,
432+
boolean caseSensitive
433+
) {
434+
this.resolveEnvVars = resolveEnvVars;
435+
this.prefix = requireNonNull(prefix, "prefix");
436+
this.caseSensitive = caseSensitive;
437+
}
438+
439+
/**
440+
* Returns whether environment variables should be resolved.
441+
*
442+
* @return whether environment variables should be resolved
443+
*/
444+
public boolean resolveEnvVars() {
445+
return resolveEnvVars;
446+
}
447+
448+
/**
449+
* Returns the string with which the environment variables must begin
450+
* in order to be resolved.
451+
*
452+
* @return the string environment variables have to begin with to be resolved
453+
* @see EnvVarResolutionConfiguration#resolveEnvVarsWithPrefix(String, boolean)
454+
*/
455+
public String prefix() {
456+
return prefix;
457+
}
458+
459+
/**
460+
* Returns whether the resolution of environment variables should be
461+
* case-sensitive.
462+
*
463+
* @return whether the resolution of environment variables should be
464+
* case-sensitive
465+
* @see EnvVarResolutionConfiguration#resolveEnvVarsWithPrefix(String, boolean)
466+
*/
467+
public boolean caseSensitive() {
468+
return caseSensitive;
469+
}
470+
}
471+
307472
/**
308473
* Returns the field filter used to filter the fields of a configuration class.
309474
*
@@ -388,4 +553,14 @@ public final boolean inputNulls() {
388553
final boolean serializeSetsAsLists() {
389554
return serializeSetsAsLists;
390555
}
556+
557+
/**
558+
* Returns the configuration that determines whether and how environment
559+
* variables should be resolved.
560+
*
561+
* @return the configuration
562+
*/
563+
public final EnvVarResolutionConfiguration getEnvVarResolutionConfiguration() {
564+
return envVarResolutionConfiguration;
565+
}
391566
}

0 commit comments

Comments
 (0)