Skip to content

Commit

Permalink
4.x: Fix sitegen config substitutions (#1103)
Browse files Browse the repository at this point in the history
Do a depth-first traversal of the config to support substitutions in .asMap and asList.
  • Loading branch information
romain-grecourt authored Jan 29, 2025
1 parent 2fb9240 commit afe781e
Show file tree
Hide file tree
Showing 11 changed files with 410 additions and 104 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, 2024 Oracle and/or its affiliates.
* Copyright (c) 2022, 2025 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -21,10 +21,15 @@
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -123,7 +128,7 @@ public Optional<Config> asOptional() {
}

/**
* Get the value as string.
* Get the value as a string.
*
* @return optional
*/
Expand All @@ -132,7 +137,7 @@ public Optional<String> asString() {
}

/**
* Get the value as boolean.
* Get the value as a boolean.
*
* @return optional
*/
Expand Down Expand Up @@ -166,7 +171,7 @@ public <T> Optional<T> as(Function<Object, T> mapper) {
* @return optional
*/
public <T> Optional<T> as(Class<T> type) {
return as(o -> cast(o, type));
return as(o -> convert(o, type));
}

/**
Expand All @@ -175,87 +180,99 @@ public <T> Optional<T> as(Class<T> type) {
* @return optional
*/
public Optional<List<Config>> asNodeList() {
return asList(e -> new Config(e, this));
if (value == null) {
return Optional.empty();
}
if (value instanceof List) {
return Optional.of(((List<?>) value).stream()
.map(e -> new Config(e, this))
.collect(Collectors.toList()));
}
throw new MappingException(value.getClass(), List.class);
}

/**
* Map the value to a list of object.
* Get the value as a list.
*
* @return optional
*/
public Optional<List<Object>> asList() {
public Optional<List<String>> asList() {
return asList(Function.identity());
}

/**
* Map the value to a list of a given type.
* Get the value as a list.
*
* @param type requested type
* @param <T> requested type
* @param type value type
* @param <T> value type
* @return optional
*/
public <T> Optional<List<T>> asList(Class<T> type) {
return asList(e -> cast(e, type));
return asList(e -> convert(e, type));
}

/**
* Map the value to a list using a mapping function.
* Get the value as a list.
*
* @param mapper mapping function
* @param <T> requested type
* @param mapper value mapper
* @param <T> value type
* @return optional
*/
public <T> Optional<List<T>> asList(Function<Object, T> mapper) {
public <T> Optional<List<T>> asList(Function<String, T> mapper) {
if (value == null) {
return Optional.empty();
}
if (value instanceof List) {
return Optional.of(((List<?>) value).stream()
.map(mapper)
.collect(Collectors.toList()));
List<T> values = new ArrayList<>();
traverse((prefix, entry) -> {
T value = mapper.apply(entry.getValue());
values.add(value);
});
return Optional.of(values);
}
throw new MappingException(value.getClass(), List.class);
}

/**
* Map the value to a map using a mapping function.
* Get the value as a map.
*
* @param mapper mapping function
* @param <T> requested type
* @return optional
*/
public <T> Optional<Map<String, T>> asMap(Function<Object, T> mapper) {
if (value == null) {
return Optional.empty();
}
if (value instanceof Map) {
return Optional.of(((Map<?, ?>) value)
.entrySet()
.stream()
.map(e -> Map.entry(e.getKey().toString(), mapper.apply(e.getValue())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
}
throw new MappingException(value.getClass(), Map.class);
public Optional<Map<String, String>> asMap() {
return asMap(Function.identity());
}

/**
* Map the value to a map of object.
* Get the value as a map.
*
* @param type value type
* @param <T> value type
* @return optional
*/
public Optional<Map<String, Object>> asMap() {
return asMap(Function.identity());
public <T> Optional<Map<String, T>> asMap(Class<T> type) {
return asMap(e -> convert(e, type));
}

/**
* Map the value to a map of a given type.
* Get the value as a map.
*
* @param type requested type
* @param <T> requested type
* @param mapper value mapper
* @param <T> value type
* @return optional
*/
public <T> Optional<Map<String, T>> asMap(Class<T> type) {
return asMap(e -> cast(e, type));
public <T> Optional<Map<String, T>> asMap(Function<String, T> mapper) {
if (value == null) {
return Optional.empty();
}
if (value instanceof Map) {
Map<String, T> values = new TreeMap<>();
traverse((prefix, entry) -> {
String key = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey();
values.put(key, mapper.apply(entry.getValue()));
});
return Optional.of(values);
}
throw new MappingException(value.getClass(), Map.class);
}

/**
Expand Down Expand Up @@ -308,31 +325,98 @@ public static Config create(Reader reader, Map<String, String> properties) {
return create((Object) yaml.loadAs(reader, Object.class), properties);
}

private static final Set<Class<?>> PRIMITIVE_BOXED =
Set.of(
Boolean.class,
Character.class,
Byte.class,
Short.class,
Integer.class,
Long.class,
Float.class,
Double.class
);


private <T> T cast(Object obj, Class<T> type) {
if (obj != null) {
if (type.equals(String.class)
|| obj.getClass().isPrimitive()
|| PRIMITIVE_BOXED.contains(obj.getClass())) {
return type.cast(substitutions.resolve(String.valueOf(obj)));
private void traverse(BiConsumer<String, Map.Entry<String, String>> visitLeaf) {
Deque<String> path = new ArrayDeque<>();
traverse((k, v) -> {
if (k != null) {
path.addLast(substitutions.resolve(k));
}
if (type.isInstance(obj)) {
return type.cast(obj);
}, (k, v) -> {
if (!path.isEmpty()) {
path.removeLast();
}
}, (k, v) -> {
String prefix = String.join(".", path);
visitLeaf.accept(prefix, Map.entry(substitutions.resolve(k), substitutions.resolve(v)));
});
}

private void traverse(BiConsumer<String, Object> visitNode,
BiConsumer<String, Object> postVisitNode,
BiConsumer<String, String> visitLeaf) {

Deque<Object> parents = new ArrayDeque<>();
Deque<String> keys = new ArrayDeque<>();
Deque<Object> stack = new ArrayDeque<>();
stack.push(value);
while (!stack.isEmpty()) {
Object parent = parents.peek();
String key = keys.peek();
Object node = stack.peek();
if (parent == node) {
postVisitNode.accept(key, node);
stack.pop();
if (!stack.isEmpty()) {
parents.pop();
keys.pop();
}
} else if (node instanceof Map) {
List<? extends Map.Entry<?, ?>> entries = List.copyOf(((Map<?, ?>) node).entrySet());
ListIterator<? extends Map.Entry<?, ?>> it = entries.listIterator(entries.size());
while (it.hasPrevious()) {
Map.Entry<?, ?> previous = it.previous();
stack.push(previous.getValue());
keys.push(previous.getKey().toString());
}
parents.push(node);
visitNode.accept(key, node);
} else if (node instanceof List) {
List<?> list = (List<?>) node;
ListIterator<?> it = list.listIterator(list.size());
while (it.hasPrevious()) {
keys.push(String.valueOf(it.previousIndex()));
stack.push(it.previous());
}
parents.push(node);
visitNode.accept(key, node);
} else {
visitLeaf.accept(key, String.valueOf(node));
stack.pop();
keys.pop();
}
throw new MappingException(obj.getClass(), type);
}
return null;
}

@SuppressWarnings("unchecked")
private <T> T convert(Object obj, Class<T> type) {
String value = substitutions.resolve(String.valueOf(obj));
if (type.equals(String.class)) {
return (T) value;
}
if (type.equals(Boolean.class) || type.equals(boolean.class)) {
return (T) Boolean.valueOf(value);
}
if (type.equals(Character.class) || type.equals(char.class)) {
return (T) Character.valueOf(value.charAt(0));
}
if (type.equals(Byte.class) || type.equals(byte.class)) {
return (T) Byte.valueOf(value);
}
if (type.equals(Short.class) || type.equals(short.class)) {
return (T) Short.valueOf(value);
}
if (type.equals(Integer.class) || type.equals(int.class)) {
return (T) Integer.valueOf(value);
}
if (type.equals(Long.class) || type.equals(long.class)) {
return (T) Long.valueOf(value);
}
if (type.equals(Float.class) || type.equals(float.class)) {
return (T) Float.valueOf(value);
}
if (type.equals(Double.class) || type.equals(double.class)) {
return (T) Double.valueOf(value);
}
throw new MappingException(obj.getClass(), type);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2022 Oracle and/or its affiliates.
* Copyright (c) 2018, 2025 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -382,15 +382,11 @@ public Builder releases(String... releases) {
* @return this builder
*/
public Builder config(Config config) {
theme.putAll(config.get("theme")
.asMap(String.class)
.orElseGet(Map::of));
theme.putAll(config.get("theme").asMap().orElseGet(Map::of));
home = config.get("homePage")
.asString()
.orElse(null);
releases.addAll(config.get("releases")
.asList(String.class)
.orElseGet(List::of));
releases.addAll(config.get("releases").asList().orElseGet(List::of));
nav = config.get("navigation")
.asOptional()
.map(Nav::create)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2024 Oracle and/or its affiliates.
* Copyright (c) 2018, 2025 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -296,17 +296,9 @@ public Builder imagesDir(String imagesDir) {
* @return this builder
*/
public Builder config(Config config) {
libraries.addAll(config.get("libraries")
.asList(String.class)
.orElseGet(List::of));

attributes.putAll(config.get("attributes")
.asMap()
.orElseGet(Map::of));

imagesDir = config.get("images-dir")
.asString()
.orElse(null);
libraries.addAll(config.get("libraries").asList().orElseGet(List::of));
attributes.putAll(config.get("attributes").asMap().orElseGet(Map::of));
imagesDir = config.get("images-dir").asString().orElse(null);
return this;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2022 Oracle and/or its affiliates.
* Copyright (c) 2018, 2025 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -215,12 +215,8 @@ public Builder model(Map<String, String> model) {
* @return this builder
*/
public Builder config(Config config) {
directives.putAll(config.get("directives")
.asMap(String.class)
.orElseGet(Map::of));
model.putAll(config.get("model")
.asMap(String.class)
.orElseGet(Map::of));
directives.putAll(config.get("directives").asMap().orElseGet(Map::of));
model.putAll(config.get("model").asMap().orElseGet(Map::of));
return this;
}

Expand Down
Loading

0 comments on commit afe781e

Please sign in to comment.