InterpolatedObjectDeserializer.java
package io.github.giulong.spectrum.internals.jackson.deserializers;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import io.github.giulong.spectrum.utils.Vars;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.util.stream.Collectors.toMap;
import static lombok.AccessLevel.PRIVATE;
@Slf4j
@NoArgsConstructor(access = PRIVATE)
public class InterpolatedObjectDeserializer extends JsonDeserializer<Object> {
private static final InterpolatedObjectDeserializer INSTANCE = new InterpolatedObjectDeserializer();
private static final Pattern INT_PATTERN = Pattern.compile("(?<placeholder>\\$<(?<varName>[\\w.]+)(:-(?<defaultValue>[\\w~.:/\\\\]*))?>)");
private static final Pattern NUMBER = Pattern.compile("-?\\d+(.\\d+|,\\d+)?");
private final Vars vars = Vars.getInstance();
private final ObjectMapper objectMapper = YAMLMapper.builder().build();
public static InterpolatedObjectDeserializer getInstance() {
return INSTANCE;
}
@Override
public Object deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException {
final JsonNode jsonNode = jsonParser.readValueAsTree();
final String currentName = jsonParser.currentName();
final JsonNodeType jsonNodeType = jsonNode.getNodeType();
log.trace("Deserializing {} from {} -> {}", jsonNodeType, currentName, jsonNode);
return switch (jsonNodeType) {
case NUMBER -> jsonNode.numberValue();
case BOOLEAN -> jsonNode.booleanValue();
case STRING -> traverse(jsonNode.textValue(), currentName);
case OBJECT -> traverse(objectMapper.convertValue(jsonNode, Map.class), currentName);
case ARRAY -> traverse(objectMapper.convertValue(jsonNode, List.class), currentName);
default -> traverse(jsonNode, currentName);
};
}
Object traverse(final Object value, final String currentName) {
return switch (value) {
case String v -> {
final Matcher matcher = INT_PATTERN.matcher(v);
yield matcher.matches()
? interpolate(v, currentName, matcher)
: InterpolatedStringDeserializer.getInstance().interpolate(v, currentName);
}
case Map<?, ?> m -> m
.entrySet()
.stream()
.collect(toMap(Map.Entry::getKey, e -> traverse(e.getValue(), currentName)));
case List<?> l -> l.stream().map(e -> traverse(e, currentName)).toList();
default -> value;
};
}
int interpolate(final String value, final String currentName, final Matcher matcher) {
final String varName = matcher.group("varName");
final String placeholder = matcher.group("placeholder");
final String defaultValue = matcher.group("defaultValue");
final String envVar = System.getenv(varName);
final String envVarOrPlaceholder = envVar != null ? envVar : placeholder;
final String systemProperty = System.getProperty(varName, envVarOrPlaceholder);
String interpolatedValue = String.valueOf(vars.getOrDefault(varName, systemProperty));
if (value.equals(interpolatedValue)) {
if (defaultValue == null) {
log.debug("No variable found to interpolate '{}' for key '{}'", value, currentName);
} else {
log.trace("No variable found to interpolate '{}' for key '{}'. Using provided default '{}'", value, currentName, defaultValue);
interpolatedValue = value.replace(placeholder, defaultValue);
}
} else {
log.trace("Interpolated value for key '{}: {}' -> '{}'", currentName, value, interpolatedValue);
}
return isNumber(interpolatedValue) ? Integer.parseInt(interpolatedValue) : 0;
}
boolean isNumber(final String value) {
return NUMBER.matcher(value).matches();
}
}