Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,247 @@

import com.mageddo.dnsproxyserver.config.provider.dataformatv3.ConfigV3;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;

public class EnvConverter implements Converter {

static final String PREFIX = "DPS_";

private final Map<String, String> environment;

public EnvConverter() {
this(System.getenv());
}

EnvConverter(Map<String, String> environment) {
this.environment = new HashMap<>(environment);
}

@Override
public ConfigV3 parse() {
return null;
return new Parser(this.environment).parse();
}

@Override
public String serialize(ConfigV3 config) {
return "";
throw new UnsupportedOperationException("Not implemented yet");
}

@Override
public int priority() {
return 0;
}

private static final class Parser {

private static final Map<Class<?>, Map<String, Field>> CACHE = new ConcurrentHashMap<>();
public static final String FIELD_AND_CASE_SEPARATOR = "_";

private final Map<String, String> environment;

private Parser(Map<String, String> environment) {
this.environment = environment;
}

private ConfigV3 parse() {
final var config = new ConfigV3();
this.filterMatchingVariables()
.forEach(entry -> this.apply(config, entry.getKey(), entry.getValue()));
return config;
}

private Stream<Map.Entry<String, String>> filterMatchingVariables() {
return this.environment
.entrySet()
.stream()
.filter(entry -> isTargetVariable(entry.getKey(), entry.getValue()));
}

private static boolean isTargetVariable(String key, String value) {
return key != null && key.startsWith(PREFIX) && value != null;
}

private void apply(ConfigV3 config, String key, String value) {
final var tokens = this.buildTokens(key);
this.setValue(config, ConfigV3.class, tokens, value);
}

private Deque<String> buildTokens(String key) {
final var rawPath = key.substring(PREFIX.length());
return new ArrayDeque<>(List.of(rawPath.split(FIELD_AND_CASE_SEPARATOR)));
}

private void setValue(Object current, Class<?> type, Deque<String> tokens, String value) {
final var match = this.matchField(type, tokens);
consume(tokens, match.segmentsConsumed());

try {
final var field = match.field();
final var fieldType = field.getType();
if (List.class.isAssignableFrom(fieldType)) {
setListValue(current, field, tokens, value);
return;
}

if (tokens.isEmpty()) {
final var convertedValue = convert(value, fieldType);
field.set(current, convertedValue);
return;
}

var nestedValue = field.get(current);
if (nestedValue == null) {
nestedValue = instantiate(fieldType);
field.set(current, nestedValue);
}
setValue(nestedValue, fieldType, tokens, value);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Failed to set value for key: " + match.field()
.getName(), e);
}
}

private void setListValue(Object current, Field field, Deque<String> tokens, String value)
throws ReflectiveOperationException {

if (tokens.isEmpty()) {
throw new IllegalArgumentException("Missing list index for field " + field.getName());
}

final var index = Integer.parseInt(tokens.removeFirst());
final var list = ensureList(current, field);
ensureCapacity(list, index);

final var elementType = resolveListElementType(field);
if (tokens.isEmpty()) {
list.set(index, convert(value, elementType));
return;
}

var item = list.get(index);
if (item == null) {
item = instantiate(elementType);
list.set(index, item);
}
setValue(item, elementType, tokens, value);
}

private static List<Object> ensureList(Object current, Field field) throws IllegalAccessException {
@SuppressWarnings("unchecked") var list = (List<Object>) field.get(current);
if (list == null) {
list = new ArrayList<>();
field.set(current, list);
}
return list;
}

private static void ensureCapacity(List<Object> list, int index) {
while (list.size() <= index) {
list.add(null);
}
}

private static Object convert(String value, Class<?> targetType) {
if (Objects.equals(targetType, String.class)) {
return value;
}
if (Objects.equals(targetType, Integer.class) || targetType == int.class) {
return Integer.valueOf(value);
}
if (Objects.equals(targetType, Boolean.class) || targetType == boolean.class) {
return Boolean.valueOf(value);
}
if (Objects.equals(targetType, Long.class) || targetType == long.class) {
return Long.valueOf(value);
}
throw new IllegalArgumentException("Unsupported conversion to " + targetType);
}

private static Object instantiate(Class<?> type) {
try {
return type.getDeclaredConstructor()
.newInstance();
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Could not instantiate " + type.getName(), e);
}
}

private FieldMatch matchField(Class<?> type, Deque<String> tokens) {
final var fields = CACHE.computeIfAbsent(type, Parser::loadFields);
final var iterator = tokens.iterator();
final var consumed = new ArrayList<String>();
final var builder = new StringBuilder();

while (iterator.hasNext()) {
final var segment = iterator.next();
if (!consumed.isEmpty()) {
builder.append(FIELD_AND_CASE_SEPARATOR);
}
builder.append(segment);
consumed.add(segment);
final var field = fields.get(builder.toString());
if (field != null) {
return new FieldMatch(field, consumed.size());
}
}
throw new IllegalArgumentException("Unknown configuration path: " + String.join(FIELD_AND_CASE_SEPARATOR, tokens));
}

private static Map<String, Field> loadFields(Class<?> type) {
final var fields = new HashMap<String, Field>();
for (Field field : type.getFields()) {
if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
continue;
}
field.setAccessible(true);
fields.put(toEnvKey(field.getName()), field);
}
return fields;
}

private static String toEnvKey(String name) {
final var builder = new StringBuilder();
for (int i = 0; i < name.length(); i++) {
final var ch = name.charAt(i);
if (Character.isUpperCase(ch)) {
builder.append('_');
}
builder.append(Character.toUpperCase(ch));
}
return builder.toString();
}

private static void consume(Deque<String> tokens, int items) {
for (int i = 0; i < items; i++) {
tokens.removeFirst();
}
}

private static Class<?> resolveListElementType(Field field) {
final var type = field.getGenericType();
if (type instanceof ParameterizedType parameterizedType) {
final var actualType = parameterizedType.getActualTypeArguments()[0];
return extractClass(actualType);
}
throw new IllegalArgumentException("Unable to resolve list element type for field " + field.getName());
}

private static Class<?> extractClass(Type type) {
if (type instanceof Class<?> clazz) {
return clazz;
}
if (type instanceof ParameterizedType parameterizedType) {
return extractClass(parameterizedType.getRawType());
}
throw new IllegalArgumentException("Unsupported type: " + type);
}

private record FieldMatch(Field field, int segmentsConsumed) {
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.mageddo.dnsproxyserver.config.provider.dataformatv3.converter;

import com.mageddo.dnsproxyserver.config.provider.dataformatv3.ConfigV3;
import com.mageddo.dnsproxyserver.config.provider.dataformatv3.templates.ConfigV3Templates;
import org.junit.jupiter.api.Test;

import java.util.Map;

import static java.util.Map.entry;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

class EnvConverterTest {

@Test
void shouldParseEnvironmentVariablesIntoConfigV3() {
var expected = new ConfigV3Templates().build();

var converter = new EnvConverter(Map.ofEntries(
entry("DPS_VERSION", "3"),
entry("DPS_SERVER_DNS_PORT", "53"),
entry("DPS_SERVER_DNS_NO_ENTRIES_RESPONSE_CODE", "3"),
entry("DPS_SERVER_WEB_PORT", "5380"),
entry("DPS_SERVER_PROTOCOL", "UDP_TCP"),
entry("DPS_SOLVER_REMOTE_ACTIVE", "true"),
entry("DPS_SOLVER_REMOTE_DNS_SERVERS_0", "8.8.8.8"),
entry("DPS_SOLVER_REMOTE_DNS_SERVERS_1", "4.4.4.4:53"),
entry("DPS_SOLVER_REMOTE_CIRCUIT_BREAKER_NAME", "STATIC_THRESHOLD"),
entry("DPS_SOLVER_DOCKER_REGISTER_CONTAINER_NAMES", "false"),
entry("DPS_SOLVER_DOCKER_DOMAIN", "docker"),
entry("DPS_SOLVER_DOCKER_HOST_MACHINE_FALLBACK", "true"),
entry("DPS_SOLVER_DOCKER_DPS_NETWORK_NAME", "dps"),
entry("DPS_SOLVER_DOCKER_DPS_NETWORK_AUTO_CREATE", "false"),
entry("DPS_SOLVER_DOCKER_DPS_NETWORK_AUTO_CONNECT", "false"),
entry("DPS_SOLVER_SYSTEM_HOST_MACHINE_HOSTNAME", "host.docker"),
entry("DPS_SOLVER_LOCAL_ACTIVE_ENV", ""),
entry("DPS_SOLVER_LOCAL_ENVS_0_NAME", ""),
entry("DPS_SOLVER_LOCAL_ENVS_0_HOSTNAMES_0_TYPE", "A"),
entry("DPS_SOLVER_LOCAL_ENVS_0_HOSTNAMES_0_HOSTNAME", "github.com"),
entry("DPS_SOLVER_LOCAL_ENVS_0_HOSTNAMES_0_IP", "192.168.0.1"),
entry("DPS_SOLVER_LOCAL_ENVS_0_HOSTNAMES_0_TTL", "255"),
entry("DPS_SOLVER_STUB_DOMAIN_NAME", "stub"),
entry("DPS_DEFAULT_DNS_ACTIVE", "true"),
entry("DPS_DEFAULT_DNS_RESOLV_CONF_PATHS", "/host/etc/systemd/resolved.conf,/host/etc/resolv.conf,/etc/systemd/resolved.conf,/etc/resolv.conf"),
entry("DPS_DEFAULT_DNS_RESOLV_CONF_OVERRIDE_NAME_SERVERS", "true"),
entry("DPS_LOG_LEVEL", "DEBUG"),
entry("DPS_LOG_FILE", "console")
));

ConfigV3 actual = converter.parse();

assertEquals(expected, actual);
}

@Test
void shouldFailWhenUnknownVariableIsProvided() {
var converter = new EnvConverter(Map.of("DPS_UNKNOWN", "value"));

assertThrows(IllegalArgumentException.class, converter::parse);
}
}