Skip to content

Commit bdf110c

Browse files
authored
Config v3 - EnvConverter #594 (#635)
1 parent c1eb973 commit bdf110c

File tree

8 files changed

+316
-3
lines changed

8 files changed

+316
-3
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ dependencies {
5959
implementation('jakarta.ws.rs:jakarta.ws.rs-api:2.1.6')
6060
implementation('com.mageddo.commons:commons-lang:0.1.21')
6161
implementation('org.apache.commons:commons-exec:1.3')
62+
implementation("org.apache.commons:commons-text:1.14.0")
6263

6364
implementation('ch.qos.logback:logback-classic:1.5.6')
6465
implementation('net.java.dev.jna:jna:5.13.0')
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package com.mageddo.dataformat.env;
2+
3+
import com.mageddo.dnsproxyserver.utils.Numbers;
4+
import com.mageddo.json.JsonUtils;
5+
import lombok.NoArgsConstructor;
6+
import org.apache.commons.lang3.StringUtils;
7+
import org.apache.commons.text.CaseUtils;
8+
9+
import javax.inject.Inject;
10+
import javax.inject.Singleton;
11+
import java.util.ArrayList;
12+
import java.util.LinkedHashMap;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.regex.Pattern;
16+
import java.util.stream.Stream;
17+
18+
@Singleton
19+
@NoArgsConstructor(onConstructor_ = @Inject)
20+
public class EnvMapper {
21+
22+
private static final String SEGMENT_SEPARATOR = "__";
23+
private static final Pattern ARRAY_INDEX_PATTERN = Pattern.compile("(.+)_([0-9]+)$");
24+
private static final Pattern INTEGER_PATTERN = Pattern.compile("-?[0-9]+");
25+
26+
private final SegmentMapper segmentMapper = new SegmentMapper();
27+
28+
public String toJson(final Map<String, String> env, final String varsPrefix) {
29+
final var root = new LinkedHashMap<String, Object>();
30+
31+
this.findMatchingEnvs(env, varsPrefix)
32+
.forEach(e -> insertPropertyAt(root, e, varsPrefix));
33+
34+
return JsonUtils.writeValueAsString(root);
35+
}
36+
37+
private void insertPropertyAt(
38+
Map<String, Object> root,
39+
Map.Entry<String, String> entry,
40+
String varsPrefix
41+
) {
42+
this.insert(root, buildEnvWithoutPrefix(entry.getKey(), varsPrefix), entry.getValue());
43+
}
44+
45+
private static String buildEnvWithoutPrefix(final String key, String prefix) {
46+
return key.substring(prefix.length());
47+
}
48+
49+
private Stream<Map.Entry<String, String>> findMatchingEnvs(Map<String, String> env, String varsPrefix) {
50+
return env.entrySet()
51+
.stream()
52+
.filter(e -> e.getKey() != null && e.getKey()
53+
.startsWith(varsPrefix))
54+
.sorted(Map.Entry.comparingByKey());
55+
}
56+
57+
@SuppressWarnings("unchecked")
58+
private void insert(final Map<String, Object> root, final String rawKey, final String rawValue) {
59+
60+
final var segments = this.segmentMapper.ofRawKey(rawKey);
61+
var current = root;
62+
63+
for (var i = 0; i < segments.size(); i++) {
64+
final var seg = segments.get(i);
65+
final var isLast = (i == segments.size() - 1);
66+
67+
if (seg.hasIndex()) {
68+
final var list = getOrCreateList(current, seg.name());
69+
ensureSize(list, seg.index());
70+
71+
if (isLast) {
72+
list.set(seg.index(), convertValue(rawValue));
73+
return;
74+
}
75+
76+
final var next = list.get(seg.index());
77+
if (next instanceof Map) {
78+
current = (Map<String, Object>) next;
79+
} else {
80+
final var newMap = new LinkedHashMap<String, Object>();
81+
list.set(seg.index(), newMap);
82+
current = newMap;
83+
}
84+
continue;
85+
}
86+
87+
if (isLast) {
88+
current.put(seg.name(), convertValue(rawValue));
89+
return;
90+
}
91+
92+
final var next = current.get(seg.name());
93+
if (next instanceof Map) {
94+
current = (Map<String, Object>) next;
95+
} else {
96+
final var newMap = new LinkedHashMap<String, Object>();
97+
current.put(seg.name(), newMap);
98+
current = newMap;
99+
}
100+
}
101+
}
102+
103+
private static class SegmentMapper {
104+
105+
private List<PathSegment> ofRawKey(final String rawKey) {
106+
final var segments = new ArrayList<PathSegment>();
107+
for (final var token : rawKey.split(SEGMENT_SEPARATOR)) {
108+
segments.add(this.parseSegment(token));
109+
}
110+
return segments;
111+
}
112+
113+
private PathSegment parseSegment(final String segment) {
114+
final var m = ARRAY_INDEX_PATTERN.matcher(segment);
115+
if (m.matches()) {
116+
final var name = this.toCamelCase(m.group(1));
117+
final var index = Integer.parseInt(m.group(2));
118+
return new PathSegment(name, index);
119+
}
120+
return new PathSegment(this.toCamelCase(segment), null);
121+
}
122+
123+
private String toCamelCase(String value) {
124+
return CaseUtils.toCamelCase(StringUtils.lowerCase(value), false, '_');
125+
}
126+
127+
/**
128+
* Ex.: "servers_0" => name=servers, index=0
129+
* "solver__remote__dnsServers_1" é segmentado por "__"
130+
*/
131+
record PathSegment(String name, Integer index) {
132+
boolean hasIndex() {
133+
return this.index != null;
134+
}
135+
}
136+
}
137+
138+
139+
@SuppressWarnings("unchecked")
140+
private List<Object> getOrCreateList(final Map<String, Object> current, final String key) {
141+
final var existing = current.get(key);
142+
if (existing instanceof List) {
143+
return (List<Object>) existing;
144+
}
145+
final var list = new ArrayList<>();
146+
current.put(key, list);
147+
return list;
148+
}
149+
150+
private void ensureSize(final List<Object> list, final int index) {
151+
while (list.size() <= index) {
152+
list.add(null);
153+
}
154+
}
155+
156+
private Object convertValue(final String rawValue) {
157+
if (rawValue == null) {
158+
return null;
159+
}
160+
161+
final var value = rawValue.trim();
162+
if (value.isEmpty()) {
163+
return "";
164+
}
165+
if ("null".equalsIgnoreCase(value)) {
166+
return null;
167+
}
168+
if ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value)) {
169+
return Boolean.valueOf(value);
170+
}
171+
172+
if (isInteger(value)) {
173+
try {
174+
final var asLong = Long.parseLong(value);
175+
if (Numbers.canBeInt(asLong)) {
176+
return (int) asLong;
177+
}
178+
return asLong;
179+
} catch (NumberFormatException ignore) {
180+
// não deve ocorrer por causa do regex, mas mantemos seguro
181+
}
182+
}
183+
return value;
184+
}
185+
186+
private static boolean isInteger(String value) {
187+
return INTEGER_PATTERN
188+
.matcher(value)
189+
.matches();
190+
}
191+
192+
}

src/main/java/com/mageddo/dnsproxyserver/config/provider/dataformatv3/converter/EnvConverter.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,35 @@
11
package com.mageddo.dnsproxyserver.config.provider.dataformatv3.converter;
22

3+
import com.mageddo.dataformat.env.EnvMapper;
34
import com.mageddo.dnsproxyserver.config.provider.dataformatv3.ConfigV3;
5+
import lombok.RequiredArgsConstructor;
46

7+
import javax.inject.Inject;
8+
import javax.inject.Singleton;
9+
import java.util.Map;
10+
11+
@Singleton
12+
@RequiredArgsConstructor(onConstructor_ = @Inject)
513
public class EnvConverter implements Converter {
14+
15+
private static final String PREFIX = "DPS_";
16+
17+
private final EnvMapper envMapper;
18+
private final JsonConverter jsonConverter;
19+
620
@Override
721
public ConfigV3 parse() {
8-
return null;
22+
return this.parse(System.getenv());
23+
}
24+
25+
ConfigV3 parse(Map<String, String> env) {
26+
final var json = this.envMapper.toJson(env, PREFIX);
27+
return this.jsonConverter.parse(json);
928
}
1029

1130
@Override
1231
public String serialize(ConfigV3 config) {
13-
return "";
32+
throw new UnsupportedOperationException();
1433
}
1534

1635
@Override

src/main/java/com/mageddo/dnsproxyserver/utils/Numbers.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,8 @@ public static Integer firstPositive(Integer... arr) {
2929
}
3030
return null;
3131
}
32+
33+
public static boolean canBeInt(long asLong) {
34+
return asLong >= Integer.MIN_VALUE && asLong <= Integer.MAX_VALUE;
35+
}
3236
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.mageddo.dataformat.env;
2+
3+
import com.mageddo.dnsproxyserver.config.provider.dataformatv3.templates.ConfigV3EnvTemplates;
4+
import com.mageddo.dnsproxyserver.config.provider.dataformatv3.templates.ConfigV3Templates;
5+
import com.mageddo.json.JsonUtils;
6+
import org.junit.jupiter.api.Test;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
10+
class EnvMapperTest {
11+
12+
private final EnvMapper mapper = new EnvMapper();
13+
14+
@Test
15+
void mustConvertEnvVariablesToJsonStructure() {
16+
// Arrange
17+
final var env = ConfigV3EnvTemplates.build();
18+
final var expected = JsonUtils.readTree(ConfigV3Templates.buildJson());
19+
20+
// Act
21+
final var json = this.mapper.toJson(env, "DPS_");
22+
final var actual = JsonUtils.readTree(json);
23+
24+
// Assert
25+
assertEquals(expected, actual);
26+
}
27+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.mageddo.dnsproxyserver.config.provider.dataformatv3.converter;
2+
3+
import com.mageddo.dataformat.env.EnvMapper;
4+
import com.mageddo.dnsproxyserver.config.provider.dataformatv3.templates.ConfigV3EnvTemplates;
5+
import com.mageddo.dnsproxyserver.config.provider.dataformatv3.templates.ConfigV3Templates;
6+
import org.junit.jupiter.api.Test;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
10+
class EnvConverterTest {
11+
12+
private final EnvConverter converter = new EnvConverter(new EnvMapper(), new JsonConverter());
13+
14+
@Test
15+
void mustParseEnvironmentIntoConfig() {
16+
// Arrange
17+
final var expected = ConfigV3Templates.build();
18+
final var env = ConfigV3EnvTemplates.build();
19+
20+
// Act
21+
final var actual = this.converter.parse(env);
22+
23+
// Assert
24+
assertEquals(expected, actual);
25+
}
26+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.mageddo.dnsproxyserver.config.provider.dataformatv3.templates;
2+
3+
import java.util.LinkedHashMap;
4+
import java.util.Map;
5+
6+
public final class ConfigV3EnvTemplates {
7+
8+
private ConfigV3EnvTemplates() {
9+
}
10+
11+
public static Map<String, String> build() {
12+
final var env = new LinkedHashMap<String, String>();
13+
env.put("DPS_VERSION", "3");
14+
env.put("DPS_SERVER__DNS__PORT", "53");
15+
env.put("DPS_SERVER__DNS__NO_ENTRIES_RESPONSE_CODE", "3");
16+
env.put("DPS_SERVER__WEB__PORT", "5380");
17+
env.put("DPS_SERVER__PROTOCOL", "UDP_TCP");
18+
env.put("DPS_SOLVER__REMOTE__ACTIVE", "true");
19+
env.put("DPS_SOLVER__REMOTE__DNS_SERVERS_0", "8.8.8.8");
20+
env.put("DPS_SOLVER__REMOTE__DNS_SERVERS_1", "4.4.4.4:53");
21+
env.put("DPS_SOLVER__REMOTE__CIRCUIT_BREAKER__NAME", "STATIC_THRESHOLD");
22+
env.put("DPS_SOLVER__DOCKER__REGISTER_CONTAINER_NAMES", "false");
23+
env.put("DPS_SOLVER__DOCKER__DOMAIN", "docker");
24+
env.put("DPS_SOLVER__DOCKER__HOST_MACHINE_FALLBACK", "true");
25+
env.put("DPS_SOLVER__DOCKER__DPS_NETWORK__NAME", "dps");
26+
env.put("DPS_SOLVER__DOCKER__DPS_NETWORK__AUTO_CREATE", "false");
27+
env.put("DPS_SOLVER__DOCKER__DPS_NETWORK__AUTO_CONNECT", "false");
28+
env.put("DPS_SOLVER__DOCKER__DOCKER_DAEMON_URI", "null");
29+
env.put("DPS_SOLVER__SYSTEM__HOST_MACHINE_HOSTNAME", "host.docker");
30+
env.put("DPS_SOLVER__LOCAL__ACTIVE_ENV", "");
31+
env.put("DPS_SOLVER__LOCAL__ENVS_0__NAME", "");
32+
env.put("DPS_SOLVER__LOCAL__ENVS_0__HOSTNAMES_0__TYPE", "A");
33+
env.put("DPS_SOLVER__LOCAL__ENVS_0__HOSTNAMES_0__HOSTNAME", "github.com");
34+
env.put("DPS_SOLVER__LOCAL__ENVS_0__HOSTNAMES_0__IP", "192.168.0.1");
35+
env.put("DPS_SOLVER__LOCAL__ENVS_0__HOSTNAMES_0__TTL", "255");
36+
env.put("DPS_SOLVER__STUB__DOMAIN_NAME", "stub");
37+
env.put("DPS_DEFAULT_DNS__ACTIVE", "true");
38+
env.put("DPS_DEFAULT_DNS__RESOLV_CONF__PATHS", "/host/etc/systemd/resolved.conf,/host/etc/resolv.conf,/etc/systemd/resolved.conf,/etc/resolv.conf");
39+
env.put("DPS_DEFAULT_DNS__RESOLV_CONF__OVERRIDE_NAME_SERVERS", "true");
40+
env.put("DPS_LOG__LEVEL", "DEBUG");
41+
env.put("DPS_LOG__FILE", "console");
42+
return env;
43+
}
44+
}

src/test/java/com/mageddo/dnsproxyserver/config/provider/dataformatv3/templates/ConfigV3Templates.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public static String buildYaml() {
5959
""");
6060
}
6161

62-
public ConfigV3 build() {
62+
public static ConfigV3 build() {
6363
return new JsonConverter().parse(buildJson());
6464
}
6565

0 commit comments

Comments
 (0)