io.jstach.kiwi.kvs.DefaultKeyValuesSourceLoader Maven / Gradle / Ivy
The newest version!
package io.jstach.kiwi.kvs;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import io.jstach.kiwi.kvs.KeyValue.Flag;
import io.jstach.kiwi.kvs.KeyValuesServiceProvider.KeyValuesFilter.FilterContext;
/*
* This class is not reusable or threadsafe unless
* the static of method is used.
*/
class DefaultKeyValuesSourceLoader implements KeyValuesSourceLoader {
private final KeyValuesSystem system;
private final Variables variables;
private final Map variableStore;
private final Map keys = new LinkedHashMap<>();
private final List sourcesStack = new ArrayList<>();
private final List keyValuesStore = new ArrayList<>();
private final KeyValuesResourceParser resourceParser = DefaultKeyValuesResourceParser.of();
private final KeyValuesEnvironment.Logger logger;
/*
* TODO We use a node to wrap a source to represent each branch to fully recover the
* load path but we probably do not need to do this as each kv has the source.
*/
record Node(NamedKeyValuesSource current, @Nullable Node parent) {
}
static KeyValuesLoader of(KeyValuesSystem system, Variables rootVariables,
List extends NamedKeyValuesSource> resources) {
record ReusableLoader(KeyValuesSystem system, Variables rootVariables,
List extends NamedKeyValuesSource> resources) implements KeyValuesLoader {
@Override
public KeyValues load() throws IOException {
try {
return new DefaultKeyValuesSourceLoader(system, rootVariables).load(resources);
}
catch (RuntimeException e) {
system.environment().getLogger().fatal(e);
throw e;
}
catch (IOException e) {
system.environment().getLogger().fatal(e);
throw e;
}
}
}
return new ReusableLoader(system, rootVariables, resources);
}
private DefaultKeyValuesSourceLoader(KeyValuesSystem system, Variables rootVariables) {
super();
this.system = system;
this.variableStore = new LinkedHashMap<>();
this.variables = Variables.builder().add(variableStore).add(rootVariables).build();
this.logger = system.environment().getLogger();
}
@Override
public KeyValues load(List extends NamedKeyValuesSource> sources) throws IOException {
if (sources.isEmpty()) {
return KeyValues.empty();
}
var fs = this.sourcesStack;
KeyValues keyValues = () -> keyValuesStore.stream();
{
List nodes = sources.stream().map(s -> new Node(s, null)).toList();
validateNames(nodes);
fs.addAll(0, nodes);
}
for (; !fs.isEmpty();) {
// pop
var node = fs.remove(0);
var resource = node.current;
Set flags = KeyValuesSource.loadFlags(resource);
var kvs = switch (resource) {
case KeyValuesResource r -> {
InternalKeyValuesResource normalizedResource = normalizeResource(r, node);
yield load(node, normalizedResource, flags);
}
case NamedKeyValues _kvs -> _kvs.keyValues();
};
var kvFlags = LoadFlag.toKeyValueFlags(flags);
if (!kvFlags.isEmpty()) {
kvs = kvs.map(kv -> kv.addFlags(kvFlags));
}
if (!LoadFlag.NO_INTERPOLATE.isSet(flags)) {
// technically this would be a noop
// anyway because the kv have the
// no interpolate flag.
kvs = kvs.expand(variables);
}
List extends InternalKeyValuesResource> foundResources = parseResources(kvs, node);
var nodes = foundResources.stream().map(s -> new Node(s, node)).toList();
validateNames(nodes);
// push
fs.addAll(0, nodes);
kvs = resourceParser.filterResources(kvs);
boolean added = false;
if (!LoadFlag.NO_ADD.isSet(flags)) {
for (var kv : kvs) {
if (LoadFlag.NO_REPLACE.isSet(flags) && keys.containsKey(kv.key())) {
continue;
}
keys.put(kv.key(), kv);
keyValuesStore.add(kv);
added = true;
}
if (!added && LoadFlag.NO_EMPTY.isSet(flags)) {
throw new IOException("Resource did not have any key values and was flagged not empty. resource: "
+ describe(node));
}
}
else {
variableStore.putAll(kvs.interpolate(variables));
}
/*
* We interpolate the entire list again. Every time a resource is loaded so
* that the next resource has the previous resources keys as variables for
* interpolation.
*/
variableStore.putAll(keyValues.interpolate(variables));
}
return keyValues.expand(variables).memoize();
}
private List extends InternalKeyValuesResource> parseResources(KeyValues kvs, Node node) throws IOException {
List extends InternalKeyValuesResource> foundResources;
try {
foundResources = resourceParser.parseResources(kvs);
}
catch (KeyValuesResourceParserException e) {
throw new IOException("Resource has an invalid resource key. resource: " + describe(node), e);
}
return foundResources;
}
private InternalKeyValuesResource normalizeResource(KeyValuesResource r, Node node) throws IOException {
InternalKeyValuesResource normalizedResource;
try {
normalizedResource = resourceParser.normalizeResource(r);
}
catch (KeyValuesResourceParserException e) {
throw new IOException("Resource has invalid resource key in URI. resource: " + describe(node), e);
}
return normalizedResource;
}
static List validateNames(List nodes) {
Set names = new HashSet<>();
for (var n : nodes) {
String name = n.current.name();
if (!names.add(name)) {
throw new IllegalStateException("Duplicate name found in grouped resources. name=" + name);
}
}
return nodes;
}
static String describe(Node node) {
StringBuilder sb = new StringBuilder();
describe(sb, node);
return sb.toString();
}
static void describe(StringBuilder sb, Node node) {
KeyValuesSource.fullDescribe(sb, node.current);
}
KeyValues load(Node node, InternalKeyValuesResource resource, Set flags)
throws IOException, FileNotFoundException {
if (!resource.normalized()) {
throw new IllegalStateException("bug");
}
logger.load(resource);
if (LoadFlag.NO_LOAD_CHILDREN.isSet(flags)) {
throw new IOException("Resource not allowed to chain. resource: " + describe(node));
}
var context = DefaultLoaderContext.of(system, variables, resourceParser);
try {
var kvs = system.loaderFinder()
.findLoader(context, resource)
.orElseThrow(() -> new IOException("Resource Loader not found. resource: " + describe(node)))
.load();
logger.loaded(resource);
return filter(resource, kvs);
}
catch (FileNotFoundException e) {
logger.missing(resource, e);
if (LoadFlag.NO_REQUIRE.isSet(flags)) {
return KeyValues.empty();
}
throw new IOException("Resource not found. resource: " + describe(node), e);
}
catch (IOException e) {
throw new IOException("Resource load fail. resource: " + describe(node), e);
}
}
KeyValues filter(InternalKeyValuesResource resource, KeyValues keyValues) {
var filters = resource.filters();
if (filters.isEmpty()) {
return keyValues;
}
FilterContext context = new FilterContext(system.environment(), resource.parameters());
for (var f : filters) {
keyValues = system.filter().filter(context, keyValues, f);
}
return keyValues;
}
}
enum LoadFlag {
/**
* Makes the resource optional so that if it is not found an error does not happen.
*/
NO_REQUIRE(List.of(KeyValuesResource.FLAG_NO_REQUIRE, KeyValuesResource.FLAG_OPTIONAL,
KeyValuesResource.FLAG_NOT_REQUIRED)),
/**
* TODO
*/
NO_EMPTY(KeyValuesResource.FLAG_NO_EMPTY),
/**
* Confusing but this means the resource should not have its properties overriden. Not
* to be confused with {@link #NO_REPLACE} which sounds like what this does.
*/
LOCK(KeyValuesResource.FLAG_LOCK),
/**
* This basically says the resource can only add new key values.
*/
NO_REPLACE(KeyValuesResource.FLAG_NO_REPLACE),
/**
* Will add the kvs to variables but not to the final resolved key values.
*/
NO_ADD(KeyValuesResource.FLAG_NO_ADD),
/**
* Will add key values but are not allowed for interpolation.
*/
NO_ADD_VARIABLES(KeyValuesResource.FLAG_NO_ADD_VARIABLES),
/**
* Disables _load calls on child.
*/
NO_LOAD_CHILDREN(KeyValuesResource.FLAG_NO_LOAD_CHILDREN),
/**
* Will not interpolate key values loaded ever.
*/
NO_INTERPOLATE(KeyValuesResource.FLAG_NO_INTERPOLATE),
/**
* Will not toString or print out sensitive
*/
SENSITIVE(KeyValuesResource.FLAG_SENSITIVE),
/**
* TODO
*/
NO_RELOAD(KeyValuesResource.FLAG_NO_RELOAD),
/**
* TODO
*/
INHERIT(KeyValuesResource.FLAG_INHERIT);
@SuppressWarnings("ImmutableEnumChecker")
private final Set names;
@SuppressWarnings("ImmutableEnumChecker")
private final Set reverseNames;
private LoadFlag(List names, List reverseNames) {
if (names.isEmpty()) {
names = List.of(name());
}
FlagNames fn = new FlagNames(names, reverseNames);
this.names = Set.copyOf(fn.names());
this.reverseNames = Set.copyOf(fn.reverseNames());
}
private LoadFlag(List names) {
this(names, List.of());
}
private LoadFlag(String name) {
this(List.of(name));
}
private LoadFlag() {
this(List.of(), List.of());
}
boolean isSet(Set flags) {
return flags.contains(this);
}
void set(EnumSet set, boolean add) {
if (add) {
set.add(this);
}
else {
set.remove(this);
}
}
public static Set toKeyValueFlags(Iterable loadFlags) {
EnumSet flags = EnumSet.noneOf(KeyValue.Flag.class);
for (var lf : loadFlags) {
switch (lf) {
case NO_INTERPOLATE -> flags.add(Flag.NO_INTERPOLATION);
case SENSITIVE -> flags.add(Flag.SENSITIVE);
default -> {
}
}
}
return flags;
}
public static void parse(EnumSet set, String key) {
var flags = LoadFlag.values();
key = key.toUpperCase(Locale.ROOT);
for (var flag : flags) {
if (nameMatches(flag.names, key)) {
flag.set(set, true);
return;
}
else if (nameMatches(flag.reverseNames, key)) {
flag.set(set, false);
return;
}
}
throw new IllegalArgumentException("bad load flag: " + key);
}
public static void parseCSV(EnumSet flags, String csv) {
DefaultKeyValuesMedia.parseCSV(csv, k -> LoadFlag.parse(flags, k));
}
public static EnumSet parseCSV(String csv) {
EnumSet flags = EnumSet.noneOf(LoadFlag.class);
parseCSV(flags, csv);
return flags;
}
private static boolean nameMatches(Set aliases, String name) {
return aliases.contains(name.toUpperCase(Locale.ROOT));
}
static String toCSV(Stream loadFlags) {
return loadFlags.map(f -> f.name()).collect(Collectors.joining(","));
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy