diff --git a/build.gradle b/build.gradle index 1c8ad9150..2f61695f5 100644 --- a/build.gradle +++ b/build.gradle @@ -59,6 +59,7 @@ dependencies { implementation('jakarta.ws.rs:jakarta.ws.rs-api:2.1.6') implementation('com.mageddo.commons:commons-lang:0.1.21') implementation('org.apache.commons:commons-exec:1.3') + implementation("org.apache.commons:commons-text:1.14.0") implementation('ch.qos.logback:logback-classic:1.5.6') implementation('net.java.dev.jna:jna:5.13.0') diff --git a/src/main/java/com/mageddo/dataformat/env/EnvMapper.java b/src/main/java/com/mageddo/dataformat/env/EnvMapper.java new file mode 100644 index 000000000..0c98daac0 --- /dev/null +++ b/src/main/java/com/mageddo/dataformat/env/EnvMapper.java @@ -0,0 +1,192 @@ +package com.mageddo.dataformat.env; + +import com.mageddo.dnsproxyserver.utils.Numbers; +import com.mageddo.json.JsonUtils; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.CaseUtils; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +@Singleton +@NoArgsConstructor(onConstructor_ = @Inject) +public class EnvMapper { + + private static final String SEGMENT_SEPARATOR = "__"; + private static final Pattern ARRAY_INDEX_PATTERN = Pattern.compile("(.+)_([0-9]+)$"); + private static final Pattern INTEGER_PATTERN = Pattern.compile("-?[0-9]+"); + + private final SegmentMapper segmentMapper = new SegmentMapper(); + + public String toJson(final Map env, final String varsPrefix) { + final var root = new LinkedHashMap(); + + this.findMatchingEnvs(env, varsPrefix) + .forEach(e -> insertPropertyAt(root, e, varsPrefix)); + + return JsonUtils.writeValueAsString(root); + } + + private void insertPropertyAt( + Map root, + Map.Entry entry, + String varsPrefix + ) { + this.insert(root, buildEnvWithoutPrefix(entry.getKey(), varsPrefix), entry.getValue()); + } + + private static String buildEnvWithoutPrefix(final String key, String prefix) { + return key.substring(prefix.length()); + } + + private Stream> findMatchingEnvs(Map env, String varsPrefix) { + return env.entrySet() + .stream() + .filter(e -> e.getKey() != null && e.getKey() + .startsWith(varsPrefix)) + .sorted(Map.Entry.comparingByKey()); + } + + @SuppressWarnings("unchecked") + private void insert(final Map root, final String rawKey, final String rawValue) { + + final var segments = this.segmentMapper.ofRawKey(rawKey); + var current = root; + + for (var i = 0; i < segments.size(); i++) { + final var seg = segments.get(i); + final var isLast = (i == segments.size() - 1); + + if (seg.hasIndex()) { + final var list = getOrCreateList(current, seg.name()); + ensureSize(list, seg.index()); + + if (isLast) { + list.set(seg.index(), convertValue(rawValue)); + return; + } + + final var next = list.get(seg.index()); + if (next instanceof Map) { + current = (Map) next; + } else { + final var newMap = new LinkedHashMap(); + list.set(seg.index(), newMap); + current = newMap; + } + continue; + } + + if (isLast) { + current.put(seg.name(), convertValue(rawValue)); + return; + } + + final var next = current.get(seg.name()); + if (next instanceof Map) { + current = (Map) next; + } else { + final var newMap = new LinkedHashMap(); + current.put(seg.name(), newMap); + current = newMap; + } + } + } + + private static class SegmentMapper { + + private List ofRawKey(final String rawKey) { + final var segments = new ArrayList(); + for (final var token : rawKey.split(SEGMENT_SEPARATOR)) { + segments.add(this.parseSegment(token)); + } + return segments; + } + + private PathSegment parseSegment(final String segment) { + final var m = ARRAY_INDEX_PATTERN.matcher(segment); + if (m.matches()) { + final var name = this.toCamelCase(m.group(1)); + final var index = Integer.parseInt(m.group(2)); + return new PathSegment(name, index); + } + return new PathSegment(this.toCamelCase(segment), null); + } + + private String toCamelCase(String value) { + return CaseUtils.toCamelCase(StringUtils.lowerCase(value), false, '_'); + } + + /** + * Ex.: "servers_0" => name=servers, index=0 + * "solver__remote__dnsServers_1" é segmentado por "__" + */ + record PathSegment(String name, Integer index) { + boolean hasIndex() { + return this.index != null; + } + } + } + + + @SuppressWarnings("unchecked") + private List getOrCreateList(final Map current, final String key) { + final var existing = current.get(key); + if (existing instanceof List) { + return (List) existing; + } + final var list = new ArrayList<>(); + current.put(key, list); + return list; + } + + private void ensureSize(final List list, final int index) { + while (list.size() <= index) { + list.add(null); + } + } + + private Object convertValue(final String rawValue) { + if (rawValue == null) { + return null; + } + + final var value = rawValue.trim(); + if (value.isEmpty()) { + return ""; + } + if ("null".equalsIgnoreCase(value)) { + return null; + } + if ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value)) { + return Boolean.valueOf(value); + } + + if (isInteger(value)) { + try { + final var asLong = Long.parseLong(value); + if (Numbers.canBeInt(asLong)) { + return (int) asLong; + } + return asLong; + } catch (NumberFormatException ignore) { + // não deve ocorrer por causa do regex, mas mantemos seguro + } + } + return value; + } + + private static boolean isInteger(String value) { + return INTEGER_PATTERN + .matcher(value) + .matches(); + } + +} diff --git a/src/main/java/com/mageddo/dnsproxyserver/config/provider/dataformatv3/converter/EnvConverter.java b/src/main/java/com/mageddo/dnsproxyserver/config/provider/dataformatv3/converter/EnvConverter.java index d1817e9c3..4be948d98 100644 --- a/src/main/java/com/mageddo/dnsproxyserver/config/provider/dataformatv3/converter/EnvConverter.java +++ b/src/main/java/com/mageddo/dnsproxyserver/config/provider/dataformatv3/converter/EnvConverter.java @@ -1,16 +1,35 @@ package com.mageddo.dnsproxyserver.config.provider.dataformatv3.converter; +import com.mageddo.dataformat.env.EnvMapper; import com.mageddo.dnsproxyserver.config.provider.dataformatv3.ConfigV3; +import lombok.RequiredArgsConstructor; +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Map; + +@Singleton +@RequiredArgsConstructor(onConstructor_ = @Inject) public class EnvConverter implements Converter { + + private static final String PREFIX = "DPS_"; + + private final EnvMapper envMapper; + private final JsonConverter jsonConverter; + @Override public ConfigV3 parse() { - return null; + return this.parse(System.getenv()); + } + + ConfigV3 parse(Map env) { + final var json = this.envMapper.toJson(env, PREFIX); + return this.jsonConverter.parse(json); } @Override public String serialize(ConfigV3 config) { - return ""; + throw new UnsupportedOperationException(); } @Override diff --git a/src/main/java/com/mageddo/dnsproxyserver/utils/Numbers.java b/src/main/java/com/mageddo/dnsproxyserver/utils/Numbers.java index a8d874646..3ebfe04ed 100644 --- a/src/main/java/com/mageddo/dnsproxyserver/utils/Numbers.java +++ b/src/main/java/com/mageddo/dnsproxyserver/utils/Numbers.java @@ -29,4 +29,8 @@ public static Integer firstPositive(Integer... arr) { } return null; } + + public static boolean canBeInt(long asLong) { + return asLong >= Integer.MIN_VALUE && asLong <= Integer.MAX_VALUE; + } } diff --git a/src/test/java/com/mageddo/dataformat/env/EnvMapperTest.java b/src/test/java/com/mageddo/dataformat/env/EnvMapperTest.java new file mode 100644 index 000000000..cd9676a45 --- /dev/null +++ b/src/test/java/com/mageddo/dataformat/env/EnvMapperTest.java @@ -0,0 +1,27 @@ +package com.mageddo.dataformat.env; + +import com.mageddo.dnsproxyserver.config.provider.dataformatv3.templates.ConfigV3EnvTemplates; +import com.mageddo.dnsproxyserver.config.provider.dataformatv3.templates.ConfigV3Templates; +import com.mageddo.json.JsonUtils; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class EnvMapperTest { + + private final EnvMapper mapper = new EnvMapper(); + + @Test + void mustConvertEnvVariablesToJsonStructure() { + // Arrange + final var env = ConfigV3EnvTemplates.build(); + final var expected = JsonUtils.readTree(ConfigV3Templates.buildJson()); + + // Act + final var json = this.mapper.toJson(env, "DPS_"); + final var actual = JsonUtils.readTree(json); + + // Assert + assertEquals(expected, actual); + } +} diff --git a/src/test/java/com/mageddo/dnsproxyserver/config/provider/dataformatv3/converter/EnvConverterTest.java b/src/test/java/com/mageddo/dnsproxyserver/config/provider/dataformatv3/converter/EnvConverterTest.java new file mode 100644 index 000000000..29edb19bf --- /dev/null +++ b/src/test/java/com/mageddo/dnsproxyserver/config/provider/dataformatv3/converter/EnvConverterTest.java @@ -0,0 +1,26 @@ +package com.mageddo.dnsproxyserver.config.provider.dataformatv3.converter; + +import com.mageddo.dataformat.env.EnvMapper; +import com.mageddo.dnsproxyserver.config.provider.dataformatv3.templates.ConfigV3EnvTemplates; +import com.mageddo.dnsproxyserver.config.provider.dataformatv3.templates.ConfigV3Templates; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class EnvConverterTest { + + private final EnvConverter converter = new EnvConverter(new EnvMapper(), new JsonConverter()); + + @Test + void mustParseEnvironmentIntoConfig() { + // Arrange + final var expected = ConfigV3Templates.build(); + final var env = ConfigV3EnvTemplates.build(); + + // Act + final var actual = this.converter.parse(env); + + // Assert + assertEquals(expected, actual); + } +} diff --git a/src/test/java/com/mageddo/dnsproxyserver/config/provider/dataformatv3/templates/ConfigV3EnvTemplates.java b/src/test/java/com/mageddo/dnsproxyserver/config/provider/dataformatv3/templates/ConfigV3EnvTemplates.java new file mode 100644 index 000000000..6f6fa60c3 --- /dev/null +++ b/src/test/java/com/mageddo/dnsproxyserver/config/provider/dataformatv3/templates/ConfigV3EnvTemplates.java @@ -0,0 +1,44 @@ +package com.mageddo.dnsproxyserver.config.provider.dataformatv3.templates; + +import java.util.LinkedHashMap; +import java.util.Map; + +public final class ConfigV3EnvTemplates { + + private ConfigV3EnvTemplates() { + } + + public static Map build() { + final var env = new LinkedHashMap(); + env.put("DPS_VERSION", "3"); + env.put("DPS_SERVER__DNS__PORT", "53"); + env.put("DPS_SERVER__DNS__NO_ENTRIES_RESPONSE_CODE", "3"); + env.put("DPS_SERVER__WEB__PORT", "5380"); + env.put("DPS_SERVER__PROTOCOL", "UDP_TCP"); + env.put("DPS_SOLVER__REMOTE__ACTIVE", "true"); + env.put("DPS_SOLVER__REMOTE__DNS_SERVERS_0", "8.8.8.8"); + env.put("DPS_SOLVER__REMOTE__DNS_SERVERS_1", "4.4.4.4:53"); + env.put("DPS_SOLVER__REMOTE__CIRCUIT_BREAKER__NAME", "STATIC_THRESHOLD"); + env.put("DPS_SOLVER__DOCKER__REGISTER_CONTAINER_NAMES", "false"); + env.put("DPS_SOLVER__DOCKER__DOMAIN", "docker"); + env.put("DPS_SOLVER__DOCKER__HOST_MACHINE_FALLBACK", "true"); + env.put("DPS_SOLVER__DOCKER__DPS_NETWORK__NAME", "dps"); + env.put("DPS_SOLVER__DOCKER__DPS_NETWORK__AUTO_CREATE", "false"); + env.put("DPS_SOLVER__DOCKER__DPS_NETWORK__AUTO_CONNECT", "false"); + env.put("DPS_SOLVER__DOCKER__DOCKER_DAEMON_URI", "null"); + env.put("DPS_SOLVER__SYSTEM__HOST_MACHINE_HOSTNAME", "host.docker"); + env.put("DPS_SOLVER__LOCAL__ACTIVE_ENV", ""); + env.put("DPS_SOLVER__LOCAL__ENVS_0__NAME", ""); + env.put("DPS_SOLVER__LOCAL__ENVS_0__HOSTNAMES_0__TYPE", "A"); + env.put("DPS_SOLVER__LOCAL__ENVS_0__HOSTNAMES_0__HOSTNAME", "github.com"); + env.put("DPS_SOLVER__LOCAL__ENVS_0__HOSTNAMES_0__IP", "192.168.0.1"); + env.put("DPS_SOLVER__LOCAL__ENVS_0__HOSTNAMES_0__TTL", "255"); + env.put("DPS_SOLVER__STUB__DOMAIN_NAME", "stub"); + env.put("DPS_DEFAULT_DNS__ACTIVE", "true"); + env.put("DPS_DEFAULT_DNS__RESOLV_CONF__PATHS", "/host/etc/systemd/resolved.conf,/host/etc/resolv.conf,/etc/systemd/resolved.conf,/etc/resolv.conf"); + env.put("DPS_DEFAULT_DNS__RESOLV_CONF__OVERRIDE_NAME_SERVERS", "true"); + env.put("DPS_LOG__LEVEL", "DEBUG"); + env.put("DPS_LOG__FILE", "console"); + return env; + } +} diff --git a/src/test/java/com/mageddo/dnsproxyserver/config/provider/dataformatv3/templates/ConfigV3Templates.java b/src/test/java/com/mageddo/dnsproxyserver/config/provider/dataformatv3/templates/ConfigV3Templates.java index 4472f2a76..080537ea8 100644 --- a/src/test/java/com/mageddo/dnsproxyserver/config/provider/dataformatv3/templates/ConfigV3Templates.java +++ b/src/test/java/com/mageddo/dnsproxyserver/config/provider/dataformatv3/templates/ConfigV3Templates.java @@ -59,7 +59,7 @@ public static String buildYaml() { """); } - public ConfigV3 build() { + public static ConfigV3 build() { return new JsonConverter().parse(buildJson()); }