downgrade java version to java 8 (velocity still use java 11)

master
InkerBot 3 months ago
parent 093ded264d
commit f2b7c98d36

@ -1,4 +1,4 @@
allprojects { allprojects {
group = "de.exlll" group = "org.inksnow.husk"
version = "4.5.0" version = "4.5.1"
} }

@ -6,8 +6,8 @@ plugins {
} }
java { java {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_1_8
withJavadocJar() withJavadocJar()
withSourcesJar() withSourcesJar()
@ -37,11 +37,19 @@ dependencies {
publishing { publishing {
repositories { repositories {
maven { maven {
name = "GitHubPackages" name = "husk-release"
url = uri("https://maven.pkg.github.com/Exlll/ConfigLib") url = findProperty("repository.huskrelease.url")
?.let(::uri)
?: throw GradleException("repository.huskrelease.url is not set")
credentials { credentials {
username = System.getenv("GITHUB_ACTOR") username = findProperty("repository.huskrelease.username")
password = System.getenv("GITHUB_TOKEN") ?.toString()
?: throw GradleException("repository.huskrelease.username is not set")
password = findProperty("repository.huskrelease.password")
?.toString()
?: throw GradleException("repository.huskrelease.password is not set")
} }
} }
} }

@ -24,9 +24,22 @@ import java.util.List;
* int fn2; * int fn2;
* } * }
* </pre> * </pre>
*
* @param comments
* @param elementNames
*/ */
record CommentNode(List<String> comments, List<String> elementNames) {} public class CommentNode {
private final List<String> comments;
private final List<String> elementNames;
public CommentNode(List<String> comments, List<String> elementNames) {
this.comments = comments;
this.elementNames = elementNames;
}
public List<String> elementNames() {
return elementNames;
}
public List<String> comments() {
return comments;
}
}

@ -2,9 +2,11 @@ package de.exlll.configlib;
import de.exlll.configlib.ConfigurationElements.FieldElement; import de.exlll.configlib.ConfigurationElements.FieldElement;
import de.exlll.configlib.ConfigurationElements.RecordComponentElement; import de.exlll.configlib.ConfigurationElements.RecordComponentElement;
import de.exlll.configlib.util.ref.R$Class;
import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedElement;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import static de.exlll.configlib.Validator.requireConfigurationType; import static de.exlll.configlib.Validator.requireConfigurationType;
import static de.exlll.configlib.Validator.requireNonNull; import static de.exlll.configlib.Validator.requireNonNull;
@ -20,10 +22,23 @@ final class CommentNodeExtractor {
this.outputNull = properties.outputNulls(); this.outputNull = properties.outputNulls();
} }
private record State( private class State {
Iterator<? extends ConfigurationElement<?>> iterator, private final Iterator<? extends ConfigurationElement<?>> iterator;
Object elementHolder private final Object elementHolder;
) {}
private State(Iterator<? extends ConfigurationElement<?>> iterator, Object elementHolder) {
this.iterator = iterator;
this.elementHolder = elementHolder;
}
public Iterator<? extends ConfigurationElement<?>> iterator() {
return iterator;
}
public Object elementHolder() {
return elementHolder;
}
}
/** /**
* Extracts {@code CommentNode}s of the given configuration type in a DFS manner. * Extracts {@code CommentNode}s of the given configuration type in a DFS manner.
@ -37,8 +52,8 @@ final class CommentNodeExtractor {
public Queue<CommentNode> extractCommentNodes(final Object elementHolder) { public Queue<CommentNode> extractCommentNodes(final Object elementHolder) {
requireConfigurationType(elementHolder.getClass()); requireConfigurationType(elementHolder.getClass());
final Queue<CommentNode> result = new ArrayDeque<>(); final Queue<CommentNode> result = new ArrayDeque<>();
final var elementNameStack = new ArrayDeque<>(List.of("")); final ArrayDeque<String> elementNameStack = new ArrayDeque<>(Collections.singletonList(""));
final var stateStack = new ArrayDeque<>(List.of(stateFromObject(elementHolder))); final ArrayDeque<State> stateStack = new ArrayDeque<>(Collections.singletonList(stateFromObject(elementHolder)));
State state; State state;
while (!stateStack.isEmpty()) { while (!stateStack.isEmpty()) {
@ -46,21 +61,21 @@ final class CommentNodeExtractor {
elementNameStack.removeLast(); elementNameStack.removeLast();
while (state.iterator.hasNext()) { while (state.iterator.hasNext()) {
final var element = state.iterator.next(); final ConfigurationElement<?> element = state.iterator.next();
final var elementValue = element.value(state.elementHolder); final Object elementValue = element.value(state.elementHolder);
if ((elementValue == null) && !outputNull) if ((elementValue == null) && !outputNull)
continue; continue;
final var elementName = element.name(); final String elementName = element.name();
final var commentNode = createNodeIfCommentPresent( final Optional<CommentNode> commentNode = createNodeIfCommentPresent(
element.element(), element.element(),
elementName, elementName,
elementNameStack elementNameStack
); );
commentNode.ifPresent(result::add); commentNode.ifPresent(result::add);
final var elementType = element.type(); final Class<?> elementType = element.type();
if ((elementValue != null) && Reflect.isConfigurationType(elementType)) { if ((elementValue != null) && Reflect.isConfigurationType(elementType)) {
stateStack.addLast(state); stateStack.addLast(state);
elementNameStack.addLast(nameFormatter.format(elementName)); elementNameStack.addLast(nameFormatter.format(elementName));
@ -73,8 +88,8 @@ final class CommentNodeExtractor {
} }
private State stateFromObject(final Object elementHolder) { private State stateFromObject(final Object elementHolder) {
final var type = elementHolder.getClass(); final Class<?> type = elementHolder.getClass();
final var iter = type.isRecord() final Iterator<? extends ConfigurationElement<? extends AnnotatedElement>> iter = R$Class.of(type).isRecord()
? recordComponentElements(elementHolder) ? recordComponentElements(elementHolder)
: fieldElements(elementHolder); : fieldElements(elementHolder);
return new State(iter, elementHolder); return new State(iter, elementHolder);
@ -86,13 +101,13 @@ final class CommentNodeExtractor {
final Deque<String> elementNameStack final Deque<String> elementNameStack
) { ) {
if (element.isAnnotationPresent(Comment.class)) { if (element.isAnnotationPresent(Comment.class)) {
final var comments = Arrays.stream(element.getAnnotation(Comment.class).value()) final List<String> comments = Arrays.stream(element.getAnnotation(Comment.class).value())
.flatMap(s -> Arrays.stream(s.split("\n", -1))) .flatMap(s -> Arrays.stream(s.split("\n", -1)))
.toList(); .collect(Collectors.toList());
final var formattedName = nameFormatter.format(elementName); final String formattedName = nameFormatter.format(elementName);
final var elementNames = new ArrayList<>(elementNameStack); final ArrayList<String> elementNames = new ArrayList<>(elementNameStack);
elementNames.add(formattedName); elementNames.add(formattedName);
final var result = new CommentNode(comments, elementNames); final CommentNode result = new CommentNode(comments, elementNames);
return Optional.of(result); return Optional.of(result);
} }
return Optional.empty(); return Optional.empty();
@ -106,7 +121,7 @@ final class CommentNodeExtractor {
} }
private Iterator<RecordComponentElement> recordComponentElements(Object record) { private Iterator<RecordComponentElement> recordComponentElements(Object record) {
return Arrays.stream(record.getClass().getRecordComponents()) return Arrays.stream(R$Class.of(record.getClass()).getRecordComponents())
.map(RecordComponentElement::new) .map(RecordComponentElement::new)
.iterator(); .iterator();
} }

@ -4,18 +4,14 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.AnnotatedType; import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.RecordComponent;
/** /**
* Represents an element of a serializable configuration type. The element can either be a * Represents an element of a serializable configuration type. The element can either be a
* {@link Field} for configuration classes or a {@link RecordComponent} for records. * {@link Field} for configuration classes or a {@link java.lang.reflect.RecordComponent} for records.
* *
* @param <T> the type of the element * @param <T> the type of the element
*/ */
public sealed interface ConfigurationElement<T extends AnnotatedElement> public interface ConfigurationElement<T extends AnnotatedElement> {
permits
ConfigurationElements.FieldElement,
ConfigurationElements.RecordComponentElement {
/** /**
* Returns the element itself. * Returns the element itself.
* *

@ -1,13 +1,25 @@
package de.exlll.configlib; package de.exlll.configlib;
import de.exlll.configlib.util.ref.R$RecordComponent;
import java.lang.reflect.AnnotatedType; import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.RecordComponent;
final class ConfigurationElements { final class ConfigurationElements {
private ConfigurationElements() {} private ConfigurationElements() {}
record FieldElement(Field element) implements ConfigurationElement<Field> { static class FieldElement implements ConfigurationElement<Field> {
private final Field element;
public FieldElement(Field element) {
this.element = element;
}
@Override
public Field element() {
return element;
}
@Override @Override
public String name() { public String name() {
return element.getName(); return element.getName();
@ -34,8 +46,18 @@ final class ConfigurationElements {
} }
} }
record RecordComponentElement(RecordComponent element) static class RecordComponentElement implements ConfigurationElement<R$RecordComponent> {
implements ConfigurationElement<RecordComponent> { private final R$RecordComponent element;
RecordComponentElement(R$RecordComponent element) {
this.element = element;
}
@Override
public R$RecordComponent element() {
return element;
}
@Override @Override
public String name() { public String name() {
return element.getName(); return element.getName();

@ -4,6 +4,7 @@ import de.exlll.configlib.ConfigurationElements.FieldElement;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
final class ConfigurationSerializer<T> extends TypeSerializer<T, FieldElement> { final class ConfigurationSerializer<T> extends TypeSerializer<T, FieldElement> {
ConfigurationSerializer(Class<T> configurationType, ConfigurationProperties properties) { ConfigurationSerializer(Class<T> configurationType, ConfigurationProperties properties) {
@ -12,8 +13,8 @@ final class ConfigurationSerializer<T> extends TypeSerializer<T, FieldElement> {
@Override @Override
public T deserialize(Map<?, ?> serializedConfiguration) { public T deserialize(Map<?, ?> serializedConfiguration) {
final var deserializedElements = deserializeConfigurationElements(serializedConfiguration); final Object[] deserializedElements = deserializeConfigurationElements(serializedConfiguration);
final var elements = elements(); final List<FieldElement> elements = elements();
final T result = newDefaultInstance(); final T result = newDefaultInstance();
for (int i = 0; i < deserializedElements.length; i++) { for (int i = 0; i < deserializedElements.length; i++) {
final FieldElement fieldElement = elements.get(i); final FieldElement fieldElement = elements.get(i);
@ -33,8 +34,7 @@ final class ConfigurationSerializer<T> extends TypeSerializer<T, FieldElement> {
@Override @Override
protected String baseDeserializeExceptionMessage(FieldElement element, Object value) { protected String baseDeserializeExceptionMessage(FieldElement element, Object value) {
return "Deserialization of value '%s' with type '%s' for field '%s' failed." return "Deserialization of value '" + value + "' with type '" + value.getClass() + "' for field '" + element.element() + "' failed.";
.formatted(value, value.getClass(), element.element());
} }
@Override @Override
@ -42,7 +42,7 @@ final class ConfigurationSerializer<T> extends TypeSerializer<T, FieldElement> {
return FieldExtractors.CONFIGURATION.extract(type) return FieldExtractors.CONFIGURATION.extract(type)
.filter(properties.getFieldFilter()) .filter(properties.getFieldFilter())
.map(FieldElement::new) .map(FieldElement::new)
.toList(); .collect(Collectors.toList());
} }
@Override @Override

@ -3,6 +3,7 @@ package de.exlll.configlib;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.*; import java.util.*;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
enum FieldExtractors implements FieldExtractor { enum FieldExtractors implements FieldExtractor {
@ -22,7 +23,7 @@ enum FieldExtractors implements FieldExtractor {
List<Field> fields = classes.stream() List<Field> fields = classes.stream()
.flatMap(c -> Arrays.stream(c.getDeclaredFields())) .flatMap(c -> Arrays.stream(c.getDeclaredFields()))
.filter(FieldFilters.DEFAULT) .filter(FieldFilters.DEFAULT)
.toList(); .collect(Collectors.toList());
requireNoShadowing(fields); requireNoShadowing(fields);
return fields.stream(); return fields.stream();
} }
@ -32,8 +33,8 @@ enum FieldExtractors implements FieldExtractor {
Map<String, Class<?>> map = new LinkedHashMap<>(); Map<String, Class<?>> map = new LinkedHashMap<>();
for (Field field : fields) { for (Field field : fields) {
var fieldName = field.getName(); String fieldName = field.getName();
var fieldClass = field.getDeclaringClass(); Class<?> fieldClass = field.getDeclaringClass();
if (map.containsKey(fieldName)) { if (map.containsKey(fieldName)) {
Class<?> superClass = map.get(fieldName); Class<?> superClass = map.get(fieldName);

@ -1,5 +1,7 @@
package de.exlll.configlib; package de.exlll.configlib;
import de.exlll.configlib.util.StringUtil;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
@ -21,22 +23,20 @@ final class PolymorphicSerializer implements Serializer<Object, Map<?, ?>> {
} }
private void requireNonBlankProperty() { private void requireNonBlankProperty() {
if (polymorphic.property().isBlank()) { if (StringUtil.isBlank(polymorphic.property())) {
String msg = "The @Polymorphic annotation does not allow a blank property name but " + throw new ConfigurationException("The @Polymorphic annotation does not allow a blank property name but type '" + polymorphicType.getName() + "' uses one.");
"type '%s' uses one.".formatted(polymorphicType.getName());
throw new ConfigurationException(msg);
} }
} }
private void initAliases() { private void initAliases() {
final var polymorphicTypes = polymorphicType.getAnnotation(PolymorphicTypes.class); final PolymorphicTypes polymorphicTypes = polymorphicType.getAnnotation(PolymorphicTypes.class);
if (polymorphicTypes == null) if (polymorphicTypes == null)
return; return;
for (PolymorphicTypes.Type pType : polymorphicTypes.value()) { for (PolymorphicTypes.Type pType : polymorphicTypes.value()) {
final var type = pType.type(); final Class<?> type = pType.type();
final var alias = pType.alias().isBlank() ? type.getName() : pType.alias(); final String alias = StringUtil.isBlank(pType.alias()) ? type.getName() : pType.alias();
requireDistinctAliases(alias); requireDistinctAliases(alias);
requireDistinctTypes(type); requireDistinctTypes(type);
@ -48,37 +48,32 @@ final class PolymorphicSerializer implements Serializer<Object, Map<?, ?>> {
private void requireDistinctAliases(String alias) { private void requireDistinctAliases(String alias) {
if (typeByAlias.containsKey(alias)) { if (typeByAlias.containsKey(alias)) {
String msg = "The @PolymorphicTypes annotation must not use the same alias for " + throw new ConfigurationException("The @PolymorphicTypes annotation must not use the same alias for multiple types. Alias '" + alias + "' appears more than once.");
"multiple types. Alias '%s' appears more than once."
.formatted(alias);
throw new ConfigurationException(msg);
} }
} }
private void requireDistinctTypes(Class<?> type) { private void requireDistinctTypes(Class<?> type) {
if (aliasByType.containsKey(type)) { if (aliasByType.containsKey(type)) {
String msg = "The @PolymorphicTypes annotation must not contain multiple " + throw new ConfigurationException("The @PolymorphicTypes annotation must not contain multiple definitions for the same subtype. Type '" + type.getName() + "' appears more than once.");
"definitions for the same subtype. Type '%s' appears more than once."
.formatted(type.getName());
throw new ConfigurationException(msg);
} }
} }
@Override @Override
@SuppressWarnings("rawtypes")
public Map<?, ?> serialize(Object element) { public Map<?, ?> serialize(Object element) {
final Class<?> elementType = element.getClass(); final Class<?> elementType = element.getClass();
// this cast won't cause any exceptions as we only pass objects of types the // this cast won't cause any exceptions as we only pass objects of types the
// serializer expects // serializer expects
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
final var serializer = (TypeSerializer<Object, ?>) TypeSerializer.newSerializerFor( final TypeSerializer<Object, ?> serializer = (TypeSerializer<Object, ?>) TypeSerializer.newSerializerFor(
elementType, elementType,
context.properties() context.properties()
); );
final var serialization = serializer.serialize(element); final Map serialization = serializer.serialize(element);
requireSerializationNotContainsProperty(serialization); requireSerializationNotContainsProperty(serialization);
final var result = new LinkedHashMap<>(); final LinkedHashMap result = new LinkedHashMap<>();
result.put(polymorphic.property(), getTypeIdentifierByType(elementType)); result.put(polymorphic.property(), getTypeIdentifierByType(elementType));
result.putAll(serialization); result.putAll(serialization);
return result; return result;
@ -91,11 +86,9 @@ final class PolymorphicSerializer implements Serializer<Object, Map<?, ?>> {
private void requireSerializationNotContainsProperty(Map<?, ?> serialization) { private void requireSerializationNotContainsProperty(Map<?, ?> serialization) {
if (serialization.containsKey(polymorphic.property())) { if (serialization.containsKey(polymorphic.property())) {
String msg = ("Polymorphic serialization for type '%s' failed. The type contains a " + throw new ConfigurationException("Polymorphic serialization for type '" + polymorphicType.getName() +
"configuration element with name '%s' but that name is " + "' failed. The type contains a configuration element with name '" + polymorphic.property() +
"used by the @Polymorphic property.") "' but that name is used by the @Polymorphic property.");
.formatted(polymorphicType.getName(), polymorphic.property());
throw new ConfigurationException(msg);
} }
} }
@ -103,11 +96,11 @@ final class PolymorphicSerializer implements Serializer<Object, Map<?, ?>> {
public Object deserialize(Map<?, ?> element) { public Object deserialize(Map<?, ?> element) {
requirePropertyPresent(element); requirePropertyPresent(element);
final var typeIdentifier = element.get(polymorphic.property()); final Object typeIdentifier = element.get(polymorphic.property());
requireTypeIdentifierString(typeIdentifier); requireTypeIdentifierString(typeIdentifier);
final var type = getTypeByTypeIdentifier((String) typeIdentifier); final Class<?> type = getTypeByTypeIdentifier((String) typeIdentifier);
final var serializer = TypeSerializer.newSerializerFor(type, context.properties()); final TypeSerializer<?, ?> serializer = TypeSerializer.newSerializerFor(type, context.properties());
return serializer.deserialize(element); return serializer.deserialize(element);
} }
@ -120,41 +113,23 @@ final class PolymorphicSerializer implements Serializer<Object, Map<?, ?>> {
try { try {
return Reflect.getClassByName(className); return Reflect.getClassByName(className);
} catch (RuntimeException e) { } catch (RuntimeException e) {
String msg = ("Polymorphic deserialization for type '%s' failed. " + throw new ConfigurationException("Polymorphic deserialization for type '" + polymorphicType.getName() + "' failed. The class '" + className + "' does not exist.", e);
"The class '%s' does not exist.")
.formatted(polymorphicType.getName(), className);
throw new ConfigurationException(msg, e);
} }
} }
private void requirePropertyPresent(Map<?, ?> element) { private void requirePropertyPresent(Map<?, ?> element) {
if (element.get(polymorphic.property()) != null) if (element.get(polymorphic.property()) != null)
return; return;
String msg = """ throw new ConfigurationException("Polymorphic deserialization for type '" + polymorphicType.getName() +
Polymorphic deserialization for type '%s' failed. \ "' failed. The property '" + polymorphic.property() + "' which holds the type is missing. " +
The property '%s' which holds the type is missing. \ "Value to be deserialized:\n" + element);
Value to be deserialized:
%s\
"""
.formatted(
polymorphicType.getName(),
polymorphic.property(),
element
);
throw new ConfigurationException(msg);
} }
private void requireTypeIdentifierString(Object typeIdentifier) { private void requireTypeIdentifierString(Object typeIdentifier) {
if (typeIdentifier instanceof String) if (typeIdentifier instanceof String)
return; return;
String msg = ("Polymorphic deserialization for type '%s' failed. The type identifier '%s' " + throw new ConfigurationException("Polymorphic deserialization for type '" + polymorphicType.getName() + "' failed. The type identifier '" + typeIdentifier + "' " +
"which should hold the type is not a string but of type '%s'.") "which should hold the type is not a string but of type '" + typeIdentifier.getClass().getName() + "'.");
.formatted(
polymorphicType.getName(),
typeIdentifier,
typeIdentifier.getClass().getName()
);
throw new ConfigurationException(msg);
} }
Class<?> getPolymorphicType() { Class<?> getPolymorphicType() {

@ -1,10 +1,12 @@
package de.exlll.configlib; package de.exlll.configlib;
import de.exlll.configlib.ConfigurationElements.RecordComponentElement; import de.exlll.configlib.ConfigurationElements.RecordComponentElement;
import de.exlll.configlib.util.ref.R$Class;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
final class RecordSerializer<R> extends TypeSerializer<R, RecordComponentElement> { final class RecordSerializer<R> extends TypeSerializer<R, RecordComponentElement> {
RecordSerializer(Class<R> recordType, ConfigurationProperties properties) { RecordSerializer(Class<R> recordType, ConfigurationProperties properties) {
@ -13,31 +15,28 @@ final class RecordSerializer<R> extends TypeSerializer<R, RecordComponentElement
@Override @Override
public R deserialize(Map<?, ?> serializedConfiguration) { public R deserialize(Map<?, ?> serializedConfiguration) {
final var ctorArgs = deserializeConfigurationElements(serializedConfiguration); final Object[] ctorArgs = deserializeConfigurationElements(serializedConfiguration);
final var result = Reflect.callCanonicalConstructor(type, ctorArgs); final R result = Reflect.callCanonicalConstructor(type, ctorArgs);
return postProcessor.apply(result); return postProcessor.apply(result);
} }
@Override @Override
protected void requireSerializableElements() { protected void requireSerializableElements() {
if (serializers.isEmpty()) { if (serializers.isEmpty()) {
String msg = "Record type '%s' does not define any components." throw new ConfigurationException("Record type '" + type.getSimpleName() + "' does not define any components.");
.formatted(type.getSimpleName());
throw new ConfigurationException(msg);
} }
} }
@Override @Override
protected String baseDeserializeExceptionMessage(RecordComponentElement element, Object value) { protected String baseDeserializeExceptionMessage(RecordComponentElement element, Object value) {
return "Deserialization of value '%s' with type '%s' for component '%s' of record '%s' failed." return "Deserialization of value '" + value + "' with type '" + value.getClass() + "' for component '" + element.element() + "' of record '" + element.declaringType() + "' failed.";
.formatted(value, value.getClass(), element.element(), element.declaringType());
} }
@Override @Override
protected List<RecordComponentElement> elements() { protected List<RecordComponentElement> elements() {
return Arrays.stream(type.getRecordComponents()) return Arrays.stream(R$Class.of(type).getRecordComponents())
.map(RecordComponentElement::new) .map(RecordComponentElement::new)
.toList(); .collect(Collectors.toList());
} }
@Override @Override

@ -1,5 +1,8 @@
package de.exlll.configlib; package de.exlll.configlib;
import de.exlll.configlib.util.ref.R$Class;
import de.exlll.configlib.util.ref.R$RecordComponent;
import java.lang.reflect.*; import java.lang.reflect.*;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -45,19 +48,14 @@ final class Reflect {
constructor.setAccessible(true); constructor.setAccessible(true);
return constructor.newInstance(arguments); return constructor.newInstance(arguments);
} catch (NoSuchMethodException e) { } catch (NoSuchMethodException e) {
String msg = "Type '%s' doesn't have a constructor with parameters: %s." throw new RuntimeException("Type '" + cls.getSimpleName() + "' doesn't have a constructor with parameters: " + argumentTypeNamesJoined(argumentTypes) + ".", e);
.formatted(cls.getSimpleName(), argumentTypeNamesJoined(argumentTypes));
throw new RuntimeException(msg, e);
} catch (IllegalAccessException e) { } catch (IllegalAccessException e) {
// cannot happen because we set the constructor to be accessible. // cannot happen because we set the constructor to be accessible.
throw new RuntimeException(e); throw new RuntimeException(e);
} catch (InstantiationException e) { } catch (InstantiationException e) {
String msg = "Type '%s' is not instantiable.".formatted(cls.getSimpleName()); throw new RuntimeException("Type '" + cls.getSimpleName() + "' is not instantiable.", e);
throw new RuntimeException(msg, e);
} catch (InvocationTargetException e) { } catch (InvocationTargetException e) {
String msg = "Constructor of type '%s' with parameters '%s' threw an exception." throw new RuntimeException("Constructor of type '" + cls.getSimpleName() + "' with parameters '" + argumentTypeNamesJoined(argumentTypes) + "' threw an exception.", e);
.formatted(cls.getSimpleName(), argumentTypeNamesJoined(argumentTypes));
throw new RuntimeException(msg, e);
} }
} }
@ -79,22 +77,15 @@ final class Reflect {
constructor.setAccessible(true); constructor.setAccessible(true);
return constructor.newInstance(); return constructor.newInstance();
} catch (NoSuchMethodException e) { } catch (NoSuchMethodException e) {
String msg = "Type '%s' doesn't have a no-args constructor." throw new RuntimeException("Type '" + cls.getSimpleName() + "' doesn't have a no-args constructor.", e);
.formatted(cls.getSimpleName());
throw new RuntimeException(msg, e);
} catch (IllegalAccessException e) { } catch (IllegalAccessException e) {
/* This exception should not be thrown because /* This exception should not be thrown because
* we set the constructor to be accessible. */ * we set the constructor to be accessible. */
String msg = "No-args constructor of type '%s' not accessible." throw new RuntimeException("No-args constructor of type '" + cls.getSimpleName() + "' not accessible.", e);
.formatted(cls.getSimpleName());
throw new RuntimeException(msg, e);
} catch (InstantiationException e) { } catch (InstantiationException e) {
String msg = "Type '%s' is not instantiable.".formatted(cls.getSimpleName()); throw new RuntimeException("Type '" + cls.getSimpleName() + "' is not instantiable.", e);
throw new RuntimeException(msg, e);
} catch (InvocationTargetException e) { } catch (InvocationTargetException e) {
String msg = "No-args constructor of type '%s' threw an exception." throw new RuntimeException("No-args constructor of type '" + cls.getSimpleName() + "' threw an exception.", e);
.formatted(cls.getSimpleName());
throw new RuntimeException(msg, e);
} }
} }
@ -115,9 +106,7 @@ final class Reflect {
// cannot happen because records are instantiable // cannot happen because records are instantiable
throw new RuntimeException(e); throw new RuntimeException(e);
} catch (InvocationTargetException e) { } catch (InvocationTargetException e) {
String msg = "The canonical constructor of record type '%s' threw an exception." throw new RuntimeException("The canonical constructor of record type '" + recordType.getSimpleName() + "' threw an exception.", e);
.formatted(recordType.getSimpleName());
throw new RuntimeException(msg, e);
} }
} }
@ -135,8 +124,8 @@ final class Reflect {
} }
private static <R> Class<?>[] recordParameterTypes(Class<R> recordType) { private static <R> Class<?>[] recordParameterTypes(Class<R> recordType) {
return Arrays.stream(recordType.getRecordComponents()) return Arrays.stream(R$Class.of(recordType).getRecordComponents())
.map(RecordComponent::getType) .map(R$RecordComponent::getType)
.toArray(Class<?>[]::new); .toArray(Class<?>[]::new);
} }
@ -158,25 +147,20 @@ final class Reflect {
return field.get(instance); return field.get(instance);
} catch (IllegalAccessException e) { } catch (IllegalAccessException e) {
/* This exception should not be thrown because we set the field to be accessible. */ /* This exception should not be thrown because we set the field to be accessible. */
String msg = "Illegal access of field '%s' on object %s.".formatted(field, instance); throw new RuntimeException("Illegal access of field '" + field + "' on object " + instance + ".", e);
throw new RuntimeException(msg, e);
} }
} }
static Object getValue(RecordComponent component, Object recordInstance) { static Object getValue(R$RecordComponent component, Object recordInstance) {
final Method accessor = component.getAccessor(); final Method accessor = component.getAccessor();
try { try {
accessor.setAccessible(true); accessor.setAccessible(true);
return accessor.invoke(recordInstance); return accessor.invoke(recordInstance);
} catch (IllegalAccessException e) { } catch (IllegalAccessException e) {
/* Should not be thrown because we set the method to be accessible. */ /* Should not be thrown because we set the method to be accessible. */
String msg = "Illegal access of method '%s' on record '%s'." throw new RuntimeException("Illegal access of method '" + accessor + "' on record '" + recordInstance + "'.", e);
.formatted(accessor, recordInstance);
throw new RuntimeException(msg, e);
} catch (InvocationTargetException e) { } catch (InvocationTargetException e) {
String msg = "Invocation of method '%s' on record '%s' failed." throw new RuntimeException("Invocation of method '" + accessor + "' on record '" + recordInstance + "' failed.", e);
.formatted(accessor, recordInstance);
throw new RuntimeException(msg, e);
} }
} }
@ -186,8 +170,7 @@ final class Reflect {
field.set(instance, value); field.set(instance, value);
} catch (IllegalAccessException e) { } catch (IllegalAccessException e) {
/* This exception should not be thrown because we set the field to be accessible. */ /* This exception should not be thrown because we set the field to be accessible. */
String msg = "Illegal access of field '%s' on object %s.".formatted(field, instance); throw new RuntimeException("Illegal access of field '" + field + "' on object " + instance + ".", e);
throw new RuntimeException(msg, e);
} }
} }
@ -228,7 +211,7 @@ final class Reflect {
} }
static boolean isConfigurationType(Class<?> type) { static boolean isConfigurationType(Class<?> type) {
return type.isRecord() || (type.getAnnotation(Configuration.class) != null); return R$Class.of(type).isRecord() || (type.getAnnotation(Configuration.class) != null);
} }
static boolean isIgnored(Field field) { static boolean isIgnored(Field field) {

@ -4,15 +4,30 @@ import java.lang.reflect.AnnotatedType;
import static de.exlll.configlib.Validator.requireNonNull; import static de.exlll.configlib.Validator.requireNonNull;
record SerializerContextImpl( class SerializerContextImpl implements SerializerContext {
ConfigurationProperties properties, private final ConfigurationProperties properties;
ConfigurationElement<?> element, private final ConfigurationElement<?> element;
AnnotatedType annotatedType private final AnnotatedType annotatedType;
) implements SerializerContext {
SerializerContextImpl { SerializerContextImpl(ConfigurationProperties properties, ConfigurationElement<?> element, AnnotatedType annotatedType) {
properties = requireNonNull(properties, "configuration properties"); this.properties = requireNonNull(properties, "configuration properties");
element = requireNonNull(element, "configuration element"); this.element = requireNonNull(element, "configuration element");
annotatedType = requireNonNull(annotatedType, "annotated type"); this.annotatedType = requireNonNull(annotatedType, "annotated type");
}
@Override
public ConfigurationProperties properties() {
return properties;
}
@Override
public ConfigurationElement<?> element() {
return element;
}
@Override
public AnnotatedType annotatedType() {
return annotatedType;
} }
} }

@ -1,8 +1,10 @@
package de.exlll.configlib; package de.exlll.configlib;
import de.exlll.configlib.Serializers.*; import de.exlll.configlib.Serializers.*;
import de.exlll.configlib.util.MapUtil;
import java.io.File; import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.*; import java.lang.reflect.*;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
@ -16,39 +18,41 @@ import java.time.LocalTime;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.function.Function;
import java.util.function.Predicate;
import static de.exlll.configlib.Validator.requireNonNull; import static de.exlll.configlib.Validator.requireNonNull;
final class SerializerSelector { final class SerializerSelector {
private static final Map<Class<?>, Serializer<?, ?>> DEFAULT_SERIALIZERS = Map.ofEntries( private static final Map<Class<?>, Serializer<?, ?>> DEFAULT_SERIALIZERS = Map.ofEntries(
Map.entry(boolean.class, new BooleanSerializer()), MapUtil.entry(boolean.class, new BooleanSerializer()),
Map.entry(Boolean.class, new BooleanSerializer()), MapUtil.entry(Boolean.class, new BooleanSerializer()),
Map.entry(byte.class, new NumberSerializer(byte.class)), MapUtil.entry(byte.class, new NumberSerializer(byte.class)),
Map.entry(Byte.class, new NumberSerializer(Byte.class)), MapUtil.entry(Byte.class, new NumberSerializer(Byte.class)),
Map.entry(short.class, new NumberSerializer(short.class)), MapUtil.entry(short.class, new NumberSerializer(short.class)),
Map.entry(Short.class, new NumberSerializer(Short.class)), MapUtil.entry(Short.class, new NumberSerializer(Short.class)),
Map.entry(int.class, new NumberSerializer(int.class)), MapUtil.entry(int.class, new NumberSerializer(int.class)),
Map.entry(Integer.class, new NumberSerializer(Integer.class)), MapUtil.entry(Integer.class, new NumberSerializer(Integer.class)),
Map.entry(long.class, new NumberSerializer(long.class)), MapUtil.entry(long.class, new NumberSerializer(long.class)),
Map.entry(Long.class, new NumberSerializer(Long.class)), MapUtil.entry(Long.class, new NumberSerializer(Long.class)),
Map.entry(float.class, new NumberSerializer(float.class)), MapUtil.entry(float.class, new NumberSerializer(float.class)),
Map.entry(Float.class, new NumberSerializer(Float.class)), MapUtil.entry(Float.class, new NumberSerializer(Float.class)),
Map.entry(double.class, new NumberSerializer(double.class)), MapUtil.entry(double.class, new NumberSerializer(double.class)),
Map.entry(Double.class, new NumberSerializer(Double.class)), MapUtil.entry(Double.class, new NumberSerializer(Double.class)),
Map.entry(char.class, new CharacterSerializer()), MapUtil.entry(char.class, new CharacterSerializer()),
Map.entry(Character.class, new CharacterSerializer()), MapUtil.entry(Character.class, new CharacterSerializer()),
Map.entry(String.class, new StringSerializer()), MapUtil.entry(String.class, new StringSerializer()),
Map.entry(BigInteger.class, new BigIntegerSerializer()), MapUtil.entry(BigInteger.class, new BigIntegerSerializer()),
Map.entry(BigDecimal.class, new BigDecimalSerializer()), MapUtil.entry(BigDecimal.class, new BigDecimalSerializer()),
Map.entry(LocalDate.class, new LocalDateSerializer()), MapUtil.entry(LocalDate.class, new LocalDateSerializer()),
Map.entry(LocalTime.class, new LocalTimeSerializer()), MapUtil.entry(LocalTime.class, new LocalTimeSerializer()),
Map.entry(LocalDateTime.class, new LocalDateTimeSerializer()), MapUtil.entry(LocalDateTime.class, new LocalDateTimeSerializer()),
Map.entry(Instant.class, new InstantSerializer()), MapUtil.entry(Instant.class, new InstantSerializer()),
Map.entry(UUID.class, new UuidSerializer()), MapUtil.entry(UUID.class, new UuidSerializer()),
Map.entry(File.class, new FileSerializer()), MapUtil.entry(File.class, new FileSerializer()),
Map.entry(Path.class, new PathSerializer()), MapUtil.entry(Path.class, new PathSerializer()),
Map.entry(URL.class, new UrlSerializer()), MapUtil.entry(URL.class, new UrlSerializer()),
Map.entry(URI.class, new UriSerializer()) MapUtil.entry(URI.class, new UriSerializer())
); );
private final ConfigurationProperties properties; private final ConfigurationProperties properties;
/** /**
@ -101,18 +105,28 @@ final class SerializerSelector {
} }
private Serializer<?, ?> selectCustomSerializer(AnnotatedType annotatedType) { private Serializer<?, ?> selectCustomSerializer(AnnotatedType annotatedType) {
return findConfigurationElementSerializer(annotatedType) Optional<Serializer<?, ?>> o = findConfigurationElementSerializer(annotatedType);
.or(() -> findSerializerFactoryForType(annotatedType)) if (o.isPresent()) return o.get();
.or(() -> findSerializerForType(annotatedType))
.or(() -> findSerializerOnType(annotatedType)) o = findSerializerFactoryForType(annotatedType);
.or(() -> findMetaSerializerOnType(annotatedType)) if (o.isPresent()) return o.get();
.or(() -> findSerializerByCondition(annotatedType))
.orElse(null); o = findSerializerForType(annotatedType);
if (o.isPresent()) return o.get();
o = findSerializerOnType(annotatedType);
if (o.isPresent()) return o.get();
o = findMetaSerializerOnType(annotatedType);
if (o.isPresent()) return o.get();
o = findSerializerByCondition(annotatedType);
return o.orElse(null);
} }
private Optional<Serializer<?, ?>> findConfigurationElementSerializer(AnnotatedType annotatedType) { private Optional<Serializer<?, ?>> findConfigurationElementSerializer(AnnotatedType annotatedType) {
// SerializeWith annotation on configuration elements // SerializeWith annotation on configuration elements
final var annotation = element.annotation(SerializeWith.class); final SerializeWith annotation = element.annotation(SerializeWith.class);
if ((annotation != null) && (currentNesting == annotation.nesting())) { if ((annotation != null) && (currentNesting == annotation.nesting())) {
return Optional.of(newSerializerFromAnnotation(annotatedType, annotation)); return Optional.of(newSerializerFromAnnotation(annotatedType, annotation));
} }
@ -121,11 +135,12 @@ final class SerializerSelector {
private Optional<Serializer<?, ?>> findSerializerFactoryForType(AnnotatedType annotatedType) { private Optional<Serializer<?, ?>> findSerializerFactoryForType(AnnotatedType annotatedType) {
// Serializer factory registered for Type via configurations properties // Serializer factory registered for Type via configurations properties
if ((annotatedType.getType() instanceof Class<?> cls) && if ((annotatedType.getType() instanceof Class<?>) &&
properties.getSerializerFactories().containsKey(cls)) { properties.getSerializerFactories().containsKey((Class<?>) annotatedType.getType())) {
final var context = new SerializerContextImpl(properties, element, annotatedType); Class<?> cls = (Class<?>) annotatedType.getType();
final var factory = properties.getSerializerFactories().get(cls); final SerializerContextImpl context = new SerializerContextImpl(properties, element, annotatedType);
final var serializer = factory.apply(context); final Function<? super SerializerContext, ? extends Serializer<?, ?>> factory = properties.getSerializerFactories().get(cls);
final Serializer<?, ?> serializer = factory.apply(context);
if (serializer == null) { if (serializer == null) {
String msg = "Serializer factories must not return null."; String msg = "Serializer factories must not return null.";
throw new ConfigurationException(msg); throw new ConfigurationException(msg);
@ -137,8 +152,9 @@ final class SerializerSelector {
private Optional<Serializer<?, ?>> findSerializerForType(AnnotatedType annotatedType) { private Optional<Serializer<?, ?>> findSerializerForType(AnnotatedType annotatedType) {
// Serializer registered for Type via configurations properties // Serializer registered for Type via configurations properties
if ((annotatedType.getType() instanceof Class<?> cls) && if ((annotatedType.getType() instanceof Class<?>) &&
properties.getSerializers().containsKey(cls)) { properties.getSerializers().containsKey((Class<?>) annotatedType.getType())) {
Class<?> cls = (Class<?>) annotatedType.getType();
return Optional.of(properties.getSerializers().get(cls)); return Optional.of(properties.getSerializers().get(cls));
} }
return Optional.empty(); return Optional.empty();
@ -146,9 +162,10 @@ final class SerializerSelector {
private Optional<Serializer<?, ?>> findSerializerOnType(AnnotatedType annotatedType) { private Optional<Serializer<?, ?>> findSerializerOnType(AnnotatedType annotatedType) {
// SerializeWith annotation on type // SerializeWith annotation on type
if ((annotatedType.getType() instanceof Class<?> cls) && if ((annotatedType.getType() instanceof Class<?>) &&
(cls.getDeclaredAnnotation(SerializeWith.class) != null)) { (((Class<?>) annotatedType.getType()).getDeclaredAnnotation(SerializeWith.class) != null)) {
final var annotation = cls.getDeclaredAnnotation(SerializeWith.class); final Class<?> cls = (Class<?>) annotatedType.getType();
final SerializeWith annotation = cls.getDeclaredAnnotation(SerializeWith.class);
return Optional.of(newSerializerFromAnnotation(annotatedType, annotation)); return Optional.of(newSerializerFromAnnotation(annotatedType, annotation));
} }
return Optional.empty(); return Optional.empty();
@ -156,10 +173,11 @@ final class SerializerSelector {
private Optional<Serializer<?, ?>> findMetaSerializerOnType(AnnotatedType annotatedType) { private Optional<Serializer<?, ?>> findMetaSerializerOnType(AnnotatedType annotatedType) {
// SerializeWith meta annotation on type // SerializeWith meta annotation on type
if ((annotatedType.getType() instanceof Class<?> cls)) { if (annotatedType.getType() instanceof Class<?>) {
for (final var meta : cls.getDeclaredAnnotations()) { final Class<?> cls = (Class<?>) annotatedType.getType();
final var metaType = meta.annotationType(); for (final Annotation meta : cls.getDeclaredAnnotations()) {
final var annotation = metaType.getDeclaredAnnotation(SerializeWith.class); final Class<? extends Annotation> metaType = meta.annotationType();
final SerializeWith annotation = metaType.getDeclaredAnnotation(SerializeWith.class);
if (annotation != null) if (annotation != null)
return Optional.of(newSerializerFromAnnotation(annotatedType, annotation)); return Optional.of(newSerializerFromAnnotation(annotatedType, annotation));
} }
@ -169,7 +187,7 @@ final class SerializerSelector {
private Optional<Serializer<?, ?>> findSerializerByCondition(AnnotatedType annotatedType) { private Optional<Serializer<?, ?>> findSerializerByCondition(AnnotatedType annotatedType) {
// Serializer registered for condition via configurations properties // Serializer registered for condition via configurations properties
for (var entry : properties.getSerializersByCondition().entrySet()) { for (Map.Entry<Predicate<? super Type>, Serializer<?, ?>> entry : properties.getSerializersByCondition().entrySet()) {
if (entry.getKey().test(annotatedType.getType())) if (entry.getKey().test(annotatedType.getType()))
return Optional.of(entry.getValue()); return Optional.of(entry.getValue());
} }
@ -180,7 +198,7 @@ final class SerializerSelector {
AnnotatedType annotatedType, AnnotatedType annotatedType,
SerializeWith annotation SerializeWith annotation
) { ) {
final var context = new SerializerContextImpl(properties, element, annotatedType); final SerializerContextImpl context = new SerializerContextImpl(properties, element, annotatedType);
return Serializers.newCustomSerializer(annotation.serializer(), context); return Serializers.newCustomSerializer(annotation.serializer(), context);
} }
@ -191,7 +209,7 @@ final class SerializerSelector {
if (Reflect.isEnumType(cls)) { if (Reflect.isEnumType(cls)) {
// The following cast won't fail because we just checked that it's an enum. // The following cast won't fail because we just checked that it's an enum.
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
final var enumType = (Class<? extends Enum<?>>) cls; final Class<? extends Enum<?>> enumType = (Class<? extends Enum<?>>) cls;
return new Serializers.EnumSerializer(enumType); return new Serializers.EnumSerializer(enumType);
} }
if (Reflect.isArrayType(cls)) if (Reflect.isArrayType(cls))
@ -225,34 +243,34 @@ final class SerializerSelector {
} else if (elementType == double.class) { } else if (elementType == double.class) {
return new PrimitiveDoubleArraySerializer(); return new PrimitiveDoubleArraySerializer();
} }
var elementSerializer = selectForType(annotatedElementType); Serializer<?, ?> elementSerializer = selectForType(annotatedElementType);
var inputNulls = properties.inputNulls(); boolean inputNulls = properties.inputNulls();
var outputNulls = properties.outputNulls(); boolean outputNulls = properties.outputNulls();
return new ArraySerializer<>(elementType, elementSerializer, outputNulls, inputNulls); return new ArraySerializer<>(elementType, elementSerializer, outputNulls, inputNulls);
} }
private Serializer<?, ?> selectForParameterizedType(AnnotatedParameterizedType annotatedType) { private Serializer<?, ?> selectForParameterizedType(AnnotatedParameterizedType annotatedType) {
// the raw type returned by Java is always a class // the raw type returned by Java is always a class
final var type = (ParameterizedType) annotatedType.getType(); final ParameterizedType type = (ParameterizedType) annotatedType.getType();
final var rawType = (Class<?>) type.getRawType(); final Class<?> rawType = (Class<?>) type.getRawType();
final var typeArgs = annotatedType.getAnnotatedActualTypeArguments(); final AnnotatedType[] typeArgs = annotatedType.getAnnotatedActualTypeArguments();
final var inputNulls = properties.inputNulls(); final boolean inputNulls = properties.inputNulls();
final var outputNulls = properties.outputNulls(); final boolean outputNulls = properties.outputNulls();
if (Reflect.isListType(rawType)) { if (Reflect.isListType(rawType)) {
var elementSerializer = selectForType(typeArgs[0]); Serializer<?, ?> elementSerializer = selectForType(typeArgs[0]);
return new ListSerializer<>(elementSerializer, outputNulls, inputNulls); return new ListSerializer<>(elementSerializer, outputNulls, inputNulls);
} else if (Reflect.isSetType(rawType)) { } else if (Reflect.isSetType(rawType)) {
var elementSerializer = selectForType(typeArgs[0]); Serializer<?, ?> elementSerializer = selectForType(typeArgs[0]);
return properties.serializeSetsAsLists() return properties.serializeSetsAsLists()
? new SetAsListSerializer<>(elementSerializer, outputNulls, inputNulls) ? new SetAsListSerializer<>(elementSerializer, outputNulls, inputNulls)
: new SetSerializer<>(elementSerializer, outputNulls, inputNulls); : new SetSerializer<>(elementSerializer, outputNulls, inputNulls);
} else if (Reflect.isMapType(rawType)) { } else if (Reflect.isMapType(rawType)) {
if ((typeArgs[0].getType() instanceof Class<?> cls) && if ((typeArgs[0].getType() instanceof Class<?>) &&
(DEFAULT_SERIALIZERS.containsKey(cls) || (DEFAULT_SERIALIZERS.containsKey((Class<?>) typeArgs[0].getType()) ||
Reflect.isEnumType(cls))) { Reflect.isEnumType((Class<?>) typeArgs[0].getType()))) {
var keySerializer = selectForClass(typeArgs[0]); Serializer<?, ?> keySerializer = selectForClass(typeArgs[0]);
var valSerializer = selectForType(typeArgs[1]); Serializer<?, ?> valSerializer = selectForType(typeArgs[1]);
return new MapSerializer<>(keySerializer, valSerializer, outputNulls, inputNulls); return new MapSerializer<>(keySerializer, valSerializer, outputNulls, inputNulls);
} }
String msg = baseExceptionMessage(type) + String msg = baseExceptionMessage(type) +
@ -266,6 +284,6 @@ final class SerializerSelector {
} }
private String baseExceptionMessage(Type type) { private String baseExceptionMessage(Type type) {
return "Cannot select serializer for type '%s'.\n".formatted(type); return "Cannot select serializer for type '" + type + "'.\n";
} }
} }

@ -7,6 +7,7 @@ import java.net.MalformedURLException;
import java.net.URI; import java.net.URI;
import java.net.URL; import java.net.URL;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@ -300,7 +301,7 @@ final class Serializers {
@Override @Override
public Path deserialize(String element) { public Path deserialize(String element) {
return Path.of(element); return Paths.get(element);
} }
} }
@ -454,10 +455,10 @@ final class Serializers {
for (final Map.Entry<S1, S2> entry : element.entrySet()) { for (final Map.Entry<S1, S2> entry : element.entrySet()) {
if (!outputNulls && isEntryNull(entry)) if (!outputNulls && isEntryNull(entry))
continue; continue;
var s1key = entry.getKey(); S1 s1key = entry.getKey();
var s2val = entry.getValue(); S2 s2val = entry.getValue();
var t1key = (s1key == null) ? null : keySerializer.serialize(s1key); T1 t1key = (s1key == null) ? null : keySerializer.serialize(s1key);
var t2val = (s2val == null) ? null : valSerializer.serialize(s2val); T2 t2val = (s2val == null) ? null : valSerializer.serialize(s2val);
result.put(t1key, t2val); result.put(t1key, t2val);
} }
return result; return result;
@ -470,10 +471,10 @@ final class Serializers {
for (final Map.Entry<T1, T2> entry : element.entrySet()) { for (final Map.Entry<T1, T2> entry : element.entrySet()) {
if (!inputNulls && isEntryNull(entry)) if (!inputNulls && isEntryNull(entry))
continue; continue;
var t1key = entry.getKey(); T1 t1key = entry.getKey();
var t2val = entry.getValue(); T2 t2val = entry.getValue();
var s1key = (t1key == null) ? null : keySerializer.deserialize(t1key); S1 s1key = (t1key == null) ? null : keySerializer.deserialize(t1key);
var s2val = (t2val == null) ? null : valSerializer.deserialize(t2val); S2 s2val = (t2val == null) ? null : valSerializer.deserialize(t2val);
result.put(s1key, s2val); result.put(s1key, s2val);
} }
return result; return result;
@ -515,7 +516,7 @@ final class Serializers {
final Stream<T2> stream = outputNulls final Stream<T2> stream = outputNulls
? Arrays.stream(element).map(s -> s == null ? null : serializer.serialize(s)) ? Arrays.stream(element).map(s -> s == null ? null : serializer.serialize(s))
: Arrays.stream(element).filter(Objects::nonNull).map(serializer::serialize); : Arrays.stream(element).filter(Objects::nonNull).map(serializer::serialize);
return stream.toList(); return stream.collect(Collectors.toList());
} }
@Override @Override
@ -543,7 +544,8 @@ final class Serializers {
@Override @Override
public List<Boolean> serialize(Object element) { public List<Boolean> serialize(Object element) {
final boolean[] array = (boolean[]) element; final boolean[] array = (boolean[]) element;
return IntStream.range(0, array.length).mapToObj(i -> array[i]).toList(); return IntStream.range(0, array.length).mapToObj(i -> array[i])
.collect(Collectors.toList());
} }
@Override @Override
@ -564,7 +566,7 @@ final class Serializers {
final char[] array = (char[]) element; final char[] array = (char[]) element;
return IntStream.range(0, array.length) return IntStream.range(0, array.length)
.mapToObj(i -> serializer.serialize(array[i])) .mapToObj(i -> serializer.serialize(array[i]))
.toList(); .collect(Collectors.toList());
} }
@Override @Override
@ -587,7 +589,7 @@ final class Serializers {
final byte[] array = (byte[]) element; final byte[] array = (byte[]) element;
return IntStream.range(0, array.length). return IntStream.range(0, array.length).
mapToObj(i -> serializer.serialize(array[i])) mapToObj(i -> serializer.serialize(array[i]))
.toList(); .collect(Collectors.toList());
} }
@Override @Override
@ -610,7 +612,7 @@ final class Serializers {
final short[] array = (short[]) element; final short[] array = (short[]) element;
return IntStream.range(0, array.length) return IntStream.range(0, array.length)
.mapToObj(i -> serializer.serialize(array[i])) .mapToObj(i -> serializer.serialize(array[i]))
.toList(); .collect(Collectors.toList());
} }
@Override @Override
@ -631,7 +633,7 @@ final class Serializers {
@Override @Override
public List<Number> serialize(Object element) { public List<Number> serialize(Object element) {
final int[] array = (int[]) element; final int[] array = (int[]) element;
return Arrays.stream(array).mapToObj(serializer::serialize).toList(); return Arrays.stream(array).mapToObj(serializer::serialize).collect(Collectors.toList());
} }
@Override @Override
@ -652,7 +654,7 @@ final class Serializers {
@Override @Override
public List<Number> serialize(Object element) { public List<Number> serialize(Object element) {
final long[] array = (long[]) element; final long[] array = (long[]) element;
return Arrays.stream(array).mapToObj(serializer::serialize).toList(); return Arrays.stream(array).mapToObj(serializer::serialize).collect(Collectors.toList());
} }
@Override @Override
@ -675,7 +677,7 @@ final class Serializers {
final float[] array = (float[]) element; final float[] array = (float[]) element;
return IntStream.range(0, array.length) return IntStream.range(0, array.length)
.mapToObj(i -> serializer.serialize(array[i])) .mapToObj(i -> serializer.serialize(array[i]))
.toList(); .collect(Collectors.toList());
} }
@Override @Override
@ -696,7 +698,7 @@ final class Serializers {
@Override @Override
public List<Number> serialize(Object element) { public List<Number> serialize(Object element) {
final double[] array = (double[]) element; final double[] array = (double[]) element;
return Arrays.stream(array).mapToObj(serializer::serialize).toList(); return Arrays.stream(array).mapToObj(serializer::serialize).collect(Collectors.toList());
} }
@Override @Override

@ -2,11 +2,13 @@ package de.exlll.configlib;
import de.exlll.configlib.ConfigurationElements.FieldElement; import de.exlll.configlib.ConfigurationElements.FieldElement;
import de.exlll.configlib.ConfigurationElements.RecordComponentElement; import de.exlll.configlib.ConfigurationElements.RecordComponentElement;
import de.exlll.configlib.util.PredicateUtil;
import de.exlll.configlib.util.ref.R$Class;
import de.exlll.configlib.util.ref.R$RecordComponent;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.lang.reflect.RecordComponent;
import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@ -17,9 +19,8 @@ import java.util.stream.Collectors;
import static de.exlll.configlib.Validator.requireNonNull; import static de.exlll.configlib.Validator.requireNonNull;
sealed abstract class TypeSerializer<T, E extends ConfigurationElement<?>> public abstract class TypeSerializer<T, E extends ConfigurationElement<?>>
implements Serializer<T, Map<?, ?>> implements Serializer<T, Map<?, ?>> {
permits ConfigurationSerializer, RecordSerializer {
protected final Class<T> type; protected final Class<T> type;
protected final ConfigurationProperties properties; protected final ConfigurationProperties properties;
protected final NameFormatter formatter; protected final NameFormatter formatter;
@ -39,13 +40,13 @@ sealed abstract class TypeSerializer<T, E extends ConfigurationElement<?>>
Class<T> type, Class<T> type,
ConfigurationProperties properties ConfigurationProperties properties
) { ) {
return type.isRecord() return R$Class.of(type).isRecord()
? new RecordSerializer<>(type, properties) ? new RecordSerializer<>(type, properties)
: new ConfigurationSerializer<>(type, properties); : new ConfigurationSerializer<>(type, properties);
} }
Map<String, Serializer<?, ?>> buildSerializerMap() { Map<String, Serializer<?, ?>> buildSerializerMap() {
final var selector = new SerializerSelector(properties); final SerializerSelector selector = new SerializerSelector(properties);
try { try {
return elements().stream().collect(Collectors.toMap( return elements().stream().collect(Collectors.toMap(
ConfigurationElement::name, ConfigurationElement::name,
@ -79,20 +80,14 @@ sealed abstract class TypeSerializer<T, E extends ConfigurationElement<?>>
// This cast can lead to a ClassCastException if an element of type X is // This cast can lead to a ClassCastException if an element of type X is
// serialized by a custom serializer that expects a different type Y. // serialized by a custom serializer that expects a different type Y.
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
final var serializer = (Serializer<Object, Object>) serializers.get(element.name()); final Serializer<Object, Object> serializer = (Serializer<Object, Object>) serializers.get(element.name());
try { try {
return (value != null) ? serializer.serialize(value) : null; return (value != null) ? serializer.serialize(value) : null;
} catch (ClassCastException e) { } catch (ClassCastException e) {
String msg = ("Serialization of value '%s' for element '%s' of type '%s' failed.\n" + throw new ConfigurationException(
"Serialization of value '" + value + "' for element '" + element.element() + "' of type '" + element.declaringType() + "' failed.\n" +
"The type of the object to be serialized does not match the type " + "The type of the object to be serialized does not match the type " +
"the custom serializer of type '%s' expects.") "the custom serializer of type " + serializer.getClass() + "' expects.", e);
.formatted(
value,
element.element(),
element.declaringType(),
serializer.getClass()
);
throw new ConfigurationException(msg, e);
} }
} }
@ -100,17 +95,16 @@ sealed abstract class TypeSerializer<T, E extends ConfigurationElement<?>>
// This unchecked cast leads to an exception if the type of the object which // This unchecked cast leads to an exception if the type of the object which
// is deserialized is not a subtype of the type the deserializer expects. // is deserialized is not a subtype of the type the deserializer expects.
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
final var serializer = (Serializer<Object, Object>) final Serializer<Object, Object> serializer = (Serializer<Object, Object>) serializers.get(element.name());
serializers.get(element.name());
final Object deserialized; final Object deserialized;
try { try {
deserialized = serializer.deserialize(value); deserialized = serializer.deserialize(value);
} catch (ClassCastException e) { } catch (ClassCastException e) {
String msg = baseDeserializeExceptionMessage(element, value) + "\n" + throw new ConfigurationException(
baseDeserializeExceptionMessage(element, value) + "\n" +
"The type of the object to be deserialized does not " + "The type of the object to be deserialized does not " +
"match the type the deserializer expects."; "match the type the deserializer expects.", e);
throw new ConfigurationException(msg, e);
} catch (RuntimeException e) { } catch (RuntimeException e) {
String msg = baseDeserializeExceptionMessage(element, value); String msg = baseDeserializeExceptionMessage(element, value);
throw new ConfigurationException(msg, e); throw new ConfigurationException(msg, e);
@ -121,12 +115,12 @@ sealed abstract class TypeSerializer<T, E extends ConfigurationElement<?>>
protected final Object[] deserializeConfigurationElements( protected final Object[] deserializeConfigurationElements(
Map<?, ?> serializedConfiguration Map<?, ?> serializedConfiguration
) { ) {
final var elements = elements(); final List<E> elements = elements();
final var result = new Object[elements.size()]; final Object[] result = new Object[elements.size()];
for (int i = 0, size = elements.size(); i < size; i++) { for (int i = 0, size = elements.size(); i < size; i++) {
final var element = elements.get(i); final E element = elements.get(i);
final var formattedName = formatter.format(element.name()); final String formattedName = formatter.format(element.name());
if (!serializedConfiguration.containsKey(formattedName)) { if (!serializedConfiguration.containsKey(formattedName)) {
final Object defaultValue = getDefaultValueOf(element); final Object defaultValue = getDefaultValueOf(element);
@ -134,7 +128,7 @@ sealed abstract class TypeSerializer<T, E extends ConfigurationElement<?>>
continue; continue;
} }
final var serializedValue = serializedConfiguration.get(formattedName); final Object serializedValue = serializedConfiguration.get(formattedName);
if ((serializedValue == null) && properties.inputNulls()) { if ((serializedValue == null) && properties.inputNulls()) {
// This statement (and hence the whole block) could be removed, // This statement (and hence the whole block) could be removed,
@ -159,12 +153,12 @@ sealed abstract class TypeSerializer<T, E extends ConfigurationElement<?>>
Object result = deserializeValue; Object result = deserializeValue;
boolean postProcessed = false; boolean postProcessed = false;
for (final var entry : properties.getPostProcessorsByCondition().entrySet()) { for (final Map.Entry<Predicate<? super ConfigurationElement<?>>, UnaryOperator<?>> entry : properties.getPostProcessorsByCondition().entrySet()) {
final var condition = entry.getKey(); final Predicate<? super ConfigurationElement<?>> condition = entry.getKey();
if (!condition.test(element)) continue; if (!condition.test(element)) continue;
final var postProcessor = entry.getValue(); final UnaryOperator<?> postProcessor = entry.getValue();
result = tryApplyPostProcessorForElement(element, postProcessor, result); result = tryApplyPostProcessorForElement(element, postProcessor, result);
postProcessed = true; postProcessed = true;
} }
@ -186,14 +180,13 @@ sealed abstract class TypeSerializer<T, E extends ConfigurationElement<?>>
// This cast can lead to a ClassCastException if an element of type X is // This cast can lead to a ClassCastException if an element of type X is
// annotated with a post-processor that takes values of some other type Y. // annotated with a post-processor that takes values of some other type Y.
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
final var pp = (UnaryOperator<Object>) postProcessor; final UnaryOperator<Object> pp = (UnaryOperator<Object>) postProcessor;
return pp.apply(value); return pp.apply(value);
} catch (ClassCastException e) { } catch (ClassCastException e) {
String msg = ("Deserialization of value '%s' for element '%s' of type '%s' failed.\n" + throw new ConfigurationException(
"Deserialization of value '" + value + "' for element '" + element.element() + "' of type '" + element.declaringType() + "' failed.\n" +
"The type of the object to be deserialized does not match the type " + "The type of the object to be deserialized does not match the type " +
"post-processor '%s' expects.") "post-processor '" + postProcessor + "' expects.", e);
.formatted(value, element.element(), element.declaringType(), postProcessor);
throw new ConfigurationException(msg, e);
} }
} }
@ -202,22 +195,22 @@ sealed abstract class TypeSerializer<T, E extends ConfigurationElement<?>>
) { ) {
if (!element.type().isPrimitive()) return; if (!element.type().isPrimitive()) return;
if (element instanceof RecordComponentElement recordComponentElement) { if (element instanceof RecordComponentElement) {
final RecordComponent component = recordComponentElement.element(); RecordComponentElement recordComponentElement = (RecordComponentElement) element;
String msg = """ final R$RecordComponent component = recordComponentElement.element();
Post-processors must not return null for primitive record \ throw new ConfigurationException(
components but some post-processor of component '%s' of \ "Post-processors must not return null for primitive record " +
record type '%s' does.\ "components but some post-processor of component '" + component + "' of " +
""".formatted(component, component.getDeclaringRecord()); "record type '" + component.getDeclaringRecord() + "' does.");
throw new ConfigurationException(msg);
} }
if (element instanceof FieldElement fieldElement) { if (element instanceof FieldElement) {
FieldElement fieldElement = (FieldElement) element;
final Field field = fieldElement.element(); final Field field = fieldElement.element();
String msg = ("Post-processors must not return null for primitive fields " + throw new ConfigurationException(
"but some post-processor of field '%s' does.") "Post-processors must not return null for primitive fields " +
.formatted(field); "but some post-processor of field '" + field + "' does."
throw new ConfigurationException(msg); );
} }
throw new ConfigurationException("Unhandled ConfigurationElement: " + element); throw new ConfigurationException("Unhandled ConfigurationElement: " + element);
@ -226,20 +219,22 @@ sealed abstract class TypeSerializer<T, E extends ConfigurationElement<?>>
private static void requireNonPrimitiveType(ConfigurationElement<?> element) { private static void requireNonPrimitiveType(ConfigurationElement<?> element) {
if (!element.type().isPrimitive()) return; if (!element.type().isPrimitive()) return;
if (element instanceof RecordComponentElement recordComponentElement) { if (element instanceof RecordComponentElement) {
final RecordComponent component = recordComponentElement.element(); RecordComponentElement recordComponentElement = (RecordComponentElement) element;
String msg = ("Cannot set component '%s' of record type '%s' to null. " + final R$RecordComponent component = recordComponentElement.element();
"Primitive types cannot be assigned null values.") throw new ConfigurationException(
.formatted(component, component.getDeclaringRecord()); "Cannot set component '" + component + "' of record type '" + component.getDeclaringRecord() + "' to null. " +
throw new ConfigurationException(msg); "Primitive types cannot be assigned null values."
);
} }
if (element instanceof FieldElement fieldElement) { if (element instanceof FieldElement) {
FieldElement fieldElement = (FieldElement) element;
final Field field = fieldElement.element(); final Field field = fieldElement.element();
String msg = ("Cannot set field '%s' to null value. " + throw new ConfigurationException(
"Primitive types cannot be assigned null.") "Cannot set field '" + field + "' to null value. " +
.formatted(field); "Primitive types cannot be assigned null."
throw new ConfigurationException(msg); );
} }
throw new ConfigurationException("Unhandled ConfigurationElement: " + element); throw new ConfigurationException("Unhandled ConfigurationElement: " + element);
@ -248,45 +243,45 @@ sealed abstract class TypeSerializer<T, E extends ConfigurationElement<?>>
final UnaryOperator<T> createPostProcessorFromAnnotatedMethod() { final UnaryOperator<T> createPostProcessorFromAnnotatedMethod() {
final List<Method> list = Arrays.stream(type.getDeclaredMethods()) final List<Method> list = Arrays.stream(type.getDeclaredMethods())
.filter(method -> method.isAnnotationPresent(PostProcess.class)) .filter(method -> method.isAnnotationPresent(PostProcess.class))
.filter(Predicate.not(Method::isSynthetic)) .filter(PredicateUtil.not(Method::isSynthetic))
.filter(Predicate.not(this::isAccessorMethod)) .filter(PredicateUtil.not(this::isAccessorMethod))
.toList(); .collect(Collectors.toList());
if (list.isEmpty()) if (list.isEmpty())
return UnaryOperator.identity(); return UnaryOperator.identity();
if (list.size() > 1) { if (list.size() > 1) {
String methodNames = String.join("\n ", list.stream().map(Method::toString).toList()); String methodNames = String.join("\n ", list.stream().map(Method::toString).collect(Collectors.toList()));
String msg = "Configuration types must not define more than one method for " + throw new ConfigurationException(
"post-processing but type '%s' defines %d:\n %s" "Configuration types must not define more than one method for " +
.formatted(type, list.size(), methodNames); "post-processing but type '" + type + "' defines " + list.size() + ":\n " + methodNames
throw new ConfigurationException(msg); );
} }
final Method method = list.get(0); final Method method = list.get(0);
final int modifiers = method.getModifiers(); final int modifiers = method.getModifiers();
if (Modifier.isAbstract(modifiers) || Modifier.isStatic(modifiers)) { if (Modifier.isAbstract(modifiers) || Modifier.isStatic(modifiers)) {
String msg = "Post-processing methods must be neither abstract nor static, " + throw new ConfigurationException(
"but post-processing method '%s' of type '%s' is." "Post-processing methods must be neither abstract nor static, " +
.formatted(method, type); "but post-processing method '" + method + "' of type '" + type + "' is."
throw new ConfigurationException(msg); );
} }
final int parameterCount = method.getParameterCount(); final int parameterCount = method.getParameterCount();
if (parameterCount > 0) { if (parameterCount > 0) {
String msg = "Post-processing methods must not define any parameters but " + throw new ConfigurationException(
"post-processing method '%s' of type '%s' defines %d." "Post-processing methods must not define any parameters but " +
.formatted(method, type, parameterCount); "post-processing method '" + method + "' of type '" + type + "' defines " + parameterCount + "."
throw new ConfigurationException(msg); );
} }
final Class<?> returnType = method.getReturnType(); final Class<?> returnType = method.getReturnType();
if ((returnType != void.class) && (returnType != type)) { if ((returnType != void.class) && (returnType != type)) {
String msg = "The return type of post-processing methods must either be 'void' or " + throw new ConfigurationException(
"The return type of post-processing methods must either be 'void' or " +
"the same type as the configuration type in which the post-processing " + "the same type as the configuration type in which the post-processing " +
"method is defined. The return type of the post-processing method of " + "method is defined. The return type of the post-processing method of " +
"type '%s' is neither 'void' nor '%s'." "type '" + type + "' is neither 'void' nor '" + type.getSimpleName() + "'."
.formatted(type, type.getSimpleName()); );
throw new ConfigurationException(msg);
} }
return object -> { return object -> {
@ -303,11 +298,11 @@ sealed abstract class TypeSerializer<T, E extends ConfigurationElement<?>>
} }
final boolean isAccessorMethod(Method method) { final boolean isAccessorMethod(Method method) {
if (!type.isRecord()) return false; if (!R$Class.of(type).isRecord()) return false;
if (!method.getDeclaringClass().equals(type)) return false; if (!method.getDeclaringClass().equals(type)) return false;
if (method.getParameterCount() > 0) return false; if (method.getParameterCount() > 0) return false;
return Arrays.stream(type.getRecordComponents()) return Arrays.stream(R$Class.of(type).getRecordComponents())
.map(RecordComponent::getName) .map(R$RecordComponent::getName)
.anyMatch(s -> s.equals(method.getName())); .anyMatch(s -> s.equals(method.getName()));
} }

@ -1,5 +1,7 @@
package de.exlll.configlib; package de.exlll.configlib;
import de.exlll.configlib.util.ref.R$Class;
import java.util.Objects; import java.util.Objects;
final class Validator { final class Validator {
@ -29,7 +31,7 @@ final class Validator {
static <T> Class<T> requireRecord(Class<T> cls) { static <T> Class<T> requireRecord(Class<T> cls) {
requireNonNull(cls, "type"); requireNonNull(cls, "type");
if (!cls.isRecord()) { if (!R$Class.of(cls).isRecord()) {
String msg = "Class '" + cls.getSimpleName() + "' must be a record."; String msg = "Class '" + cls.getSimpleName() + "' must be a record.";
throw new ConfigurationException(msg); throw new ConfigurationException(msg);
} }

@ -0,0 +1,14 @@
package de.exlll.configlib.util;
import java.util.AbstractMap;
import java.util.Map;
public final class MapUtil {
private MapUtil() {
throw new UnsupportedOperationException("This class cannot be instantiated");
}
public static <K, V> Map.Entry<K, V> entry(K key, V value) {
return new AbstractMap.SimpleImmutableEntry<>(key, value);
}
}

@ -0,0 +1,15 @@
package de.exlll.configlib.util;
import java.util.Objects;
import java.util.function.Predicate;
public final class PredicateUtil {
private PredicateUtil() {
throw new UnsupportedOperationException("This class cannot be instantiated");
}
public static <T> Predicate<T> not(Predicate<? super T> target) {
Objects.requireNonNull(target);
return (Predicate<T>) target.negate();
}
}

@ -0,0 +1,85 @@
package de.exlll.configlib.util;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
public final class RefUtil {
private RefUtil() {
throw new UnsupportedOperationException("This class cannot be instantiated");
}
private static final MethodHandles.Lookup lookup = MethodHandles.lookup();
public static MethodHandle bootstrapStaticMethod(String methodDesc) {
String[] descParts = splitMethodDesc(methodDesc);
Class<?> owner;
try {
owner = Class.forName(descParts[0].replace('/', '.'));
} catch (ClassNotFoundException e) {
return null;
}
MethodType methodType;
try {
methodType = MethodType.fromMethodDescriptorString(descParts[2], owner.getClassLoader());
} catch (TypeNotPresentException e) {
return null;
}
try {
return lookup.findStatic(owner, descParts[1], methodType);
} catch (NoSuchMethodException e) {
return null;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public static MethodHandle bootstrapVirtualMethod(String methodDesc) {
String[] descParts = splitMethodDesc(methodDesc);
Class<?> owner;
try {
owner = Class.forName(descParts[0].replace('/', '.'));
} catch (ClassNotFoundException e) {
return null;
}
MethodType methodType;
try {
methodType = MethodType.fromMethodDescriptorString(descParts[2], owner.getClassLoader());
} catch (TypeNotPresentException e) {
return null;
}
try {
return lookup.findVirtual(owner, descParts[1], methodType);
} catch (NoSuchMethodException e) {
return null;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private static String[] splitMethodDesc(String methodDesc) {
if (methodDesc.length() < 5) { // L;()V
throw new IllegalArgumentException("Invalid method descriptor: " + methodDesc);
}
int descSplit = methodDesc.indexOf(';');
if (descSplit == -1) {
throw new IllegalArgumentException("Invalid method descriptor: " + methodDesc);
}
int argsStart = methodDesc.indexOf('(');
if (argsStart == -1) {
throw new IllegalArgumentException("Invalid method descriptor: " + methodDesc);
}
return new String[] {
methodDesc.substring(1, descSplit), // java/lang/Object
methodDesc.substring(descSplit + 1, argsStart), // method name
methodDesc.substring(argsStart) // (Ljava/lang/Object;)V
};
}
public static <T extends Throwable> RuntimeException sneakyThrow(Throwable t) throws T {
throw (T) t;
}
}

@ -0,0 +1,64 @@
package de.exlll.configlib.util;
import java.util.Objects;
public final class StringUtil {
private StringUtil() {
throw new UnsupportedOperationException("This class cannot be instantiated");
}
/**
* Returns a string consisting of a specific number of concatenated copies of an input string. For
* example, {@code repeat("hey", 3)} returns the string {@code "heyheyhey"}.
*
* <p><b>Java 11+ users:</b> use {@code string.repeat(count)} instead.
*
* @param string any non-null string
* @param count the number of times to repeat it; a nonnegative integer
* @return a string containing {@code string} repeated {@code count} times (the empty string if
* {@code count} is zero)
* @throws IllegalArgumentException if {@code count} is negative
*/
public static String repeat(String string, int count) {
if (string == null) {
throw new IllegalArgumentException("string cannot be null");
}
if (count <= 1) {
if (count < 0) {
throw new IllegalArgumentException("invalid count: " + count);
}
return (count == 0) ? "" : string;
}
// IF YOU MODIFY THE CODE HERE, you must update StringsRepeatBenchmark
final int len = string.length();
final long longSize = (long) len * (long) count;
final int size = (int) longSize;
if (size != longSize) {
throw new ArrayIndexOutOfBoundsException("Required array size too large: " + longSize);
}
final char[] array = new char[size];
string.getChars(0, len, array, 0);
int n;
for (n = len; n < size - n; n <<= 1) {
System.arraycopy(array, 0, array, n, n);
}
System.arraycopy(array, 0, array, n, size - n);
return new String(array);
}
public static boolean isBlank(String $this) {
final int strLen = $this.length();
if (strLen == 0) {
return true;
}
for (int i = 0; i < strLen; i++) {
if (!Character.isWhitespace($this.charAt(i))) {
return false;
}
}
return true;
}
}

@ -0,0 +1,54 @@
package de.exlll.configlib.util.ref;
import de.exlll.configlib.util.RefUtil;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Array;
public final class R$Class {
private static final MethodHandle v$isRecord = RefUtil.bootstrapVirtualMethod("Ljava/lang/Class;isRecord()Z");
private static final MethodHandle v$getRecordComponents = RefUtil.bootstrapVirtualMethod("Ljava/lang/Class;getRecordComponents()[Ljava/lang/reflect/RecordComponent;");
private final Class<?> instance;
private R$Class(Class<?> instance) {
this.instance = instance;
}
public static R$Class of(Class<?> instance) {
return new R$Class(instance);
}
public Class<?> instance() {
return instance;
}
public boolean isRecord() {
if (v$isRecord == null) {
return false;
}
try {
return (boolean) v$isRecord.invoke(instance);
} catch (Throwable e) {
throw RefUtil.sneakyThrow(e);
}
}
public R$RecordComponent[] getRecordComponents() {
if (v$getRecordComponents == null) {
throw new UnsupportedOperationException("Method java.lang.Class#getRecordComponents() not found");
}
Object arr;
try {
arr = v$getRecordComponents.invoke(instance);
} catch (Throwable e) {
throw RefUtil.sneakyThrow(e);
}
int arrLength = Array.getLength(arr);
R$RecordComponent[] result = new R$RecordComponent[arrLength];
for (int i = 0; i < arrLength; i++) {
result[i] = R$RecordComponent.of(Array.get(arr, i));
}
return result;
}
}

@ -0,0 +1,122 @@
package de.exlll.configlib.util.ref;
import de.exlll.configlib.util.RefUtil;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Method;
public final class R$RecordComponent implements AnnotatedElement {
private static final MethodHandle v$getName = RefUtil.bootstrapVirtualMethod("Ljava/lang/reflect/RecordComponent;getName()Ljava/lang/String;");
private static final MethodHandle v$getType = RefUtil.bootstrapVirtualMethod("Ljava/lang/reflect/RecordComponent;getType()Ljava/lang/Class;");
private static final MethodHandle v$getAnnotatedType = RefUtil.bootstrapVirtualMethod("Ljava/lang/reflect/RecordComponent;getAnnotatedType()Ljava/lang/reflect/AnnotatedType;");
private static final MethodHandle v$getDeclaringRecord = RefUtil.bootstrapVirtualMethod("Ljava/lang/reflect/RecordComponent;getDeclaringRecord()Ljava/lang/Class;");
private static final MethodHandle v$getAccessor = RefUtil.bootstrapVirtualMethod("Ljava/lang/reflect/RecordComponent;getAccessor()Ljava/lang/reflect/Method;");
private final Object instance;
private R$RecordComponent(Object instance) {
this.instance = instance;
}
public Object instance() {
return instance;
}
public static R$RecordComponent of(Object instance) {
return new R$RecordComponent(instance);
}
public String getName() {
if (v$getName == null) {
throw new UnsupportedOperationException("Method java.lang.reflect.RecordComponent#getName() not found");
}
try {
return (String) v$getName.invoke(instance);
} catch (Throwable e) {
throw RefUtil.sneakyThrow(e);
}
}
public Class<?> getType() {
if (v$getType == null) {
throw new UnsupportedOperationException("Method java.lang.reflect.RecordComponent#getType() not found");
}
try {
return (Class<?>) v$getType.invoke(instance);
} catch (Throwable e) {
throw RefUtil.sneakyThrow(e);
}
}
public AnnotatedType getAnnotatedType() {
if (v$getAnnotatedType == null) {
throw new UnsupportedOperationException("Method java.lang.reflect.RecordComponent#getAnnotatedType() not found");
}
try {
return (AnnotatedType) v$getAnnotatedType.invoke(instance);
} catch (Throwable e) {
throw RefUtil.sneakyThrow(e);
}
}
public Class<?> getDeclaringRecord() {
if (v$getDeclaringRecord == null) {
throw new UnsupportedOperationException("Method java.lang.reflect.RecordComponent#getDeclaringRecord() not found");
}
try {
return (Class<?>) v$getDeclaringRecord.invoke(instance);
} catch (Throwable e) {
throw RefUtil.sneakyThrow(e);
}
}
public Method getAccessor() {
if (v$getAccessor == null) {
throw new UnsupportedOperationException("Method java.lang.reflect.RecordComponent#getAccessor() not found");
}
try {
return (Method) v$getAccessor.invoke(instance);
} catch (Throwable e) {
throw RefUtil.sneakyThrow(e);
}
}
// delegate AnnotatedElement
@Override
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
return ((AnnotatedElement) instance).getAnnotation(annotationClass);
}
@Override
public Annotation[] getAnnotations() {
return ((AnnotatedElement) instance).getAnnotations();
}
@Override
public Annotation[] getDeclaredAnnotations() {
return ((AnnotatedElement) instance).getDeclaredAnnotations();
}
@Override
public String toString() {
return instance.toString();
}
@Override
public final boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof R$RecordComponent)) return false;
R$RecordComponent that = (R$RecordComponent) o;
return instance.equals(that.instance);
}
@Override
public int hashCode() {
return instance.hashCode();
}
}

@ -4,5 +4,5 @@ plugins {
} }
dependencies { dependencies {
compileOnly("io.papermc.paper:paper-api:1.20.2-R0.1-SNAPSHOT") compileOnly("com.destroystokyo.paper:paper-api:1.16.5-R0.1-SNAPSHOT")
} }

@ -22,8 +22,8 @@ public final class ConfigLib extends JavaPlugin {
return YamlConfigurationProperties return YamlConfigurationProperties
.newBuilder() .newBuilder()
.addSerializerByCondition( .addSerializerByCondition(
type -> type instanceof Class<?> cls && type -> type instanceof Class<?> &&
ConfigurationSerializable.class.isAssignableFrom(cls), ConfigurationSerializable.class.isAssignableFrom((Class<?>) type),
BukkitConfigurationSerializableSerializer.DEFAULT BukkitConfigurationSerializableSerializer.DEFAULT
) )
.build(); .build();

@ -3,6 +3,11 @@ plugins {
`plugins-config` `plugins-config`
} }
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
dependencies { dependencies {
compileOnly("com.velocitypowered:velocity-api:3.2.0-SNAPSHOT") compileOnly("com.velocitypowered:velocity-api:3.2.0-SNAPSHOT")
annotationProcessor("com.velocitypowered:velocity-api:3.2.0-SNAPSHOT") annotationProcessor("com.velocitypowered:velocity-api:3.2.0-SNAPSHOT")

@ -10,12 +10,14 @@ import org.snakeyaml.engine.v2.nodes.Node;
import org.snakeyaml.engine.v2.nodes.Tag; import org.snakeyaml.engine.v2.nodes.Tag;
import org.snakeyaml.engine.v2.representer.StandardRepresenter; import org.snakeyaml.engine.v2.representer.StandardRepresenter;
import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Map; import java.util.Map;
import java.util.Queue;
import static de.exlll.configlib.Validator.requireNonNull; import static de.exlll.configlib.Validator.requireNonNull;
@ -55,9 +57,9 @@ public final class YamlConfigurationStore<T> implements
public void write(T configuration, OutputStream outputStream) { public void write(T configuration, OutputStream outputStream) {
requireNonNull(configuration, "configuration"); requireNonNull(configuration, "configuration");
requireNonNull(outputStream, "output stream"); requireNonNull(outputStream, "output stream");
var extractedCommentNodes = extractor.extractCommentNodes(configuration); Queue<CommentNode> extractedCommentNodes = extractor.extractCommentNodes(configuration);
var yamlFileWriter = new YamlWriter(outputStream, properties); YamlWriter yamlFileWriter = new YamlWriter(outputStream, properties);
var dumpedYaml = tryDump(configuration); String dumpedYaml = tryDump(configuration);
yamlFileWriter.writeYaml(dumpedYaml, extractedCommentNodes); yamlFileWriter.writeYaml(dumpedYaml, extractedCommentNodes);
} }
@ -66,9 +68,9 @@ public final class YamlConfigurationStore<T> implements
requireNonNull(configuration, "configuration"); requireNonNull(configuration, "configuration");
requireNonNull(configurationFile, "configuration file"); requireNonNull(configurationFile, "configuration file");
tryCreateParentDirectories(configurationFile); tryCreateParentDirectories(configurationFile);
var extractedCommentNodes = extractor.extractCommentNodes(configuration); Queue<CommentNode> extractedCommentNodes = extractor.extractCommentNodes(configuration);
var yamlFileWriter = new YamlWriter(configurationFile, properties); YamlWriter yamlFileWriter = new YamlWriter(configurationFile, properties);
var dumpedYaml = tryDump(configuration); String dumpedYaml = tryDump(configuration);
yamlFileWriter.writeYaml(dumpedYaml, extractedCommentNodes); yamlFileWriter.writeYaml(dumpedYaml, extractedCommentNodes);
} }
@ -98,8 +100,8 @@ public final class YamlConfigurationStore<T> implements
public T read(InputStream inputStream) { public T read(InputStream inputStream) {
requireNonNull(inputStream, "input stream"); requireNonNull(inputStream, "input stream");
try { try {
var yaml = YAML_LOADER.loadFromInputStream(inputStream); Object yaml = YAML_LOADER.loadFromInputStream(inputStream);
var conf = requireYamlMapForRead(yaml); Map<?, ?> conf = requireYamlMapForRead(yaml);
return serializer.deserialize(conf); return serializer.deserialize(conf);
} catch (YamlEngineException e) { } catch (YamlEngineException e) {
String msg = "The input stream does not contain valid YAML."; String msg = "The input stream does not contain valid YAML.";
@ -113,26 +115,25 @@ public final class YamlConfigurationStore<T> implements
throw new ConfigurationException(msg); throw new ConfigurationException(msg);
} }
if (!(yaml instanceof Map<?, ?> map)) { if (!(yaml instanceof Map<?, ?>)) {
String msg = "The contents of the input stream do not represent a configuration. " + String msg = "The contents of the input stream do not represent a configuration. " +
"A valid configuration contains a YAML map but instead a " + "A valid configuration contains a YAML map but instead a " +
"'" + yaml.getClass() + "' was found."; "'" + yaml.getClass() + "' was found.";
throw new ConfigurationException(msg); throw new ConfigurationException(msg);
} }
return map; return (Map<?, ?>) yaml;
} }
@Override @Override
public T load(Path configurationFile) { public T load(Path configurationFile) {
requireNonNull(configurationFile, "configuration file"); requireNonNull(configurationFile, "configuration file");
try (var reader = Files.newBufferedReader(configurationFile, properties.getCharset())) { try (BufferedReader reader = Files.newBufferedReader(configurationFile, properties.getCharset())) {
var yaml = YAML_LOADER.loadFromReader(reader); Object yaml = YAML_LOADER.loadFromReader(reader);
var conf = requireYamlMapForLoad(yaml, configurationFile); Map<?, ?> conf = requireYamlMapForLoad(yaml, configurationFile);
return serializer.deserialize(conf); return serializer.deserialize(conf);
} catch (YamlEngineException e) { } catch (YamlEngineException e) {
String msg = "The configuration file at %s does not contain valid YAML."; throw new ConfigurationException("The configuration file at " + configurationFile + " does not contain valid YAML.", e);
throw new ConfigurationException(msg.formatted(configurationFile), e);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@ -140,18 +141,17 @@ public final class YamlConfigurationStore<T> implements
private Map<?, ?> requireYamlMapForLoad(Object yaml, Path configurationFile) { private Map<?, ?> requireYamlMapForLoad(Object yaml, Path configurationFile) {
if (yaml == null) { if (yaml == null) {
String msg = "The configuration file at %s is empty or only contains null."; throw new ConfigurationException("The configuration file at " + configurationFile + " is empty or only contains null.");
throw new ConfigurationException(msg.formatted(configurationFile));
} }
if (!(yaml instanceof Map<?, ?> map)) { if (!(yaml instanceof Map<?, ?>)) {
String msg = "The contents of the YAML file at %s do not represent a configuration. " + throw new ConfigurationException(
"The contents of the YAML file at " + configurationFile + " do not represent a configuration. " +
"A valid configuration file contains a YAML map but instead a " + "A valid configuration file contains a YAML map but instead a " +
"'" + yaml.getClass() + "' was found."; "'" + yaml.getClass() + "' was found.");
throw new ConfigurationException(msg.formatted(configurationFile));
} }
return map; return (Map<?, ?>) yaml;
} }
@Override @Override

@ -27,7 +27,7 @@ public final class YamlConfigurations {
* @see YamlConfigurationStore#load(Path) * @see YamlConfigurationStore#load(Path)
*/ */
public static <T> T load(Path configurationFile, Class<T> configurationType) { public static <T> T load(Path configurationFile, Class<T> configurationType) {
final var properties = YamlConfigurationProperties.newBuilder().build(); final YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder().build();
return load(configurationFile, configurationType, properties); return load(configurationFile, configurationType, properties);
} }
@ -52,7 +52,7 @@ public final class YamlConfigurations {
Class<T> configurationType, Class<T> configurationType,
Consumer<YamlConfigurationProperties.Builder<?>> propertiesConfigurer Consumer<YamlConfigurationProperties.Builder<?>> propertiesConfigurer
) { ) {
final var builder = YamlConfigurationProperties.newBuilder(); final YamlConfigurationProperties.Builder<?> builder = YamlConfigurationProperties.newBuilder();
propertiesConfigurer.accept(builder); propertiesConfigurer.accept(builder);
return load(configurationFile, configurationType, builder.build()); return load(configurationFile, configurationType, builder.build());
} }
@ -77,7 +77,7 @@ public final class YamlConfigurations {
Class<T> configurationType, Class<T> configurationType,
YamlConfigurationProperties properties YamlConfigurationProperties properties
) { ) {
final var store = new YamlConfigurationStore<>(configurationType, properties); final YamlConfigurationStore<T> store = new YamlConfigurationStore<>(configurationType, properties);
return store.load(configurationFile); return store.load(configurationFile);
} }
@ -95,7 +95,7 @@ public final class YamlConfigurations {
* @see YamlConfigurationStore#read(InputStream) * @see YamlConfigurationStore#read(InputStream)
*/ */
public static <T> T read(InputStream inputStream, Class<T> configurationType) { public static <T> T read(InputStream inputStream, Class<T> configurationType) {
final var properties = YamlConfigurationProperties.newBuilder().build(); final YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder().build();
return read(inputStream, configurationType, properties); return read(inputStream, configurationType, properties);
} }
@ -119,7 +119,7 @@ public final class YamlConfigurations {
Class<T> configurationType, Class<T> configurationType,
Consumer<YamlConfigurationProperties.Builder<?>> propertiesConfigurer Consumer<YamlConfigurationProperties.Builder<?>> propertiesConfigurer
) { ) {
final var builder = YamlConfigurationProperties.newBuilder(); final YamlConfigurationProperties.Builder<?> builder = YamlConfigurationProperties.newBuilder();
propertiesConfigurer.accept(builder); propertiesConfigurer.accept(builder);
return read(inputStream, configurationType, builder.build()); return read(inputStream, configurationType, builder.build());
} }
@ -143,7 +143,7 @@ public final class YamlConfigurations {
Class<T> configurationType, Class<T> configurationType,
YamlConfigurationProperties properties YamlConfigurationProperties properties
) { ) {
final var store = new YamlConfigurationStore<>(configurationType, properties); final YamlConfigurationStore<T> store = new YamlConfigurationStore<>(configurationType, properties);
return store.read(inputStream); return store.read(inputStream);
} }
@ -163,7 +163,7 @@ public final class YamlConfigurations {
* @see YamlConfigurationStore#update(Path) * @see YamlConfigurationStore#update(Path)
*/ */
public static <T> T update(Path configurationFile, Class<T> configurationType) { public static <T> T update(Path configurationFile, Class<T> configurationType) {
final var properties = YamlConfigurationProperties.newBuilder().build(); final YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder().build();
return update(configurationFile, configurationType, properties); return update(configurationFile, configurationType, properties);
} }
@ -189,7 +189,7 @@ public final class YamlConfigurations {
Class<T> configurationType, Class<T> configurationType,
Consumer<YamlConfigurationProperties.Builder<?>> propertiesConfigurer Consumer<YamlConfigurationProperties.Builder<?>> propertiesConfigurer
) { ) {
final var builder = YamlConfigurationProperties.newBuilder(); final YamlConfigurationProperties.Builder<?> builder = YamlConfigurationProperties.newBuilder();
propertiesConfigurer.accept(builder); propertiesConfigurer.accept(builder);
return update(configurationFile, configurationType, builder.build()); return update(configurationFile, configurationType, builder.build());
} }
@ -215,7 +215,7 @@ public final class YamlConfigurations {
Class<T> configurationType, Class<T> configurationType,
YamlConfigurationProperties properties YamlConfigurationProperties properties
) { ) {
final var store = new YamlConfigurationStore<>(configurationType, properties); final YamlConfigurationStore<T> store = new YamlConfigurationStore<>(configurationType, properties);
return store.update(configurationFile); return store.update(configurationFile);
} }
@ -238,7 +238,7 @@ public final class YamlConfigurations {
Class<T> configurationType, Class<T> configurationType,
T configuration T configuration
) { ) {
final var properties = YamlConfigurationProperties.newBuilder().build(); final YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder().build();
save(configurationFile, configurationType, configuration, properties); save(configurationFile, configurationType, configuration, properties);
} }
@ -264,7 +264,7 @@ public final class YamlConfigurations {
T configuration, T configuration,
Consumer<YamlConfigurationProperties.Builder<?>> propertiesConfigurer Consumer<YamlConfigurationProperties.Builder<?>> propertiesConfigurer
) { ) {
final var builder = YamlConfigurationProperties.newBuilder(); final YamlConfigurationProperties.Builder<?> builder = YamlConfigurationProperties.newBuilder();
propertiesConfigurer.accept(builder); propertiesConfigurer.accept(builder);
save(configurationFile, configurationType, configuration, builder.build()); save(configurationFile, configurationType, configuration, builder.build());
} }
@ -290,7 +290,7 @@ public final class YamlConfigurations {
T configuration, T configuration,
YamlConfigurationProperties properties YamlConfigurationProperties properties
) { ) {
final var store = new YamlConfigurationStore<>(configurationType, properties); final YamlConfigurationStore<T> store = new YamlConfigurationStore<>(configurationType, properties);
store.save(configuration, configurationFile); store.save(configuration, configurationFile);
} }
@ -313,7 +313,7 @@ public final class YamlConfigurations {
Class<T> configurationType, Class<T> configurationType,
T configuration T configuration
) { ) {
final var properties = YamlConfigurationProperties.newBuilder().build(); final YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder().build();
write(outputStream, configurationType, configuration, properties); write(outputStream, configurationType, configuration, properties);
} }
@ -339,7 +339,7 @@ public final class YamlConfigurations {
T configuration, T configuration,
Consumer<YamlConfigurationProperties.Builder<?>> propertiesConfigurer Consumer<YamlConfigurationProperties.Builder<?>> propertiesConfigurer
) { ) {
final var builder = YamlConfigurationProperties.newBuilder(); final YamlConfigurationProperties.Builder<?> builder = YamlConfigurationProperties.newBuilder();
propertiesConfigurer.accept(builder); propertiesConfigurer.accept(builder);
write(outputStream, configurationType, configuration, builder.build()); write(outputStream, configurationType, configuration, builder.build());
} }
@ -365,7 +365,7 @@ public final class YamlConfigurations {
T configuration, T configuration,
YamlConfigurationProperties properties YamlConfigurationProperties properties
) { ) {
final var store = new YamlConfigurationStore<>(configurationType, properties); final YamlConfigurationStore<T> store = new YamlConfigurationStore<>(configurationType, properties);
store.write(configuration, outputStream); store.write(configuration, outputStream);
} }
} }

@ -107,8 +107,8 @@ final class YamlWriter {
* of a child. That order ultimately represents the order in which the * of a child. That order ultimately represents the order in which the
* YAML file is structured. * YAML file is structured.
*/ */
var node = nodes.poll(); CommentNode node = nodes.poll();
var currentIndentLevel = 0; int currentIndentLevel = 0;
for (final String line : yaml.split("\n")) { for (final String line : yaml.split("\n")) {
if (node == null) { if (node == null) {
@ -116,16 +116,16 @@ final class YamlWriter {
continue; continue;
} }
final var elementNames = node.elementNames(); final List<String> elementNames = node.elementNames();
final var indent = " ".repeat(currentIndentLevel); final String indent = " ".repeat(currentIndentLevel);
final var lineStart = indent + elementNames.get(currentIndentLevel) + ":"; final String lineStart = indent + elementNames.get(currentIndentLevel) + ":";
if (!line.startsWith(lineStart)) { if (!line.startsWith(lineStart)) {
writeLine(line); writeLine(line);
continue; continue;
} }
final var commentIndentLevel = elementNames.size() - 1; final int commentIndentLevel = elementNames.size() - 1;
if (currentIndentLevel++ == commentIndentLevel) { if (currentIndentLevel++ == commentIndentLevel) {
writeComments(node.comments(), commentIndentLevel); writeComments(node.comments(), commentIndentLevel);
if ((node = nodes.poll()) != null) { if ((node = nodes.poll()) != null) {

Loading…
Cancel
Save