Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
192 changes: 192 additions & 0 deletions src/main/java/com/mageddo/dataformat/env/EnvMapper.java
Original file line number Diff line number Diff line change
@@ -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<String, String> env, final String varsPrefix) {
final var root = new LinkedHashMap<String, Object>();

this.findMatchingEnvs(env, varsPrefix)
.forEach(e -> insertPropertyAt(root, e, varsPrefix));

return JsonUtils.writeValueAsString(root);
}

private void insertPropertyAt(
Map<String, Object> root,
Map.Entry<String, String> 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<Map.Entry<String, String>> findMatchingEnvs(Map<String, String> 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<String, Object> 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<String, Object>) next;
} else {
final var newMap = new LinkedHashMap<String, Object>();
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<String, Object>) next;
} else {
final var newMap = new LinkedHashMap<String, Object>();
current.put(seg.name(), newMap);
current = newMap;
}
}
}

private static class SegmentMapper {

private List<PathSegment> ofRawKey(final String rawKey) {
final var segments = new ArrayList<PathSegment>();
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<Object> getOrCreateList(final Map<String, Object> current, final String key) {
final var existing = current.get(key);
if (existing instanceof List) {
return (List<Object>) existing;
}
final var list = new ArrayList<>();
current.put(key, list);
return list;
}

private void ensureSize(final List<Object> 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();
}

}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/mageddo/dnsproxyserver/utils/Numbers.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
27 changes: 27 additions & 0 deletions src/test/java/com/mageddo/dataformat/env/EnvMapperTest.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> build() {
final var env = new LinkedHashMap<String, String>();
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public static String buildYaml() {
""");
}

public ConfigV3 build() {
public static ConfigV3 build() {
return new JsonConverter().parse(buildJson());
}

Expand Down