Initial commit

That's a lotta stuff for an initial commit, but well...
This commit is contained in:
2024-05-25 19:22:26 +02:00
commit b0f35b03b9
99 changed files with 6476 additions and 0 deletions

39
.gitignore vendored Normal file
View File

@@ -0,0 +1,39 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### IntelliJ IDEA ###
.idea/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

37
build.gradle.kts Normal file
View File

@@ -0,0 +1,37 @@
plugins {
id("java")
}
group = "de.siphalor"
version = "1.0-SNAPSHOT"
allprojects {
apply(plugin = "java")
apply(plugin = "java-library")
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
repositories {
mavenCentral()
}
dependencies {
val lombok = "org.projectlombok:lombok:${properties["lombok.version"]}"
compileOnly(lombok)
annotationProcessor(lombok)
testCompileOnly(lombok)
testAnnotationProcessor(lombok)
implementation("org.jetbrains:annotations:${properties["jetbrains_annotations.version"]}")
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
}
tasks.test {
useJUnitPlatform()
}
}

3
gradle.properties Normal file
View File

@@ -0,0 +1,3 @@
asm.version = 9.7
jetbrains_annotations.version = 24.1.0
lombok.version = 1.18.32

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#Sun Apr 14 17:59:04 CEST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
gradlew vendored Normal file
View File

@@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

1
lombok.config Normal file
View File

@@ -0,0 +1 @@
lombok.accessors.fluent = true

8
settings.gradle.kts Normal file
View File

@@ -0,0 +1,8 @@
rootProject.name = "tweed5"
include("tweed5-core")
include("tweed5-default-extensions")
include("tweed5-patchwork")
include("tweed5-serde-api")
include("tweed5-serde-extension")
include("tweed5-serde-hjson")

View File

@@ -0,0 +1,3 @@
dependencies {
api(project(":tweed5-patchwork"))
}

View File

View File

@@ -0,0 +1,31 @@
package de.siphalor.tweed5.core.api.container;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import java.util.Collection;
import java.util.Map;
public interface ConfigContainer<T> {
ConfigContainerSetupPhase setupPhase();
default boolean isReady() {
return setupPhase() == ConfigContainerSetupPhase.READY;
}
void registerExtension(TweedExtension extension);
void finishExtensionSetup();
void attachAndSealTree(ConfigEntry<T> rootEntry);
EntryExtensionsData createExtensionsData();
void initialize();
ConfigEntry<T> rootEntry();
Collection<TweedExtension> extensions();
Map<Class<?>, ? extends RegisteredExtensionData<EntryExtensionsData, ?>> entryDataExtensions();
}

View File

@@ -0,0 +1,9 @@
package de.siphalor.tweed5.core.api.container;
public enum ConfigContainerSetupPhase {
EXTENSIONS_SETUP,
TREE_SETUP,
SEALING_TREE,
TREE_SEALED,
READY,
}

View File

@@ -0,0 +1,9 @@
package de.siphalor.tweed5.core.api.entry;
import java.util.Collection;
public interface CoherentCollectionConfigEntry<E, T extends Collection<E>> extends ConfigEntry<T> {
ConfigEntry<E> elementEntry();
T instantiateCollection(int size);
}

View File

@@ -0,0 +1,12 @@
package de.siphalor.tweed5.core.api.entry;
import java.util.Map;
public interface CompoundConfigEntry<T> extends ConfigEntry<T> {
Map<String, ConfigEntry<?>> subEntries();
<V> void set(T compoundValue, String key, V value);
<V> V get(T compoundValue, String key);
T instantiateCompoundValue();
}

View File

@@ -0,0 +1,17 @@
package de.siphalor.tweed5.core.api.entry;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.validation.ConfigEntryValueValidationException;
public interface ConfigEntry<T> {
Class<T> valueClass();
void validate(T value) throws ConfigEntryValueValidationException;
void seal(ConfigContainer<?> container);
boolean sealed();
EntryExtensionsData extensionsData();
void visitInOrder(ConfigEntryVisitor visitor);
}

View File

@@ -0,0 +1,28 @@
package de.siphalor.tweed5.core.api.entry;
public interface ConfigEntryVisitor {
void visitEntry(ConfigEntry<?> entry);
default boolean enterCollectionEntry(ConfigEntry<?> entry) {
visitEntry(entry);
return true;
}
default void leaveCollectionEntry(ConfigEntry<?> entry) {
}
default boolean enterCompoundEntry(ConfigEntry<?> entry) {
visitEntry(entry);
return true;
}
default boolean enterCompoundSubEntry(String key) {
return true;
}
default void leaveCompoundSubEntry(String key) {
}
default void leaveCompoundEntry(ConfigEntry<?> entry) {
}
}

View File

@@ -0,0 +1,4 @@
package de.siphalor.tweed5.core.api.entry;
public interface SimpleConfigEntry<T> extends ConfigEntry<T> {
}

View File

@@ -0,0 +1,6 @@
package de.siphalor.tweed5.core.api.extension;
import de.siphalor.tweed5.patchwork.api.Patchwork;
public interface EntryExtensionsData extends Patchwork<EntryExtensionsData> {
}

View File

@@ -0,0 +1,7 @@
package de.siphalor.tweed5.core.api.extension;
import de.siphalor.tweed5.patchwork.api.Patchwork;
public interface RegisteredExtensionData<U extends Patchwork<U>, E> {
void set(U patchwork, E extension);
}

View File

@@ -0,0 +1,13 @@
package de.siphalor.tweed5.core.api.extension;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
public interface TweedExtension {
String getId();
default void setup(TweedExtensionSetupContext context) {
}
default void initEntry(ConfigEntry<?> configEntry) {
}
}

View File

@@ -0,0 +1,8 @@
package de.siphalor.tweed5.core.api.extension;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
public interface TweedExtensionSetupContext {
ConfigContainer<?> configContainer();
<E> RegisteredExtensionData<EntryExtensionsData, E> registerEntryExtensionData(Class<E> dataClass);
}

View File

@@ -0,0 +1,96 @@
package de.siphalor.tweed5.core.api.middleware;
import de.siphalor.tweed5.core.api.sort.AcyclicGraphSorter;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class DefaultMiddlewareContainer<M> implements MiddlewareContainer<M> {
private static final String CONTAINER_ID = "";
private List<Middleware<M>> middlewares = new ArrayList<>();
private final Set<String> middlewareIds = new HashSet<>();
private boolean sealed = false;
@Override
public String id() {
return CONTAINER_ID;
}
@Override
public void register(Middleware<M> middleware) {
if (sealed) {
throw new IllegalStateException("Middleware container has already been sealed");
}
if (middleware.id().isEmpty()) {
throw new IllegalArgumentException("Middleware id cannot be empty");
}
if (middlewareIds.contains(middleware.id())) {
throw new IllegalArgumentException("Middleware id already registered: " + middleware.id());
}
middlewares.add(middleware);
middlewareIds.add(middleware.id());
}
@Override
public void seal() {
if (sealed) {
return;
}
sealed = true;
String[] allMentionedMiddlewareIds = middlewares.stream()
.flatMap(middleware -> Stream.concat(
Stream.of(middleware.id()),
Stream.concat(middleware.mustComeAfter().stream(), middleware.mustComeBefore().stream())
)).distinct().toArray(String[]::new);
Map<String, Integer> indecesByMiddlewareId = new HashMap<>();
for (int i = 0; i < allMentionedMiddlewareIds.length; i++) {
indecesByMiddlewareId.put(allMentionedMiddlewareIds[i], i);
}
AcyclicGraphSorter sorter = new AcyclicGraphSorter(allMentionedMiddlewareIds.length);
for (Middleware<M> middleware : middlewares) {
Integer currentIndex = indecesByMiddlewareId.get(middleware.id());
middleware.mustComeAfter().stream()
.map(indecesByMiddlewareId::get)
.forEach(beforeIndex -> sorter.addEdge(beforeIndex, currentIndex));
middleware.mustComeBefore().stream()
.map(indecesByMiddlewareId::get)
.forEach(afterIndex -> sorter.addEdge(currentIndex, afterIndex));
}
Map<String, Middleware<M>> middlewaresById = middlewares.stream().collect(Collectors.toMap(Middleware::id, Function.identity()));
try {
int[] sortedIndeces = sorter.sort();
middlewares = Arrays.stream(sortedIndeces)
.mapToObj(index -> allMentionedMiddlewareIds[index])
.map(middlewaresById::get)
.filter(Objects::nonNull)
.collect(Collectors.toList());
} catch (AcyclicGraphSorter.GraphCycleException e) {
throw new IllegalStateException(e);
}
}
@Override
public M process(M inner) {
if (!sealed) {
throw new IllegalStateException("Middleware container has not been sealed");
}
M combined = inner;
for (Middleware<M> middleware : middlewares) {
combined = middleware.process(combined);
}
return combined;
}
}

View File

@@ -0,0 +1,17 @@
package de.siphalor.tweed5.core.api.middleware;
import java.util.Collections;
import java.util.Set;
public interface Middleware<M> {
String id();
default Set<String> mustComeBefore() {
return Collections.emptySet();
}
default Set<String> mustComeAfter() {
return Collections.emptySet();
}
M process(M inner);
}

View File

@@ -0,0 +1,6 @@
package de.siphalor.tweed5.core.api.middleware;
public interface MiddlewareContainer<M> extends Middleware<M> {
void register(Middleware<M> middleware);
void seal();
}

View File

@@ -0,0 +1,287 @@
package de.siphalor.tweed5.core.api.sort;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import java.math.BigDecimal;
import java.util.*;
public class AcyclicGraphSorter {
private final int nodeCount;
private final int wordCount;
private final BitSet[] outgoingEdges;
private final BitSet[] incomingEdges;
public AcyclicGraphSorter(int nodeCount) {
this.nodeCount = nodeCount;
BigDecimal[] div = BigDecimal.valueOf(nodeCount)
.divideAndRemainder(BigDecimal.valueOf(BitSet.WORD_SIZE));
this.wordCount = div[0].intValue() + (BigDecimal.ZERO.equals(div[1]) ? 0 : 1);
outgoingEdges = new BitSet[nodeCount];
incomingEdges = new BitSet[nodeCount];
for (int i = 0; i < nodeCount; i++) {
outgoingEdges[i] = BitSet.empty(nodeCount, wordCount);
incomingEdges[i] = BitSet.empty(nodeCount, wordCount);
}
}
public void addEdge(int from, int to) {
checkBounds(from);
checkBounds(to);
if (from == to) {
throw new IllegalArgumentException("Edge from and to cannot be the same");
}
outgoingEdges[from].set(to);
incomingEdges[to].set(from);
}
private void checkBounds(int index) {
if (index < 0 || index >= nodeCount) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + nodeCount);
}
}
public int[] sort() throws GraphCycleException {
BitSet visited = BitSet.ready(nodeCount, wordCount);
BitSet.Iterator visitedIter = visited.iterator();
int lastVisited = -1;
int[] sortedIndeces = new int[nodeCount];
int nextSortedIndex = 0;
while (nextSortedIndex < sortedIndeces.length) {
if (!visitedIter.next()) {
BitSet incomingEdge = incomingEdges[visitedIter.index()];
if (incomingEdge.isEmptyAfterAndNot(visited)) {
sortedIndeces[nextSortedIndex] = visitedIter.index();
visitedIter.set();
lastVisited = visitedIter.index();
nextSortedIndex++;
}
} else if (visitedIter.index() == lastVisited) {
break;
}
if (!visitedIter.hasNext()) {
visitedIter.restart();
}
}
if (nextSortedIndex < sortedIndeces.length) {
findCycleAndThrow(visited);
}
return sortedIndeces;
}
private void findCycleAndThrow(BitSet visited) throws GraphCycleException {
Deque<Integer> stack = new LinkedList<>();
BitSet.Iterator visitedIter = visited.iterator();
while (visitedIter.next()) {
if (!visitedIter.hasNext()) {
throw new IllegalStateException("Unable to find unvisited node in cycle detection");
}
}
stack.push(visitedIter.index());
outer:
//noinspection InfiniteLoopStatement
while (true) {
BitSet leftoverOutgoing = outgoingEdges[stack.getFirst()].andNot(visited);
BitSet.Iterator outgoingIter = leftoverOutgoing.iterator();
while (outgoingIter.hasNext()) {
if (outgoingIter.next()) {
if (stack.contains(outgoingIter.index())) {
throw new GraphCycleException(stack.reversed());
}
stack.push(outgoingIter.index());
continue outer;
}
}
visited.set(stack.pop());
}
}
@Getter
@ToString
public static class GraphCycleException extends Exception {
private final Collection<Integer> cycleIndeces;
public GraphCycleException(Collection<Integer> cycleIndeces) {
super("Detected illegal cycle in directed graph");
this.cycleIndeces = cycleIndeces;
}
}
@AllArgsConstructor(access = AccessLevel.PRIVATE)
private static class BitSet {
private static final int WORD_SIZE = Long.SIZE;
private final int bitCount;
private final int wordCount;
private long[] words;
static BitSet ready(int bitCount, int wordCount) {
return new BitSet(bitCount, wordCount, new long[wordCount]);
}
static BitSet empty(int bitCount, int wordCount) {
return new BitSet(bitCount, wordCount, null);
}
private void set(int index) {
cloneOnWrite();
int wordIndex = index / WORD_SIZE;
int innerIndex = index % WORD_SIZE;
words[wordIndex] |= 1L << innerIndex;
}
private void cloneOnWrite() {
if (words == null) {
words = new long[wordCount];
}
}
public boolean isEmpty() {
if (words == null) {
return true;
}
for (long word : words) {
if (word != 0L) {
return false;
}
}
return true;
}
public BitSet andNot(BitSet mask) {
if (words == null) {
return BitSet.empty(bitCount, wordCount);
}
BitSet result = BitSet.ready(bitCount, wordCount);
for (int i = 0; i < words.length; i++) {
result.words[i] = words[i] & ~mask.words[i];
}
return result;
}
public boolean isEmptyAfterAndNot(BitSet mask) {
if (words == null) {
return true;
}
for (int i = 0; i < words.length; i++) {
long maskWord = mask.words[i];
long word = words[i];
if ((word & ~maskWord) != 0) {
return false;
}
}
return true;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BitSet)) return false;
BitSet bitSet = (BitSet) o;
if (this.words == null || bitSet.words == null) {
return this.isEmpty() && bitSet.isEmpty();
}
return Objects.deepEquals(words, bitSet.words);
}
@Override
public int hashCode() {
if (isEmpty()) {
return 0;
}
return Arrays.hashCode(words);
}
@Override
public String toString() {
if (wordCount == 0) {
return "";
}
StringBuilder sb = new StringBuilder(wordCount * 9);
int leftBitCount = bitCount;
if (words == null) {
for (int i = 0; i < wordCount; i++) {
sb.repeat("0", Math.min(WORD_SIZE, leftBitCount));
sb.append(" ");
leftBitCount -= WORD_SIZE;
}
} else {
for (long word : words) {
int wordEnd = Math.min(WORD_SIZE, leftBitCount);
for (int j = 0; j < wordEnd; j++) {
sb.append((word & 1) == 1 ? "1" : "0");
word >>>= 1;
}
sb.append(" ");
leftBitCount -= WORD_SIZE;
}
}
return sb.substring(0, sb.length() - 1);
}
public Iterator iterator() {
return new Iterator();
}
public class Iterator {
private int wordIndex = 0;
private int innerIndex = -1;
@Getter
private int index = -1;
public void restart() {
wordIndex = 0;
innerIndex = -1;
index = -1;
}
public boolean hasNext() {
return index < bitCount - 1;
}
public boolean next() {
innerIndex++;
if (innerIndex == WORD_SIZE) {
innerIndex = 0;
wordIndex++;
}
index++;
if (words == null) {
return false;
}
return (words[wordIndex] & (1L << innerIndex)) != 0L;
}
public void set() {
cloneOnWrite();
words[wordIndex] |= (1L << innerIndex);
}
}
}
}

View File

@@ -0,0 +1,8 @@
package de.siphalor.tweed5.core.api.validation;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.middleware.Middleware;
public interface ConfigEntryValidationExtension {
Middleware<ConfigEntryValidationMiddleware> validationMiddleware(ConfigEntry<?> configEntry);
}

View File

@@ -0,0 +1,8 @@
package de.siphalor.tweed5.core.api.validation;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
@FunctionalInterface
public interface ConfigEntryValidationMiddleware {
<T> void validate(ConfigEntry<T> configEntry, T value) throws ConfigEntryValueValidationException;
}

View File

@@ -0,0 +1,18 @@
package de.siphalor.tweed5.core.api.validation;
public class ConfigEntryValueValidationException extends Exception {
public ConfigEntryValueValidationException() {
}
public ConfigEntryValueValidationException(String message) {
super(message);
}
public ConfigEntryValueValidationException(String message, Throwable cause) {
super(message, cause);
}
public ConfigEntryValueValidationException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1 @@
package de.siphalor.tweed5.core.generated;

View File

@@ -0,0 +1,152 @@
package de.siphalor.tweed5.core.impl;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.container.ConfigContainerSetupPhase;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.*;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkClassCreator;
import de.siphalor.tweed5.patchwork.impl.PatchworkClass;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassGenerator;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassPart;
import lombok.Getter;
import lombok.Setter;
import java.lang.invoke.MethodHandle;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class DefaultConfigContainer<T> implements ConfigContainer<T> {
@Getter
private ConfigContainerSetupPhase setupPhase = ConfigContainerSetupPhase.EXTENSIONS_SETUP;
private final HashMap<Class<? extends TweedExtension>, TweedExtension> extensions = new HashMap<>();
private ConfigEntry<T> rootEntry;
private PatchworkClass<EntryExtensionsData> entryExtensionsDataPatchworkClass;
private Map<Class<?>, RegisteredExtensionDataImpl<EntryExtensionsData, ?>> registeredEntryDataExtensions;
@Override
public Collection<TweedExtension> extensions() {
return extensions.values();
}
@Override
public void registerExtension(TweedExtension extension) {
requireSetupPhase(ConfigContainerSetupPhase.EXTENSIONS_SETUP);
extensions.put(extension.getClass(), extension);
}
@Override
public void finishExtensionSetup() {
requireSetupPhase(ConfigContainerSetupPhase.EXTENSIONS_SETUP);
registeredEntryDataExtensions = new HashMap<>();
TweedExtensionSetupContext extensionSetupContext = new TweedExtensionSetupContext() {
@Override
public ConfigContainer<T> configContainer() {
return DefaultConfigContainer.this;
}
@Override
public <E> RegisteredExtensionData<EntryExtensionsData, E> registerEntryExtensionData(Class<E> dataClass) {
if (registeredEntryDataExtensions.containsKey(dataClass)) {
throw new IllegalArgumentException("Extension " + dataClass.getName() + " is already registered");
}
RegisteredExtensionDataImpl<EntryExtensionsData, E> registered = new RegisteredExtensionDataImpl<>();
registeredEntryDataExtensions.put(dataClass, registered);
return registered;
}
};
for (TweedExtension extension : extensions.values()) {
extension.setup(extensionSetupContext);
}
PatchworkClassCreator<EntryExtensionsData> entryExtensionsDataGenerator = PatchworkClassCreator.<EntryExtensionsData>builder()
.patchworkInterface(EntryExtensionsData.class)
.classPackage("de.siphalor.tweed5.core.generated.entryextensiondata")
.classPrefix("EntryExtensionsData$")
.build();
try {
entryExtensionsDataPatchworkClass = entryExtensionsDataGenerator.createClass(registeredEntryDataExtensions.keySet());
for (PatchworkClassPart part : entryExtensionsDataPatchworkClass.parts()) {
RegisteredExtensionDataImpl<EntryExtensionsData, ?> registeredExtension = registeredEntryDataExtensions.get(part.partInterface());
registeredExtension.setter(part.fieldSetter());
}
} catch (PatchworkClassGenerator.GenerationException e) {
throw new IllegalStateException("Failed to create patchwork class for entry extensions' data", e);
}
setupPhase = ConfigContainerSetupPhase.TREE_SETUP;
}
@Override
public void attachAndSealTree(ConfigEntry<T> rootEntry) {
requireSetupPhase(ConfigContainerSetupPhase.TREE_SETUP);
this.rootEntry = rootEntry;
finishEntrySetup();
}
private void finishEntrySetup() {
setupPhase = ConfigContainerSetupPhase.SEALING_TREE;
rootEntry.visitInOrder(entry -> entry.seal(DefaultConfigContainer.this));
setupPhase = ConfigContainerSetupPhase.TREE_SEALED;
}
@Override
public EntryExtensionsData createExtensionsData() {
requireSetupPhase(ConfigContainerSetupPhase.SEALING_TREE);
try {
return (EntryExtensionsData) entryExtensionsDataPatchworkClass.constructor().invoke();
} catch (Throwable e) {
throw new IllegalStateException("Failed to construct patchwork class for entry extensions' data", e);
}
}
@Override
public Map<Class<?>, ? extends RegisteredExtensionData<EntryExtensionsData, ?>> entryDataExtensions() {
requireSetupPhase(ConfigContainerSetupPhase.TREE_SEALED);
return registeredEntryDataExtensions;
}
@Override
public void initialize() {
requireSetupPhase(ConfigContainerSetupPhase.TREE_SEALED);
rootEntry.visitInOrder(entry -> {
for (TweedExtension extension : extensions()) {
extension.initEntry(entry);
}
});
setupPhase = ConfigContainerSetupPhase.READY;
}
@Override
public ConfigEntry<T> rootEntry() {
return rootEntry;
}
private void requireSetupPhase(ConfigContainerSetupPhase required) {
if (setupPhase != required) {
throw new IllegalStateException("Config container is not in correct stage, expected " + required + ", but is in " + setupPhase);
}
}
@Setter
private static class RegisteredExtensionDataImpl<U extends Patchwork<U>, E> implements RegisteredExtensionData<U, E> {
private MethodHandle setter;
@Override
public void set(U patchwork, E extension) {
try {
setter.invokeWithArguments(patchwork, extension);
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}
}
}

View File

@@ -0,0 +1,76 @@
package de.siphalor.tweed5.core.impl.entry;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.core.api.middleware.DefaultMiddlewareContainer;
import de.siphalor.tweed5.core.api.middleware.MiddlewareContainer;
import de.siphalor.tweed5.core.api.validation.ConfigEntryValidationExtension;
import de.siphalor.tweed5.core.api.validation.ConfigEntryValidationMiddleware;
import de.siphalor.tweed5.core.api.validation.ConfigEntryValueValidationException;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
@RequiredArgsConstructor
@Getter
abstract class BaseConfigEntryImpl<T> implements ConfigEntry<T> {
private static final ConfigEntryValidationMiddleware ROOT_VALIDATION = new ConfigEntryValidationMiddleware() {
@Override
public <U> void validate(ConfigEntry<U> configEntry, U value) {}
};
@NotNull
private final Class<T> valueClass;
private ConfigContainer<?> container;
private EntryExtensionsData extensionsData;
private boolean sealed;
private ConfigEntryValidationMiddleware validationMiddleware;
@Override
public void seal(ConfigContainer<?> container) {
requireUnsealed();
this.container = container;
this.extensionsData = container.createExtensionsData();
MiddlewareContainer<ConfigEntryValidationMiddleware> validationMiddlewareContainer = new DefaultMiddlewareContainer<>();
for (TweedExtension extension : container().extensions()) {
if (extension instanceof ConfigEntryValidationExtension) {
validationMiddlewareContainer.register(((ConfigEntryValidationExtension) extension).validationMiddleware(this));
}
}
validationMiddlewareContainer.seal();
validationMiddleware = validationMiddlewareContainer.process(ROOT_VALIDATION);
sealed = true;
}
protected void requireUnsealed() {
if (sealed) {
throw new IllegalStateException("Config entry is already sealed!");
}
}
@Override
public void validate(T value) throws ConfigEntryValueValidationException {
if (value == null) {
if (valueClass.isPrimitive()) {
throw new ConfigEntryValueValidationException("Value must not be null");
}
} else if (!valueClass.isAssignableFrom(value.getClass())) {
throw new ConfigEntryValueValidationException("Value must be of type " + valueClass.getName());
}
validationMiddleware.validate(this, value);
}
@Override
public void visitInOrder(ConfigEntryVisitor visitor) {
visitor.visitEntry(this);
}
}

View File

@@ -0,0 +1,42 @@
package de.siphalor.tweed5.core.impl.entry;
import de.siphalor.tweed5.core.api.entry.CoherentCollectionConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor;
import java.util.Collection;
import java.util.function.IntFunction;
public class CoherentCollectionConfigEntryImpl<E, T extends Collection<E>> extends BaseConfigEntryImpl<T> implements CoherentCollectionConfigEntry<E, T> {
private final IntFunction<T> collectionConstructor;
private ConfigEntry<E> elementEntry;
public CoherentCollectionConfigEntryImpl(Class<T> valueClass, IntFunction<T> collectionConstructor) {
super(valueClass);
this.collectionConstructor = collectionConstructor;
}
public void elementEntry(ConfigEntry<E> elementEntry) {
requireUnsealed();
this.elementEntry = elementEntry;
}
@Override
public ConfigEntry<E> elementEntry() {
return elementEntry;
}
@Override
public T instantiateCollection(int size) {
return collectionConstructor.apply(size);
}
@Override
public void visitInOrder(ConfigEntryVisitor visitor) {
if (visitor.enterCollectionEntry(this)) {
elementEntry.visitInOrder(visitor);
visitor.leaveCollectionEntry(this);
}
}
}

View File

@@ -0,0 +1,108 @@
package de.siphalor.tweed5.core.impl.entry;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor;
import de.siphalor.tweed5.core.api.validation.ConfigEntryValueValidationException;
import lombok.Getter;
import lombok.Value;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
@Getter
public class ReflectiveCompoundConfigEntryImpl<T> extends BaseConfigEntryImpl<T> implements CompoundConfigEntry<T> {
private final Constructor<T> noArgsConstructor;
private final Map<String, CompoundEntry> compoundEntries;
public ReflectiveCompoundConfigEntryImpl(Class<T> valueClass) {
super(valueClass);
try {
this.noArgsConstructor = valueClass.getConstructor();
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Value class must have a no-arg constructor", e);
}
this.compoundEntries = new LinkedHashMap<>();
}
public void addSubEntry(String name, Field field, ConfigEntry<?> configEntry) {
requireUnsealed();
if (field.getType() != valueClass()) {
throw new IllegalArgumentException("Field is not defined on the correct type");
}
//noinspection unchecked
compoundEntries.put(name, new CompoundEntry(name, field, (ConfigEntry<Object>) configEntry));
}
public Map<String, ConfigEntry<?>> subEntries() {
return compoundEntries.values().stream().collect(Collectors.toMap(CompoundEntry::name, CompoundEntry::configEntry));
}
@Override
public <V> void set(T compoundValue, String key, V value) {
CompoundEntry compoundEntry = compoundEntries.get(key);
if (compoundEntry == null) {
throw new IllegalArgumentException("Unknown config entry: " + key);
}
try {
compoundEntry.configEntry().validate(value);
compoundEntry.field().set(compoundValue, value);
} catch (ConfigEntryValueValidationException e) {
throw new IllegalArgumentException("Invalid value for config entry: " + key, e);
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
@Override
public <V> V get(T compoundValue, String key) {
CompoundEntry compoundEntry = compoundEntries.get(key);
if (compoundEntry == null) {
throw new IllegalArgumentException("Unknown config entry: " + key);
}
try {
//noinspection unchecked
return (V) compoundEntry.field().get(compoundValue);
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
@Override
public T instantiateCompoundValue() {
try {
return noArgsConstructor.newInstance();
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException("Failed to instantiate compound value", e);
}
}
@Override
public void visitInOrder(ConfigEntryVisitor visitor) {
if (visitor.enterCompoundEntry(this)) {
for (Map.Entry<String, CompoundEntry> entry : compoundEntries.entrySet()) {
if (visitor.enterCompoundSubEntry(entry.getKey())) {
entry.getValue().configEntry().visitInOrder(visitor);
visitor.leaveCompoundSubEntry(entry.getKey());
}
}
visitor.leaveCompoundEntry(this);
}
}
@Value
public static class CompoundEntry {
String name;
Field field;
ConfigEntry<Object> configEntry;
}
}

View File

@@ -0,0 +1,9 @@
package de.siphalor.tweed5.core.impl.entry;
import de.siphalor.tweed5.core.api.entry.SimpleConfigEntry;
public class SimpleConfigEntryImpl<T> extends BaseConfigEntryImpl<T> implements SimpleConfigEntry<T> {
public SimpleConfigEntryImpl(Class<T> valueClass) {
super(valueClass);
}
}

View File

@@ -0,0 +1,67 @@
package de.siphalor.tweed5.core.impl.entry;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor;
import org.jetbrains.annotations.NotNull;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.IntFunction;
public class StaticMapCompoundConfigEntryImpl<T extends Map<String, Object>> extends BaseConfigEntryImpl<T> implements CompoundConfigEntry<T> {
private final IntFunction<T> mapConstructor;
private final Map<String, ConfigEntry<?>> compoundEntries = new LinkedHashMap<>();
public StaticMapCompoundConfigEntryImpl(@NotNull Class<T> valueClass, IntFunction<T> mapConstructor) {
super(valueClass);
this.mapConstructor = mapConstructor;
}
public void addSubEntry(String key, ConfigEntry<?> entry) {
requireUnsealed();
compoundEntries.put(key, entry);
}
@Override
public Map<String, ConfigEntry<?>> subEntries() {
return compoundEntries;
}
@Override
public <V> void set(T compoundValue, String key, V value) {
requireKey(key);
compoundValue.put(key, value);
}
@Override
public <V> V get(T compoundValue, String key) {
requireKey(key);
//noinspection unchecked
return (V) compoundValue.get(key);
}
private void requireKey(String key) {
if (!compoundEntries.containsKey(key)) {
throw new IllegalArgumentException("Key " + key + " does not exist on this compound entry!");
}
}
@Override
public T instantiateCompoundValue() {
return mapConstructor.apply(compoundEntries.size());
}
@Override
public void visitInOrder(ConfigEntryVisitor visitor) {
if (visitor.enterCompoundEntry(this)) {
compoundEntries.forEach((key, entry) -> {
if (visitor.enterCompoundSubEntry(key)) {
entry.visitInOrder(visitor);
visitor.leaveCompoundSubEntry(key);
}
});
visitor.leaveCompoundEntry(this);
}
}
}

View File

@@ -0,0 +1 @@
package de.siphalor.tweed5.core.impl;

View File

@@ -0,0 +1,66 @@
package de.siphalor.tweed5.core.impl.sort;
import de.siphalor.tweed5.core.api.sort.AcyclicGraphSorter;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
class AcyclicGraphSorterTest {
@Test
void sort1() {
AcyclicGraphSorter sorter = new AcyclicGraphSorter(4);
sorter.addEdge(2, 1);
sorter.addEdge(0, 2);
assertArrayEquals(new int[]{ 0, 2, 3, 1 }, assertDoesNotThrow(sorter::sort));
}
@Test
void sort2() {
AcyclicGraphSorter sorter = new AcyclicGraphSorter(7);
sorter.addEdge(3, 0);
sorter.addEdge(3, 1);
sorter.addEdge(3, 2);
sorter.addEdge(4, 3);
sorter.addEdge(5, 3);
sorter.addEdge(6, 3);
assertArrayEquals(new int[]{ 4, 5, 6, 3, 0, 1, 2 }, assertDoesNotThrow(sorter::sort));
}
@Test
void sort3() {
AcyclicGraphSorter sorter = new AcyclicGraphSorter(8);
sorter.addEdge(0, 3);
sorter.addEdge(1, 0);
sorter.addEdge(1, 5);
sorter.addEdge(2, 0);
sorter.addEdge(4, 1);
sorter.addEdge(4, 5);
sorter.addEdge(5, 2);
sorter.addEdge(6, 7);
sorter.addEdge(7, 2);
assertArrayEquals(new int[] { 4, 6, 7, 1, 5, 2, 0, 3 }, assertDoesNotThrow(sorter::sort));
}
@Test
void sortErrorCycle() {
AcyclicGraphSorter sorter = new AcyclicGraphSorter(8);
sorter.addEdge(0, 6);
sorter.addEdge(0, 1);
sorter.addEdge(6, 1);
sorter.addEdge(2, 3);
sorter.addEdge(2, 4);
sorter.addEdge(4, 5);
sorter.addEdge(5, 2);
AcyclicGraphSorter.GraphCycleException exception = assertThrows(AcyclicGraphSorter.GraphCycleException.class, sorter::sort);
assertEquals(Arrays.asList(2, 4, 5), exception.cycleIndeces());
}
}

View File

@@ -0,0 +1,6 @@
dependencies {
api(project(":tweed5-core"))
api(project(":tweed5-serde-extension"))
testImplementation(project(":tweed5-serde-hjson"))
}

View File

@@ -0,0 +1,12 @@
package de.siphalor.tweed5.defaultextensions.comment.api;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface AComment {
String value() default "";
}

View File

@@ -0,0 +1,7 @@
package de.siphalor.tweed5.defaultextensions.comment.api;
import de.siphalor.tweed5.core.api.middleware.Middleware;
public interface CommentModifyingExtension {
Middleware<CommentProducer> commentMiddleware();
}

View File

@@ -0,0 +1,8 @@
package de.siphalor.tweed5.defaultextensions.comment.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
@FunctionalInterface
public interface CommentProducer {
String createComment(ConfigEntry<?> entry);
}

View File

@@ -0,0 +1,74 @@
package de.siphalor.tweed5.defaultextensions.comment.impl;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
import de.siphalor.tweed5.core.api.middleware.DefaultMiddlewareContainer;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.data.extension.api.TweedEntryWriter;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteRelatedExtension;
import de.siphalor.tweed5.defaultextensions.comment.api.AComment;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentProducer;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentModifyingExtension;
import lombok.Getter;
import lombok.Value;
import org.jetbrains.annotations.Nullable;
public class CommentExtension implements TweedExtension, ReadWriteRelatedExtension {
@Getter
private RegisteredExtensionData<EntryExtensionsData, InternalCommentEntryData> internalEntryDataExtension;
private DefaultMiddlewareContainer<CommentProducer> middlewareContainer;
@Override
public String getId() {
return "comment";
}
@Override
public void setup(TweedExtensionSetupContext context) {
internalEntryDataExtension = context.registerEntryExtensionData(InternalCommentEntryData.class);
context.registerEntryExtensionData(AComment.class);
middlewareContainer = new DefaultMiddlewareContainer<>();
for (TweedExtension extension : context.configContainer().extensions()) {
if (extension instanceof CommentModifyingExtension) {
middlewareContainer.register(((CommentModifyingExtension) extension).commentMiddleware());
}
}
middlewareContainer.seal();
}
@Override
public @Nullable Middleware<TweedEntryWriter<?, ?>> entryWriterMiddleware() {
return TweedEntryWriterCommentMiddleware.INSTANCE;
}
@Override
public void initEntry(ConfigEntry<?> configEntry) {
EntryExtensionsData entryExtensionsData = configEntry.extensionsData();
String baseComment;
if (entryExtensionsData.isPatchworkPartSet(AComment.class)) {
baseComment = ((AComment) entryExtensionsData).value();
} else {
baseComment = "";
}
CommentProducer middleware = middlewareContainer.process(entry -> baseComment);
internalEntryDataExtension.set(entryExtensionsData, new InternalCommentEntryDataImpl(middleware));
}
@Nullable
String getComment(ConfigEntry<?> configEntry) {
String comment = ((InternalCommentEntryData) configEntry.extensionsData()).commentProducer().createComment(configEntry);
return comment.isEmpty() ? null : comment;
}
@Value
private static class InternalCommentEntryDataImpl implements InternalCommentEntryData {
CommentProducer commentProducer;
}
}

View File

@@ -0,0 +1,7 @@
package de.siphalor.tweed5.defaultextensions.comment.impl;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentProducer;
public interface InternalCommentEntryData {
CommentProducer commentProducer();
}

View File

@@ -0,0 +1,144 @@
package de.siphalor.tweed5.defaultextensions.comment.impl;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.data.extension.api.TweedEntryWriter;
import de.siphalor.tweed5.dataapi.api.TweedDataWriter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class TweedEntryWriterCommentMiddleware implements Middleware<TweedEntryWriter<?, ?>> {
public static final TweedEntryWriterCommentMiddleware INSTANCE = new TweedEntryWriterCommentMiddleware();
@Override
public String id() {
return "comment-writer";
}
@Override
public TweedEntryWriter<?, ?> process(TweedEntryWriter<?, ?> inner) {
//noinspection unchecked
TweedEntryWriter<Object, ConfigEntry<Object>> innerCasted = (TweedEntryWriter<Object, ConfigEntry<Object>>) inner;
return (TweedEntryWriter<Object, ConfigEntry<Object>>) (writer, value, entry, context) -> {
if (writer instanceof CompoundDataWriter) {
// Comment is already written in front of the key by the CompoundDataWriter,
// so we don't have to write it here.
// We also want to unwrap the original writer,
// so that the special comment writing is limited to compounds.
writer = ((CompoundDataWriter) writer).delegate;
} else {
String comment = getEntryComment(entry);
if (comment != null) {
writer.visitComment(comment);
}
}
if (entry instanceof CompoundConfigEntry) {
innerCasted.write(
new CompoundDataWriter(writer, ((CompoundConfigEntry<?>) entry)),
value,
entry,
context
);
} else {
innerCasted.write(writer, value, entry, context);
}
};
}
@RequiredArgsConstructor
private static class CompoundDataWriter implements TweedDataWriter {
private final TweedDataWriter delegate;
private final CompoundConfigEntry<?> compoundConfigEntry;
@Override
public void visitNull() {
delegate.visitNull();
}
@Override
public void visitBoolean(boolean value) {
delegate.visitBoolean(value);
}
@Override
public void visitByte(byte value) {
delegate.visitByte(value);
}
@Override
public void visitShort(short value) {
delegate.visitShort(value);
}
@Override
public void visitInt(int value) {
delegate.visitInt(value);
}
@Override
public void visitLong(long value) {
delegate.visitLong(value);
}
@Override
public void visitFloat(float value) {
delegate.visitFloat(value);
}
@Override
public void visitDouble(double value) {
delegate.visitDouble(value);
}
@Override
public void visitString(@NotNull String value) {
delegate.visitString(value);
}
@Override
public void visitListStart() {
delegate.visitListStart();
}
@Override
public void visitListEnd() {
delegate.visitListEnd();
}
@Override
public void visitMapStart() {
delegate.visitMapStart();
}
@Override
public void visitMapEntryKey(String key) {
ConfigEntry<?> subEntry = compoundConfigEntry.subEntries().get(key);
String subEntryComment = getEntryComment(subEntry);
if (subEntryComment != null) {
delegate.visitComment(subEntryComment);
}
delegate.visitMapEntryKey(key);
}
@Override
public void visitMapEnd() {
delegate.visitMapEnd();
}
@Override
public void visitComment(String comment) {
delegate.visitComment(comment);
}
}
private static @Nullable String getEntryComment(ConfigEntry<?> entry) {
if (!entry.extensionsData().isPatchworkPartSet(InternalCommentEntryData.class)) {
return null;
}
String comment = ((InternalCommentEntryData) entry.extensionsData()).commentProducer().createComment(entry).trim();
return comment.isEmpty() ? null : comment;
}
}

View File

@@ -0,0 +1,4 @@
@ApiStatus.Internal
package de.siphalor.tweed5.defaultextensions.comment.impl;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -0,0 +1,170 @@
package de.siphalor.tweed5.defaultextensions.comment.impl;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.core.impl.DefaultConfigContainer;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.StaticMapCompoundConfigEntryImpl;
import de.siphalor.tweed5.data.extension.api.EntryReaderWriterDefinition;
import de.siphalor.tweed5.data.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.data.extension.api.TweedEntryReader;
import de.siphalor.tweed5.data.extension.api.TweedEntryWriter;
import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriter;
import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters;
import de.siphalor.tweed5.data.extension.impl.ReadWriteExtensionImpl;
import de.siphalor.tweed5.data.hjson.HjsonCommentType;
import de.siphalor.tweed5.data.hjson.HjsonWriter;
import de.siphalor.tweed5.defaultextensions.comment.api.AComment;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentProducer;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentModifyingExtension;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import org.junit.jupiter.api.Test;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
class CommentExtensionTest {
private DefaultConfigContainer<Map<String, Object>> configContainer;
private CommentExtension commentExtension;
private StaticMapCompoundConfigEntryImpl<Map<String, Object>> rootEntry;
private SimpleConfigEntryImpl<Integer> intEntry;
private SimpleConfigEntryImpl<String> stringEntry;
private SimpleConfigEntryImpl<Long> noCommentEntry;
void setupContainer(Collection<TweedExtension> extraExtensions) {
configContainer = new DefaultConfigContainer<>();
commentExtension = new CommentExtension();
configContainer.registerExtension(commentExtension);
extraExtensions.forEach(configContainer::registerExtension);
configContainer.finishExtensionSetup();
//noinspection unchecked
rootEntry = new StaticMapCompoundConfigEntryImpl<>(((Class<Map<String, Object>>)(Class<?>) Map.class), LinkedHashMap::new);
intEntry = new SimpleConfigEntryImpl<>(Integer.class);
rootEntry.addSubEntry("int", intEntry);
stringEntry = new SimpleConfigEntryImpl<>(String.class);
rootEntry.addSubEntry("string", stringEntry);
noCommentEntry = new SimpleConfigEntryImpl<>(Long.class);
rootEntry.addSubEntry("noComment", noCommentEntry);
configContainer.attachAndSealTree(rootEntry);
//noinspection unchecked
RegisteredExtensionData<EntryExtensionsData, AComment> commentData = (RegisteredExtensionData<EntryExtensionsData, AComment>) configContainer.entryDataExtensions().get(AComment.class);
commentData.set(rootEntry.extensionsData(), new ACommentImpl("This is the root value.\nIt is the topmost value in the tree."));
commentData.set(intEntry.extensionsData(), new ACommentImpl("It is an integer"));
commentData.set(stringEntry.extensionsData(), new ACommentImpl("It is a string"));
}
@Test
void simpleComments() {
setupContainer(Collections.emptyList());
configContainer.initialize();
assertEquals("It is an integer", commentExtension.getComment(intEntry));
assertEquals("It is a string", commentExtension.getComment(stringEntry));
assertNull(commentExtension.getComment(noCommentEntry));
}
@Test
void commentProvidingExtension() {
setupContainer(Collections.singletonList(new TestCommentModifyingExtension()));
configContainer.initialize();
assertEquals("The comment is:\nIt is an integer\nEND", commentExtension.getComment(intEntry));
assertEquals("The comment is:\nIt is a string\nEND", commentExtension.getComment(stringEntry));
assertEquals("The comment is:\n\nEND", commentExtension.getComment(noCommentEntry));
}
@Test
void simpleCommentsInHjson() {
ReadWriteExtension readWriteExtension = new ReadWriteExtensionImpl();
setupContainer(Collections.singletonList(readWriteExtension));
setupReadWriteTypes();
configContainer.initialize();
Map<String, Object> value = new HashMap<>();
value.put("int", 123);
value.put("string", "Hello World");
value.put("noComment", 567L);
StringWriter output = new StringWriter();
assertDoesNotThrow(() -> readWriteExtension.write(
new HjsonWriter(output, new HjsonWriter.Options().multilineCommentType(HjsonCommentType.SLASHES)),
value,
rootEntry,
readWriteExtension.createReadWriteContextExtensionsData()
));
assertEquals("// This is the root value.\n// It is the topmost value in the tree.\n" +
"{\n\t// It is an integer\n\tint: 123\n\t// It is a string\n" +
"\tstring: Hello World\n\tnoComment: 567\n}\n", output.toString());
}
private void setupReadWriteTypes() {
//noinspection unchecked
RegisteredExtensionData<EntryExtensionsData, EntryReaderWriterDefinition> readerWriterData = (RegisteredExtensionData<EntryExtensionsData, EntryReaderWriterDefinition>) configContainer.entryDataExtensions().get(EntryReaderWriterDefinition.class);
readerWriterData.set(rootEntry.extensionsData(), new TrivialEntryReaderWriterDefinition(TweedEntryReaderWriters.compoundReaderWriter()));
readerWriterData.set(intEntry.extensionsData(), new TrivialEntryReaderWriterDefinition(TweedEntryReaderWriters.intReaderWriter()));
readerWriterData.set(stringEntry.extensionsData(), new TrivialEntryReaderWriterDefinition(TweedEntryReaderWriters.stringReaderWriter()));
readerWriterData.set(noCommentEntry.extensionsData(), new TrivialEntryReaderWriterDefinition(TweedEntryReaderWriters.longReaderWriter()));
}
@Value
private static class ACommentImpl implements AComment {
String value;
@Override
public Class<? extends Annotation> annotationType() {
return null;
}
}
private static class TestCommentModifyingExtension implements TweedExtension, CommentModifyingExtension {
@Override
public String getId() {
return "test-extension";
}
@Override
public Middleware<CommentProducer> commentMiddleware() {
return new Middleware<CommentProducer>() {
@Override
public String id() {
return "test";
}
@Override
public CommentProducer process(CommentProducer inner) {
return entry -> "The comment is:\n" + inner.createComment(entry) + "\nEND";
}
};
}
}
@RequiredArgsConstructor
private static class TrivialEntryReaderWriterDefinition implements EntryReaderWriterDefinition {
private final TweedEntryReaderWriter<?, ?> readerWriter;
@Override
public TweedEntryReader<?, ?> reader() {
return readerWriter;
}
@Override
public TweedEntryWriter<?, ?> writer() {
return readerWriter;
}
}
}

View File

@@ -0,0 +1,4 @@
dependencies {
implementation("org.ow2.asm:asm:${properties["asm.version"]}")
implementation("org.ow2.asm:asm-commons:${properties["asm.version"]}")
}

View File

@@ -0,0 +1,8 @@
package de.siphalor.tweed5.patchwork.api;
public interface Patchwork<S extends Patchwork<S>> {
boolean isPatchworkPartDefined(Class<?> patchworkInterface);
boolean isPatchworkPartSet(Class<?> patchworkInterface);
S copy();
}

View File

@@ -0,0 +1,85 @@
package de.siphalor.tweed5.patchwork.api;
import de.siphalor.tweed5.patchwork.impl.ByteArrayClassLoader;
import de.siphalor.tweed5.patchwork.impl.PatchworkClass;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassGenerator;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassPart;
import lombok.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Value
public class PatchworkClassCreator<P extends Patchwork<P>> {
@NonNull
Class<P> patchworkInterface;
@NonNull
PatchworkClassGenerator.Config generatorConfig;
public static <P extends Patchwork<P>> Builder<P> builder() {
return new Builder<>();
}
public PatchworkClass<P> createClass(Collection<Class<?>> partInterfaces) throws PatchworkClassGenerator.GenerationException {
List<PatchworkClassPart> parts = partInterfaces.stream().map(PatchworkClassPart::new).collect(Collectors.toList());
PatchworkClassGenerator generator = new PatchworkClassGenerator(generatorConfig, parts);
try {
generator.verify();
} catch (PatchworkClassGenerator.VerificationException e) {
throw new IllegalArgumentException(e);
}
generator.generate();
byte[] classBytes = generator.emit();
//noinspection unchecked
Class<P> patchworkClass = (Class<P>) ByteArrayClassLoader.loadClass(null, classBytes);
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
for (PatchworkClassPart part : parts) {
try {
MethodHandle setterHandle = lookup.findSetter(patchworkClass, part.fieldName(), part.partInterface());
part.fieldSetter(setterHandle);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new IllegalStateException("Failed to access setter for patchwork part " + part.partInterface().getName(), e);
}
}
try {
MethodHandle constructorHandle = lookup.findConstructor(patchworkClass, MethodType.methodType(Void.TYPE));
return PatchworkClass.<P>builder()
.classPackage(generatorConfig.classPackage())
.className(generator.className())
.theClass(patchworkClass)
.constructor(constructorHandle)
.parts(parts)
.build();
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new IllegalStateException("Failed to access constructor of patchwork class", e);
}
}
@Setter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Builder<P extends Patchwork<P>> {
@NonNull
private Class<P> patchworkInterface;
@NonNull
private String classPackage;
private String classPrefix = "";
public PatchworkClassCreator<P> build() {
return new PatchworkClassCreator<>(
patchworkInterface,
new PatchworkClassGenerator.Config(classPackage)
.classPrefix(classPrefix)
.markerInterfaces(Collections.singletonList(patchworkInterface))
);
}
}
}

View File

@@ -0,0 +1,7 @@
package de.siphalor.tweed5.patchwork.api;
public class PatchworkPartIsNullException extends RuntimeException {
public PatchworkPartIsNullException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,19 @@
package de.siphalor.tweed5.patchwork.impl;
public class ByteArrayClassLoader extends ClassLoader {
public static Class<?> loadClass(String binaryClassName, byte[] byteCode) {
return new ByteArrayClassLoader(ByteArrayClassLoader.class.getClassLoader())
.createClass(binaryClassName, byteCode);
}
private ByteArrayClassLoader(ClassLoader parent) {
super(parent);
}
public Class<?> createClass(String binaryClassName, byte[] byteCode) {
Class<?> clazz = defineClass(binaryClassName, byteCode, 0, byteCode.length);
resolveClass(clazz);
return clazz;
}
}

View File

@@ -0,0 +1,18 @@
package de.siphalor.tweed5.patchwork.impl;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import lombok.Builder;
import lombok.Value;
import java.lang.invoke.MethodHandle;
import java.util.Collection;
@Value
@Builder
public class PatchworkClass<P extends Patchwork<P>> {
String classPackage;
String className;
Class<P> theClass;
MethodHandle constructor;
Collection<PatchworkClassPart> parts;
}

View File

@@ -0,0 +1,619 @@
package de.siphalor.tweed5.patchwork.impl;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkPartIsNullException;
import de.siphalor.tweed5.patchwork.impl.util.StreamUtils;
import lombok.*;
import org.objectweb.asm.*;
import org.objectweb.asm.commons.GeneratorAdapter;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Getter
public class PatchworkClassGenerator {
/**
* Class version to use (Java 8)
*/
private static final int CLASS_VERSION = Opcodes.V1_8;
private static final String TARGET_PACKAGE = "de.siphalor.tweed5.core.generated.contextextensions";
private static final List<Type> DEFAULT_PATHWORK_INTERFACES = Collections.singletonList(Type.getType(Patchwork.class));
private static final String INNER_EQUALS_METHOD_NAME = "patchwork$innerEquals";
private static String generateUniqueIdentifier() {
UUID uuid = UUID.randomUUID();
return uuid.toString().replace("-", "");
}
private final Config config;
private final Collection<PatchworkClassPart> parts;
private final String className;
@Getter(AccessLevel.NONE)
private final ClassWriter classWriter;
public PatchworkClassGenerator(Config config, Collection<PatchworkClassPart> parts) {
this.config = config;
this.parts = parts;
className = config.classPrefix() + generateUniqueIdentifier();
classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
}
public String internalClassName() {
return config.classPackage().replace('.', '/') + "/" + className();
}
public String binaryClassName() {
return config.classPackage() + "." + className();
}
public void verify() throws VerificationException {
for (PatchworkClassPart part : parts) {
verifyClass(part.partInterface());
}
verifyPartMethods();
}
private void verifyClass(Class<?> partClass) throws InvalidPatchworkPartClassException {
if (!partClass.isInterface()) {
throw new InvalidPatchworkPartClassException(partClass, "Must be an interface");
}
if ((partClass.getModifiers() & Modifier.PUBLIC) == 0) {
throw new InvalidPatchworkPartClassException(partClass, "Interface must be public");
}
}
private void verifyPartMethods() throws DuplicateMethodsException {
Map<MethodDescriptor, Collection<Method>> methodsBySignature = new HashMap<>();
for (PatchworkClassPart patchworkPart : parts) {
for (Method method : patchworkPart.partInterface().getMethods()) {
MethodDescriptor signature = new MethodDescriptor(method.getName(), method.getParameterTypes());
methodsBySignature
.computeIfAbsent(signature, s -> new ArrayList<>())
.add(method);
}
}
List<Method> duplicateMethods = methodsBySignature.entrySet().stream()
.filter(entry -> entry.getValue().size() > 1)
.flatMap(entry -> entry.getValue().stream())
.collect(Collectors.toList());
if (!duplicateMethods.isEmpty()) {
throw new DuplicateMethodsException(duplicateMethods);
}
}
public void generate() throws GenerationException {
beginClass();
generateSimpleConstructor();
for (PatchworkClassPart extensionClass : parts) {
addPart(extensionClass);
}
appendPojoMethods();
appendDefaultPatchworkMethods();
classWriter.visitEnd();
}
public byte[] emit() {
return classWriter.toByteArray();
}
private void beginClass() {
String[] interfaces = StreamUtils.concat(
Stream.of(Type.getInternalName(Patchwork.class)),
config.markerInterfaces().stream().map(Type::getInternalName),
parts.stream().map(ext -> Type.getInternalName(ext.partInterface()))
).toArray(String[]::new);
classWriter.visit(
CLASS_VERSION,
Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_FINAL,
internalClassName(),
null,
Type.getInternalName(Object.class),
interfaces
);
}
private void generateSimpleConstructor() {
MethodVisitor methodWriter = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
methodWriter.visitCode();
methodWriter.visitVarInsn(Opcodes.ALOAD, 0);
methodWriter.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
methodWriter.visitInsn(Opcodes.RETURN);
methodWriter.visitMaxs(1, 1);
methodWriter.visitEnd();
}
private void appendPojoMethods() {
appendEqualsMethod();
appendHashCodeMethod();
appendToStringMethod();
}
// <editor-fold desc="POJO Methods">
private void appendEqualsMethod() {
appendInnerEqualsMethod();
GeneratorAdapter methodWriter = createMethod(
Opcodes.ACC_PUBLIC,
"equals",
"(Ljava/lang/Object;)Z",
null,
null
);
methodWriter.visitParameter("other", Opcodes.ACC_FINAL);
methodWriter.visitCode();
Label falseLabel = methodWriter.newLabel();
Label continueLabel = methodWriter.newLabel();
methodWriter.loadArg(0);
methodWriter.loadThis();
methodWriter.visitJumpInsn(Opcodes.IF_ACMPNE, continueLabel);
methodWriter.push(true);
methodWriter.returnValue();
methodWriter.visitLabel(continueLabel);
methodWriter.loadArg(0);
methodWriter.visitTypeInsn(Opcodes.INSTANCEOF, internalClassName());
methodWriter.visitJumpInsn(Opcodes.IFEQ, falseLabel);
methodWriter.loadArg(0);
methodWriter.visitTypeInsn(Opcodes.CHECKCAST, internalClassName());
methodWriter.loadThis();
methodWriter.visitMethodInsn(
Opcodes.INVOKESPECIAL,
internalClassName(),
INNER_EQUALS_METHOD_NAME,
"(L" + internalClassName() + ";)Z",
false
);
methodWriter.visitJumpInsn(Opcodes.IFEQ, falseLabel);
methodWriter.push(true);
methodWriter.returnValue();
methodWriter.visitLabel(falseLabel);
methodWriter.push(false);
methodWriter.returnValue();
methodWriter.visitMaxs(0, 0);
methodWriter.visitEnd();
}
private void appendInnerEqualsMethod() {
GeneratorAdapter methodWriter = createMethod(
Opcodes.ACC_PRIVATE,
INNER_EQUALS_METHOD_NAME,
"(L" + internalClassName() + ";)Z",
null,
null
);
methodWriter.visitParameter("other", Opcodes.ACC_FINAL);
methodWriter.visitCode();
Label falseLabel = methodWriter.newLabel();
for (PatchworkClassPart part : parts) {
methodWriter.loadArg(0);
visitFieldInsn(methodWriter, part, Opcodes.GETFIELD);
methodWriter.loadThis();
visitFieldInsn(methodWriter, part, Opcodes.GETFIELD);
methodWriter.visitMethodInsn(
Opcodes.INVOKESTATIC,
Type.getInternalName(Objects.class),
"equals",
"(Ljava/lang/Object;Ljava/lang/Object;)Z",
false
);
methodWriter.visitJumpInsn(Opcodes.IFEQ, falseLabel);
}
methodWriter.push(true);
methodWriter.returnValue();
methodWriter.visitLabel(falseLabel);
methodWriter.push(false);
methodWriter.returnValue();
methodWriter.visitMaxs(0, 0);
methodWriter.visitEnd();
}
private void appendHashCodeMethod() {
GeneratorAdapter methodWriter = createMethod(
Opcodes.ACC_PUBLIC,
"hashCode",
"()I",
null,
null
);
methodWriter.visitCode();
methodWriter.push(parts.size());
methodWriter.newArray(Type.getType(Object.class));
int i = 0;
for (PatchworkClassPart part : parts) {
methodWriter.dup();
methodWriter.push(i);
methodWriter.loadThis();
visitFieldInsn(methodWriter, part, Opcodes.GETFIELD);
methodWriter.visitInsn(Opcodes.AASTORE);
i++;
}
methodWriter.visitMethodInsn(
Opcodes.INVOKESTATIC,
"java/util/Objects",
"hash",
"([Ljava/lang/Object;)I",
false
);
methodWriter.returnValue();
methodWriter.visitMaxs(0, 0);
methodWriter.visitEnd();
}
private void appendToStringMethod() {
GeneratorAdapter methodWriter = createMethod(
Opcodes.ACC_PUBLIC,
"toString",
"()Ljava/lang/String;",
null,
null
);
methodWriter.visitCode();
String stringBuilderType = Type.getInternalName(StringBuilder.class);
methodWriter.visitTypeInsn(Opcodes.NEW, stringBuilderType);
methodWriter.dup();
methodWriter.push(className().length() + 10 + parts.size() * 64);
methodWriter.visitMethodInsn(
Opcodes.INVOKESPECIAL,
stringBuilderType,
"<init>",
"(I)V",
false
);
StringBuilder constantConcat = new StringBuilder();
constantConcat.append(className()).append("{\n");
for (PatchworkClassPart part : parts) {
constantConcat.append("\t").append(part.partInterface().getSimpleName()).append(": ");
methodWriter.push(constantConcat.toString());
constantConcat.setLength(0);
visitStringBuilderAppendString(methodWriter);
Label nullLabel = methodWriter.newLabel();
Label continueLabel = methodWriter.newLabel();
methodWriter.loadThis();
visitFieldInsn(methodWriter, part, Opcodes.GETFIELD);
methodWriter.dup();
methodWriter.visitJumpInsn(Opcodes.IFNULL, nullLabel);
visitToString(methodWriter);
methodWriter.visitJumpInsn(Opcodes.GOTO, continueLabel);
methodWriter.visitLabel(nullLabel);
methodWriter.pop();
methodWriter.push("<unset>");
methodWriter.visitLabel(continueLabel);
visitStringBuilderAppendString(methodWriter);
constantConcat.append(",\n");
}
constantConcat.append("}");
methodWriter.push(constantConcat.toString());
visitStringBuilderAppendString(methodWriter);
visitToString(methodWriter);
methodWriter.returnValue();
methodWriter.visitMaxs(0, 0);
methodWriter.visitEnd();
}
// </editor-fold>
private void appendDefaultPatchworkMethods() {
appendCopyMethod();
appendIsPatchworkPartDefinedMethod();
appendIsPatchworkPartSetMethod();
}
// <editor-fold desc="Patchwork Methods">
private void appendCopyMethod() {
GeneratorAdapter methodWriter = createMethod(
Opcodes.ACC_PUBLIC,
"copy",
"()L" + Type.getInternalName(Patchwork.class) + ";",
null,
null
);
methodWriter.visitCode();
methodWriter.visitTypeInsn(Opcodes.NEW, internalClassName());
methodWriter.dup();
methodWriter.visitMethodInsn(
Opcodes.INVOKESPECIAL,
internalClassName(),
"<init>",
"()V",
false
);
for (PatchworkClassPart part : parts) {
methodWriter.dup();
methodWriter.loadThis();
visitFieldInsn(methodWriter, part, Opcodes.GETFIELD);
visitFieldInsn(methodWriter, part, Opcodes.PUTFIELD);
}
methodWriter.returnValue();
methodWriter.visitMaxs(0, 0);
methodWriter.visitEnd();
}
private void appendIsPatchworkPartDefinedMethod() {
GeneratorAdapter methodWriter = createMethod(
Opcodes.ACC_PUBLIC,
"isPatchworkPartDefined",
"(Ljava/lang/Class;)Z",
null,
null
);
methodWriter.visitParameter(null, Opcodes.ACC_FINAL);
methodWriter.visitCode();
Label trueLabel = methodWriter.newLabel();
for (PatchworkClassPart part : parts) {
methodWriter.loadArg(0);
methodWriter.push(Type.getType(part.partInterface()));
methodWriter.ifCmp(Type.getType(Object.class), GeneratorAdapter.EQ, trueLabel);
}
methodWriter.push(false);
methodWriter.returnValue();
methodWriter.visitLabel(trueLabel);
methodWriter.push(true);
methodWriter.returnValue();
methodWriter.visitMaxs(0, 0);
methodWriter.visitEnd();
}
private void appendIsPatchworkPartSetMethod() {
GeneratorAdapter methodWriter = createMethod(
Opcodes.ACC_PUBLIC,
"isPatchworkPartSet",
"(Ljava/lang/Class;)Z",
null,
null
);
methodWriter.visitParameter(null, Opcodes.ACC_FINAL);
methodWriter.visitCode();
Label[] labels = new Label[parts.size()];
int i = 0;
for (PatchworkClassPart part : parts) {
labels[i] = methodWriter.newLabel();
methodWriter.loadArg(0);
methodWriter.push(Type.getType(part.partInterface()));
methodWriter.ifCmp(Type.getType(Object.class), GeneratorAdapter.EQ, labels[i]);
i++;
}
methodWriter.push(false);
methodWriter.returnValue();
Label falseLabel = methodWriter.newLabel();
i = 0;
for (PatchworkClassPart part : parts) {
methodWriter.visitLabel(labels[i]);
methodWriter.loadThis();
visitFieldInsn(methodWriter, part, Opcodes.GETFIELD);
methodWriter.push((String) null);
methodWriter.ifCmp(Type.getType(part.partInterface()), GeneratorAdapter.EQ, falseLabel);
methodWriter.push(true);
methodWriter.returnValue();
i++;
}
methodWriter.visitLabel(falseLabel);
methodWriter.push(false);
methodWriter.returnValue();
methodWriter.visitMaxs(1, 1);
methodWriter.visitEnd();
}
public void addPart(PatchworkClassPart patchworkPart) throws GenerationException {
patchworkPart.fieldName("f_" + generateUniqueIdentifier());
classWriter.visitField(
Opcodes.ACC_PUBLIC,
patchworkPart.fieldName(),
patchworkPart.partInterface().descriptorString(),
null,
null
);
appendPartMethods(patchworkPart);
}
private void appendPartMethods(PatchworkClassPart patchworkPart) throws GenerationException {
try {
ClassReader classReader = new ClassReader(patchworkPart.partInterface().getName());
classReader.accept(new PartClassVisitor(patchworkPart), ClassReader.SKIP_FRAMES);
} catch (IOException e) {
throw new GenerationException("Failed to read interface class file", e);
}
}
// </editor-fold>
private GeneratorAdapter createMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor methodVisitor = classWriter.visitMethod(access, name, desc, signature, exceptions);
return new GeneratorAdapter(methodVisitor, access, name, desc);
}
private void visitFieldInsn(MethodVisitor methodWriter, PatchworkClassPart part, int opcode) {
methodWriter.visitFieldInsn(
opcode,
internalClassName(),
part.fieldName(),
Type.getDescriptor(part.partInterface())
);
}
private static void visitToString(MethodVisitor methodWriter) {
methodWriter.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
Type.getInternalName(Object.class),
"toString",
"()Ljava/lang/String;",
false
);
}
private static void visitStringBuilderAppendString(MethodVisitor methodWriter) {
String stringBuilderType = Type.getInternalName(StringBuilder.class);
methodWriter.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
stringBuilderType,
"append",
"(Ljava/lang/String;)L" + stringBuilderType + ";",
false
);
}
private class PartClassVisitor extends ClassVisitor {
private final PatchworkClassPart extensionClass;
protected PartClassVisitor(PatchworkClassPart extensionClass) {
super(Opcodes.ASM9);
this.extensionClass = extensionClass;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
GeneratorAdapter methodWriter = createMethod(Opcodes.ACC_PUBLIC, name, descriptor, signature, exceptions);
return new PartMethodVisitor(api, methodWriter, descriptor, extensionClass);
}
}
private class PartMethodVisitor extends MethodVisitor {
private final GeneratorAdapter methodWriter;
private final String methodDescriptor;
private final PatchworkClassPart patchworkPart;
protected PartMethodVisitor(int api, GeneratorAdapter methodWriter, String methodDescriptor, PatchworkClassPart patchworkPart) {
super(api);
this.methodWriter = methodWriter;
this.patchworkPart = patchworkPart;
this.methodDescriptor = methodDescriptor;
}
@Override
public void visitParameter(String name, int access) {
methodWriter.visitParameter(name, access);
}
@Override
public void visitEnd() {
Label nullLabel = methodWriter.newLabel();
methodWriter.visitCode();
methodWriter.visitVarInsn(Opcodes.ALOAD, 0);
methodWriter.visitFieldInsn(Opcodes.GETFIELD, internalClassName(), patchworkPart.fieldName(), Type.getDescriptor(patchworkPart.partInterface()));
methodWriter.dup();
methodWriter.ifNull(nullLabel);
methodWriter.loadArgs();
methodWriter.visitMethodInsn(
Opcodes.INVOKEINTERFACE,
Type.getInternalName(patchworkPart.partInterface()),
methodWriter.getName(),
methodDescriptor,
true
);
methodWriter.returnValue();
methodWriter.visitLabel(nullLabel);
methodWriter.pop();
methodWriter.throwException(
Type.getType(PatchworkPartIsNullException.class),
"The patchwork part " + patchworkPart.partInterface().getSimpleName() + " has not been set"
);
methodWriter.visitMaxs(-1, -1);
methodWriter.visitEnd();
}
}
@Value
private static class MethodDescriptor {
String name;
Class<?>[] parameterTypes;
}
@Data
public static class Config {
@NonNull
private String classPackage;
@NonNull
private String classPrefix = "";
@NonNull
private Collection<Class<?>> markerInterfaces = Collections.emptyList();
}
public static class VerificationException extends Exception {
private VerificationException(String message) {
super(message);
}
}
@Value
@EqualsAndHashCode(callSuper = true)
public static class InvalidPatchworkPartClassException extends VerificationException {
Class<?> partClass;
public InvalidPatchworkPartClassException(Class<?> partClass, String message) {
super("Invalid patchwork part class " + partClass.getName() + ": " + message);
this.partClass = partClass;
}
}
@Value
@EqualsAndHashCode(callSuper = true)
public static class DuplicateMethodsException extends VerificationException {
transient Collection<Method> signatures;
private DuplicateMethodsException(Collection<Method> methods) {
super("Duplicate method signatures:\n" + methods.stream().map(DuplicateMethodsException::getMethodMessage).collect(Collectors.joining("\n")));
this.signatures = methods;
}
private static String getMethodMessage(Method method) {
StringBuilder stringBuilder = new StringBuilder("\t- " + method.getDeclaringClass().getCanonicalName() + "#(");
for (Class<?> parameterType : method.getParameterTypes()) {
stringBuilder.append(parameterType.getCanonicalName());
stringBuilder.append(", ");
}
stringBuilder.append(")");
stringBuilder.append(method.getReturnType().getCanonicalName());
stringBuilder.append("\n");
return stringBuilder.toString();
}
}
public static class GenerationException extends Exception {
public GenerationException(String message, Throwable cause) {
super(message, cause);
}
}
}

View File

@@ -0,0 +1,12 @@
package de.siphalor.tweed5.patchwork.impl;
import lombok.Data;
import java.lang.invoke.MethodHandle;
@Data
public class PatchworkClassPart {
private final Class<?> partInterface;
private String fieldName;
private MethodHandle fieldSetter;
}

View File

@@ -0,0 +1,14 @@
package de.siphalor.tweed5.patchwork.impl.util;
import java.util.stream.Stream;
public class StreamUtils {
@SafeVarargs
public static <T> Stream<T> concat(Stream<T>... streams) {
Stream<T> result = Stream.empty();
for (Stream<T> stream : streams) {
result = Stream.concat(result, stream);
}
return result;
}
}

View File

@@ -0,0 +1,269 @@
package de.siphalor.tweed5.patchwork.impl;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkPartIsNullException;
import lombok.Data;
import lombok.experimental.Accessors;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.*;
class PatchworkClassGeneratorGeneratedClassTest {
PatchworkClassPart partA;
PatchworkClassPart partB;
byte[] bytes;
Class<Patchwork<?>> patchworkClass;
Patchwork<?> patchwork;
@BeforeEach
void setUp() {
partA = new PatchworkClassPart(ExtensionA.class);
partB = new PatchworkClassPart(ExtensionB.class);
PatchworkClassGenerator generator = new PatchworkClassGenerator(
new PatchworkClassGenerator.Config("de.siphalor.tweed5.core.test")
.classPrefix("FullTest$")
.markerInterfaces(Collections.singletonList(MarkerInterface.class)),
Arrays.asList(partA, partB)
);
assertDoesNotThrow(generator::verify);
assertDoesNotThrow(generator::generate);
bytes = generator.emit();
//noinspection unchecked
patchworkClass = (Class<Patchwork<?>>) assertDoesNotThrow(() -> ByteArrayClassLoader.loadClass(null, bytes));
patchwork = createPatchworkInstance();
}
@Test
@Disabled("Dumping the class is only for testing purposes")
void dumpClass() {
try {
Path target = File.createTempFile("tweed5-patchwork", ".class").toPath();
try (OutputStream os = Files.newOutputStream(target)) {
os.write(bytes);
}
System.out.println("Dumped generated class to " + target);
} catch (IOException e) {
assertNull(e, "Must not throw exception");
}
}
@Test
void packageName() {
assertEquals("de.siphalor.tweed5.core.test", patchworkClass.getPackage().getName());
}
@Test
void className() {
assertTrue(patchworkClass.getSimpleName().startsWith("FullTest$"), "Generated class name must start with prefix FullTest$, got " + patchworkClass.getSimpleName());
}
@Test
void implementsInterfaces() {
assertImplements(MarkerInterface.class, patchworkClass);
assertImplements(Patchwork.class, patchworkClass);
assertImplements(ExtensionA.class, patchworkClass);
assertImplements(ExtensionB.class, patchworkClass);
}
@Test
void toStringMethod() {
int defaultHashCode = System.identityHashCode(patchwork);
String stringResult = patchwork.toString();
assertFalse(stringResult.contains(Integer.toHexString(defaultHashCode)), "Expected toString not to be the default toString, got: " + stringResult);
}
@Test
void toStringContent() {
setFieldValue(patchwork, partA.fieldName(), new ExtensionAImpl());
((ExtensionA) patchwork).setText("Hello World!");
String stringResult = patchwork.toString();
assertTrue(stringResult.contains("Hello World!"), "Expected toString to contain the toString of its extensions, got: " + stringResult);
}
@Test
void hashCodeMethod() {
int emptyHashCode = patchwork.hashCode();
setFieldValue(patchwork, partA.fieldName(), new ExtensionAImpl());
int hashCodeWithA = patchwork.hashCode();
((ExtensionA) patchwork).setText("Hello World!");
int hashCodeWithAContent = patchwork.hashCode();
assertNotEquals(emptyHashCode, hashCodeWithA, "Expected hashCode to be different, got: " + emptyHashCode + " and " + hashCodeWithA);
assertNotEquals(emptyHashCode, hashCodeWithAContent, "Expected hashCode to be different, got: " + emptyHashCode + " and " + hashCodeWithAContent);
assertNotEquals(hashCodeWithA, hashCodeWithAContent, "Expected hashCode to be different, got: " + hashCodeWithA + " and " + hashCodeWithAContent);
}
@SuppressWarnings("SimplifiableAssertion")
@Test
void equalsMethod() {
//noinspection ConstantValue,SimplifiableAssertion
assertFalse(patchwork.equals(null));
//noinspection EqualsWithItself
assertTrue(patchwork.equals(patchwork));
Patchwork<?> other = createPatchworkInstance();
assertTrue(patchwork.equals(other));
assertTrue(other.equals(patchwork));
setFieldValue(patchwork, partA.fieldName(), new ExtensionAImpl());
assertFalse(patchwork.equals(other));
setFieldValue(other, partA.fieldName(), new ExtensionAImpl());
assertTrue(patchwork.equals(other));
assertTrue(other.equals(patchwork));
((ExtensionA) patchwork).setText("Hello 1!");
((ExtensionA) other).setText("Hello 2!");
assertFalse(patchwork.equals(other));
}
@Test
void copy() {
ExtensionAImpl aImpl = new ExtensionAImpl();
setFieldValue(patchwork, partA.fieldName(), aImpl);
Patchwork<?> copy = patchwork.copy();
Object aValue = getFieldValue(copy, partA.fieldName());
Object bValue = getFieldValue(copy, partB.fieldName());
assertSame(aImpl, aValue);
assertNull(bValue);
}
@Test
void isPartDefined() {
assertPartDefined(ExtensionA.class, patchwork);
assertPartDefined(ExtensionB.class, patchwork);
assertPartUndefined(ExtensionC.class, patchwork);
}
static void assertPartDefined(Class<?> anInterface, Patchwork<?> patchwork) {
assertTrue(patchwork.isPatchworkPartDefined(anInterface), "Patchwork part " + anInterface.getName() + " must be defined");
}
static void assertPartUndefined(Class<?> anInterface, Patchwork<?> patchwork) {
assertFalse(patchwork.isPatchworkPartDefined(anInterface), "Patchwork part " + anInterface.getName() + " must not be defined");
}
@Test
void checkFieldsExist() {
assertFieldExists(partA.fieldName(), ExtensionA.class);
assertFieldExists(partB.fieldName(), ExtensionB.class);
}
void assertFieldExists(String fieldName, Class<?> fieldType) {
Field field = getField(fieldName);
assertEquals(fieldType, field.getType());
}
@Test
void isPartSet() {
assertPartUnset(ExtensionA.class, patchwork);
assertPartUnset(ExtensionB.class, patchwork);
assertPartUnset(ExtensionC.class, patchwork);
setFieldValue(patchwork, partA.fieldName(), new ExtensionAImpl());
assertPartSet(ExtensionA.class, patchwork);
assertPartUnset(ExtensionB.class, patchwork);
assertPartUnset(ExtensionC.class, patchwork);
setFieldValue(patchwork, partB.fieldName(), new ExtensionBImpl());
assertPartSet(ExtensionA.class, patchwork);
assertPartSet(ExtensionB.class, patchwork);
assertPartUnset(ExtensionC.class, patchwork);
}
@Test
void inheritedMethodCalls() {
assertThrows(PatchworkPartIsNullException.class, () -> ((ExtensionA) patchwork).getText());
assertThrows(PatchworkPartIsNullException.class, () -> ((ExtensionB) patchwork).multiply(1, 2));
setFieldValue(patchwork, partA.fieldName(), new ExtensionAImpl());
setFieldValue(patchwork, partB.fieldName(), new ExtensionBImpl());
assertEquals(6, assertDoesNotThrow(() -> ((ExtensionB) patchwork).multiply(2, 3)), "Method of extension b must be working");
assertDoesNotThrow(() -> ((ExtensionA) patchwork).setText("something"));
assertEquals("something", ((ExtensionA) patchwork).getText());
}
Patchwork<?> createPatchworkInstance() {
Constructor<?> constructor = assertDoesNotThrow(() -> patchworkClass.getConstructor(), "The generated class' constructor must be accessible");
return assertDoesNotThrow(() -> (Patchwork<?>) constructor.newInstance());
}
Object getFieldValue(Patchwork<?> patchwork, String fieldName) {
return assertDoesNotThrow(() -> getField(fieldName).get(patchwork));
}
void setFieldValue(Patchwork<?> patchwork, String fieldName, Object value) {
assertDoesNotThrow(() -> getField(fieldName).set(patchwork, value), "Field " + fieldName + " must be accessible");
}
Field getField(String fieldName) {
return assertDoesNotThrow(() -> patchworkClass.getField(fieldName), "Field " + fieldName + " must exist and be public");
}
static void assertPartSet(Class<?> anInterface, Patchwork<?> patchwork) {
assertTrue(patchwork.isPatchworkPartSet(anInterface), "Patchwork part " + anInterface.getName() + " must be set");
}
static void assertPartUnset(Class<?> anInterface, Patchwork<?> patchwork) {
assertFalse(patchwork.isPatchworkPartSet(anInterface), "Patchwork part " + anInterface.getName() + " must not be set");
}
static void assertImplements(Class<?> anInterface, Class<?> theClass) {
assertTrue(anInterface.isAssignableFrom(theClass), "Class " + theClass.getName() + " must implement " + anInterface.getName());
}
public interface MarkerInterface {}
public interface ExtensionA {
String getText();
void setText(String text);
}
@Data
@Accessors(fluent = false)
static class ExtensionAImpl implements ExtensionA {
private String text;
}
public interface ExtensionB {
int multiply(int a, int b);
}
static class ExtensionBImpl implements ExtensionB {
public int multiply(int a, int b) {
return a * b;
}
}
public interface ExtensionC {
void test();
}
}

View File

@@ -0,0 +1,52 @@
package de.siphalor.tweed5.patchwork.impl;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertThrows;
class PatchworkClassGeneratorTest {
@Test
void notAnInterface() {
PatchworkClassGenerator generator = createGenerator(Collections.singletonList(NotAnInterface.class));
assertThrows(PatchworkClassGenerator.VerificationException.class, generator::verify);
}
@Test
void nonPublicInterface() {
PatchworkClassGenerator generator = createGenerator(Collections.singletonList(NonPublicInterface.class));
assertThrows(PatchworkClassGenerator.VerificationException.class, generator::verify);
}
@Test
void duplicateFields() {
PatchworkClassGenerator generator = createGenerator(Arrays.asList(DuplicateA.class, DuplicateB.class));
assertThrows(PatchworkClassGenerator.VerificationException.class, generator::verify);
}
PatchworkClassGenerator createGenerator(Collection<Class<?>> partClasses) {
return new PatchworkClassGenerator(
new PatchworkClassGenerator.Config("de.siphalor.tweed5.patchwork.test.generated"),
partClasses.stream().map(PatchworkClassPart::new).collect(Collectors.toList())
);
}
public static class NotAnInterface {
}
interface NonPublicInterface {
}
public interface DuplicateA {
void test();
}
public interface DuplicateB {
void test();
}
}

View File

@@ -0,0 +1,43 @@
package de.siphalor.tweed5.dataapi.api;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.jetbrains.annotations.Nullable;
@Getter
public class TweedDataReadException extends RuntimeException {
@Nullable
private final TweedDataReaderRecoverMode recoverMode;
protected TweedDataReadException(String message, Throwable cause, @Nullable TweedDataReaderRecoverMode recoverMode) {
super(message, cause);
this.recoverMode = recoverMode;
}
public boolean canRecover() {
return recoverMode != null;
}
public static Builder builder() {
return new Builder();
}
@Setter
public static class Builder {
private String message;
private Throwable cause;
@Setter(AccessLevel.NONE)
@Nullable
private TweedDataReaderRecoverMode recoverMode;
public TweedDataReadException build() {
return new TweedDataReadException(message, cause, recoverMode);
}
public Builder recoverable(TweedDataReaderRecoverMode recoverMode) {
this.recoverMode = recoverMode;
return this;
}
}
}

View File

@@ -0,0 +1,6 @@
package de.siphalor.tweed5.dataapi.api;
public interface TweedDataReader {
TweedDataToken peekToken() throws TweedDataReadException;
TweedDataToken readToken() throws TweedDataReadException;
}

View File

@@ -0,0 +1,6 @@
package de.siphalor.tweed5.dataapi.api;
public enum TweedDataReaderRecoverMode {
SKIP,
REPEAT,
}

View File

@@ -0,0 +1,82 @@
package de.siphalor.tweed5.dataapi.api;
public interface TweedDataToken {
default boolean isNull() {
return false;
}
default boolean canReadAsBoolean() {
return false;
}
default boolean readAsBoolean() throws TweedDataReadException {
throw createUnsupportedReadException("boolean");
}
default boolean canReadAsByte() {
return false;
}
default byte readAsByte() throws TweedDataReadException {
throw createUnsupportedReadException("byte");
}
default boolean canReadAsShort() {
return false;
}
default short readAsShort() throws TweedDataReadException {
throw createUnsupportedReadException("short");
}
default boolean canReadAsInt() {
return false;
}
default int readAsInt() throws TweedDataReadException {
throw createUnsupportedReadException("integer");
}
default boolean canReadAsLong() {
return false;
}
default long readAsLong() throws TweedDataReadException {
throw createUnsupportedReadException("long");
}
default boolean canReadAsFloat() {
return false;
}
default float readAsFloat() throws TweedDataReadException {
throw createUnsupportedReadException("float");
}
default boolean canReadAsDouble() {
return false;
}
default double readAsDouble() throws TweedDataReadException {
throw createUnsupportedReadException("double");
}
default boolean canReadAsString() {
return false;
}
default String readAsString() throws TweedDataReadException {
throw createUnsupportedReadException("string");
}
default boolean isListStart() {
return false;
}
default boolean isListValue() {
return false;
}
default boolean isListEnd() {
return false;
}
default boolean isMapStart() {
return false;
}
default boolean isMapEntryKey() {
return false;
}
default boolean isMapEntryValue() {
return false;
}
default boolean isMapEnd() {
return false;
}
default TweedDataReadException createUnsupportedReadException(String requestedType) {
return TweedDataReadException.builder().message("Token can not be read as " + requestedType + ": " + this).build();
}
}

View File

@@ -0,0 +1,259 @@
package de.siphalor.tweed5.dataapi.api;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public final class TweedDataTokens {
private static final TweedDataToken NULL = new TweedDataToken() {
@Override
public boolean isNull() {
return true;
}
@Override
public String toString() {
return "NULL";
}
};
private static final TweedDataToken LIST_START = new TweedDataToken() {
@Override
public boolean isListStart() {
return true;
}
@Override
public String toString() {
return "LIST_START";
}
};
private static final TweedDataToken LIST_END = new TweedDataToken() {
@Override
public boolean isListEnd() {
return true;
}
@Override
public String toString() {
return "LIST_END";
}
};
private static final TweedDataToken MAP_START = new TweedDataToken() {
@Override
public boolean isMapStart() {
return true;
}
@Override
public String toString() {
return "MAP_START";
}
};
private static final TweedDataToken MAP_END = new TweedDataToken() {
@Override
public boolean isMapEnd() {
return true;
}
@Override
public String toString() {
return "MAP_END";
}
};
public static TweedDataToken getNull() {
return NULL;
}
public static TweedDataToken getListStart() {
return LIST_START;
}
public static TweedDataToken asListValue(TweedDataToken delegate) {
return new DelegatingToken(delegate) {
@Override
public boolean isListValue() {
return true;
}
@Override
public String toString() {
return "LIST_VALUE[" + delegate.toString() + "]";
}
};
}
public static TweedDataToken getListEnd() {
return LIST_END;
}
public static TweedDataToken getMapStart() {
return MAP_START;
}
public static TweedDataToken asMapEntryKey(TweedDataToken delegate) {
return new DelegatingToken(delegate) {
@Override
public boolean isMapEntryKey() {
return true;
}
@Override
public String toString() {
return "MAP_ENTRY_KEY[" + delegate.toString() + "]";
}
};
}
public static TweedDataToken asMapEntryValue(TweedDataToken delegate) {
return new DelegatingToken(delegate) {
@Override
public boolean isMapEntryValue() {
return true;
}
@Override
public String toString() {
return "MAP_ENTRY_VALUE[" + delegate.toString() + "]";
}
};
}
public static TweedDataToken getMapEnd() {
return MAP_END;
}
@RequiredArgsConstructor
private static class DelegatingToken implements TweedDataToken {
private final TweedDataToken delegate;
@Override
public boolean isNull() {
return delegate.isNull();
}
@Override
public boolean canReadAsBoolean() {
return delegate.canReadAsBoolean();
}
@Override
public boolean readAsBoolean() throws TweedDataReadException {
return delegate.readAsBoolean();
}
@Override
public boolean canReadAsByte() {
return delegate.canReadAsByte();
}
@Override
public byte readAsByte() throws TweedDataReadException {
return delegate.readAsByte();
}
@Override
public boolean canReadAsShort() {
return delegate.canReadAsShort();
}
@Override
public short readAsShort() throws TweedDataReadException {
return delegate.readAsShort();
}
@Override
public boolean canReadAsInt() {
return delegate.canReadAsInt();
}
@Override
public int readAsInt() throws TweedDataReadException {
return delegate.readAsInt();
}
@Override
public boolean canReadAsLong() {
return delegate.canReadAsLong();
}
@Override
public long readAsLong() throws TweedDataReadException {
return delegate.readAsLong();
}
@Override
public boolean canReadAsFloat() {
return delegate.canReadAsFloat();
}
@Override
public float readAsFloat() throws TweedDataReadException {
return delegate.readAsFloat();
}
@Override
public boolean canReadAsDouble() {
return delegate.canReadAsDouble();
}
@Override
public double readAsDouble() throws TweedDataReadException {
return delegate.readAsDouble();
}
@Override
public boolean canReadAsString() {
return delegate.canReadAsString();
}
@Override
public String readAsString() throws TweedDataReadException {
return delegate.readAsString();
}
@Override
public boolean isListStart() {
return delegate.isListStart();
}
@Override
public boolean isListValue() {
return delegate.isListValue();
}
@Override
public boolean isListEnd() {
return delegate.isListEnd();
}
@Override
public boolean isMapStart() {
return delegate.isMapStart();
}
@Override
public boolean isMapEntryKey() {
return delegate.isMapEntryKey();
}
@Override
public boolean isMapEntryValue() {
return delegate.isMapEntryValue();
}
@Override
public boolean isMapEnd() {
return delegate.isMapEnd();
}
@Override
public String toString() {
return delegate.toString() + "(delegated)";
}
}
}

View File

@@ -0,0 +1,32 @@
package de.siphalor.tweed5.dataapi.api;
import org.jetbrains.annotations.NotNull;
public interface TweedDataVisitor {
void visitNull();
void visitBoolean(boolean value);
void visitByte(byte value);
void visitShort(short value);
void visitInt(int value);
void visitLong(long value);
void visitFloat(float value);
void visitDouble(double value);
void visitString(@NotNull String value);
default void visitEmptyList() {
visitListStart();
visitListEnd();
}
void visitListStart();
void visitListEnd();
default void visitEmptyMap() {
visitMapStart();
visitMapEnd();
}
void visitMapStart();
void visitMapEntryKey(String key);
void visitMapEnd();
void visitComment(String comment);
}

View File

@@ -0,0 +1,18 @@
package de.siphalor.tweed5.dataapi.api;
public class TweedDataWriteException extends RuntimeException {
public TweedDataWriteException() {
}
public TweedDataWriteException(String message) {
super(message);
}
public TweedDataWriteException(String message, Throwable cause) {
super(message, cause);
}
public TweedDataWriteException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,5 @@
package de.siphalor.tweed5.dataapi.api;
public interface TweedDataWriter extends TweedDataVisitor {
}

View File

@@ -0,0 +1,11 @@
package de.siphalor.tweed5.dataapi.api;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public interface TweedSerde {
TweedDataReader createReader(InputStream inputStream);
TweedDataVisitor createWriter(OutputStream outputStream) throws IOException;
String getPreferredFileExtension();
}

View File

@@ -0,0 +1,7 @@
dependencies {
api(project(":tweed5-core"))
api(project(":tweed5-patchwork"))
api(project(":tweed5-serde-api"))
testImplementation(project(":tweed5-serde-hjson"))
}

View File

@@ -0,0 +1,6 @@
package de.siphalor.tweed5.data.extension.api;
public interface EntryReaderWriterDefinition {
TweedEntryReader<?, ?> reader();
TweedEntryWriter<?, ?> writer();
}

View File

@@ -0,0 +1,6 @@
package de.siphalor.tweed5.data.extension.api;
public interface ReadWriteEntryDataExtension {
TweedEntryReader<?, ?> entryReaderChain();
TweedEntryWriter<?, ?> entryWriterChain();
}

View File

@@ -0,0 +1,15 @@
package de.siphalor.tweed5.data.extension.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteContextExtensionsData;
import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataWriter;
public interface ReadWriteExtension extends TweedExtension {
ReadWriteContextExtensionsData createReadWriteContextExtensionsData();
<T> T read(TweedDataReader reader, ConfigEntry<T> entry, ReadWriteContextExtensionsData contextExtensionsData) throws TweedEntryReadException;
<T> void write(TweedDataWriter writer, T value, ConfigEntry<T> entry, ReadWriteContextExtensionsData contextExtensionsData) throws TweedEntryWriteException;
}

View File

@@ -0,0 +1,18 @@
package de.siphalor.tweed5.data.extension.api;
public class TweedEntryReadException extends Exception {
public TweedEntryReadException() {
}
public TweedEntryReadException(String message) {
super(message);
}
public TweedEntryReadException(String message, Throwable cause) {
super(message, cause);
}
public TweedEntryReadException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,10 @@
package de.siphalor.tweed5.data.extension.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.dataapi.api.TweedDataReadException;
import de.siphalor.tweed5.dataapi.api.TweedDataReader;
@FunctionalInterface
public interface TweedEntryReader<T, C extends ConfigEntry<T>> {
T read(TweedDataReader reader, C entry, TweedReadContext context) throws TweedEntryReadException, TweedDataReadException;
}

View File

@@ -0,0 +1,18 @@
package de.siphalor.tweed5.data.extension.api;
public class TweedEntryWriteException extends Exception {
public TweedEntryWriteException() {
}
public TweedEntryWriteException(String message) {
super(message);
}
public TweedEntryWriteException(String message, Throwable cause) {
super(message, cause);
}
public TweedEntryWriteException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,10 @@
package de.siphalor.tweed5.data.extension.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.dataapi.api.TweedDataWriteException;
import de.siphalor.tweed5.dataapi.api.TweedDataWriter;
@FunctionalInterface
public interface TweedEntryWriter<T, C extends ConfigEntry<T>> {
void write(TweedDataWriter writer, T value, C entry, TweedWriteContext context) throws TweedEntryWriteException, TweedDataWriteException;
}

View File

@@ -0,0 +1,7 @@
package de.siphalor.tweed5.data.extension.api;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteContextExtensionsData;
public interface TweedReadContext {
ReadWriteContextExtensionsData extensionsData();
}

View File

@@ -0,0 +1,7 @@
package de.siphalor.tweed5.data.extension.api;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteContextExtensionsData;
public interface TweedWriteContext {
ReadWriteContextExtensionsData extensionsData();
}

View File

@@ -0,0 +1,6 @@
package de.siphalor.tweed5.data.extension.api.extension;
import de.siphalor.tweed5.patchwork.api.Patchwork;
public interface ReadWriteContextExtensionsData extends Patchwork<ReadWriteContextExtensionsData> {
}

View File

@@ -0,0 +1,7 @@
package de.siphalor.tweed5.data.extension.api.extension;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
public interface ReadWriteExtensionSetupContext {
<E> RegisteredExtensionData<ReadWriteContextExtensionsData, E> registerReadWriteContextExtensionData(Class<E> extensionDataClass);
}

View File

@@ -0,0 +1,22 @@
package de.siphalor.tweed5.data.extension.api.extension;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.data.extension.api.TweedEntryReader;
import de.siphalor.tweed5.data.extension.api.TweedEntryWriter;
import org.jetbrains.annotations.Nullable;
public interface ReadWriteRelatedExtension {
default void setupReadWriteExtension(ReadWriteExtensionSetupContext context) {
}
@Nullable
default Middleware<TweedEntryReader<?, ?>> entryReaderMiddleware() {
return null;
}
@Nullable
default Middleware<TweedEntryWriter<?, ?>> entryWriterMiddleware() {
return null;
}
}

View File

@@ -0,0 +1,7 @@
package de.siphalor.tweed5.data.extension.api.readwrite;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.data.extension.api.TweedEntryReader;
import de.siphalor.tweed5.data.extension.api.TweedEntryWriter;
public interface TweedEntryReaderWriter<T, C extends ConfigEntry<T>> extends TweedEntryReader<T, C>, TweedEntryWriter<T, C> {}

View File

@@ -0,0 +1,55 @@
package de.siphalor.tweed5.data.extension.api.readwrite;
import de.siphalor.tweed5.core.api.entry.CoherentCollectionConfigEntry;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.data.extension.impl.TweedEntryReaderWriterImpls;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.util.Collection;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TweedEntryReaderWriters {
public static TweedEntryReaderWriter<Boolean, ConfigEntry<Boolean>> booleanReaderWriter() {
return TweedEntryReaderWriterImpls.BOOLEAN_READER_WRITER;
}
public static TweedEntryReaderWriter<Byte, ConfigEntry<Byte>> byteReaderWriter() {
return TweedEntryReaderWriterImpls.BYTE_READER_WRITER;
}
public static TweedEntryReaderWriter<Short, ConfigEntry<Short>> shortReaderWriter() {
return TweedEntryReaderWriterImpls.SHORT_READER_WRITER;
}
public static TweedEntryReaderWriter<Integer, ConfigEntry<Integer>> intReaderWriter() {
return TweedEntryReaderWriterImpls.INT_READER_WRITER;
}
public static TweedEntryReaderWriter<Long, ConfigEntry<Long>> longReaderWriter() {
return TweedEntryReaderWriterImpls.LONG_READER_WRITER;
}
public static TweedEntryReaderWriter<Float, ConfigEntry<Float>> floatReaderWriter() {
return TweedEntryReaderWriterImpls.FLOAT_READER_WRITER;
}
public static TweedEntryReaderWriter<Double, ConfigEntry<Double>> doubleReaderWriter() {
return TweedEntryReaderWriterImpls.DOUBLE_READER_WRITER;
}
public static TweedEntryReaderWriter<String, ConfigEntry<String>> stringReaderWriter() {
return TweedEntryReaderWriterImpls.STRING_READER_WRITER;
}
public static <T, C extends Collection<T>> TweedEntryReaderWriter<C, CoherentCollectionConfigEntry<T, C>> coherentCollectionReaderWriter() {
//noinspection unchecked
return (TweedEntryReaderWriter<C, CoherentCollectionConfigEntry<T,C>>)(TweedEntryReaderWriter<?, ?>) TweedEntryReaderWriterImpls.COHERENT_COLLECTION_READER_WRITER;
}
public static <T> TweedEntryReaderWriter<T, CompoundConfigEntry<T>> compoundReaderWriter() {
//noinspection unchecked
return (TweedEntryReaderWriter<T, CompoundConfigEntry<T>>)(TweedEntryReaderWriter<?, ?>) TweedEntryReaderWriterImpls.COMPOUND_READER_WRITER;
}
}

View File

@@ -0,0 +1,172 @@
package de.siphalor.tweed5.data.extension.impl;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
import de.siphalor.tweed5.core.api.middleware.DefaultMiddlewareContainer;
import de.siphalor.tweed5.data.extension.api.*;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteContextExtensionsData;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteExtensionSetupContext;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteRelatedExtension;
import de.siphalor.tweed5.dataapi.api.TweedDataReadException;
import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataWriteException;
import de.siphalor.tweed5.dataapi.api.TweedDataWriter;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkClassCreator;
import de.siphalor.tweed5.patchwork.impl.PatchworkClass;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassGenerator;
import lombok.Setter;
import lombok.Value;
import java.lang.invoke.MethodHandle;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class ReadWriteExtensionImpl implements ReadWriteExtension {
private RegisteredExtensionData<EntryExtensionsData, ReadWriteEntryDataExtension> readWriteEntryDataExtension;
private DefaultMiddlewareContainer<TweedEntryReader<?, ?>> entryReaderMiddlewareContainer;
private DefaultMiddlewareContainer<TweedEntryWriter<?, ?>> entryWriterMiddlewareContainer;
private Map<Class<?>, RegisteredExtensionDataImpl<ReadWriteContextExtensionsData, ?>> readWriteContextExtensionsDataClasses;
private PatchworkClass<ReadWriteContextExtensionsData> readWriteContextExtensionsDataPatchwork;
@Override
public String getId() {
return "read-write";
}
@Override
public void setup(TweedExtensionSetupContext context) {
readWriteEntryDataExtension = context.registerEntryExtensionData(ReadWriteEntryDataExtension.class);
context.registerEntryExtensionData(EntryReaderWriterDefinition.class);
Collection<TweedExtension> extensions = context.configContainer().extensions();
readWriteContextExtensionsDataClasses = new HashMap<>(extensions.size());
ReadWriteExtensionSetupContext setupContext = new ReadWriteExtensionSetupContext() {
@Override
public <E> RegisteredExtensionData<ReadWriteContextExtensionsData, E> registerReadWriteContextExtensionData(Class<E> extensionDataClass) {
if (readWriteContextExtensionsDataClasses.containsKey(extensionDataClass)) {
throw new IllegalArgumentException("Context extension " + extensionDataClass.getName() + " is already registered");
}
RegisteredExtensionDataImpl<ReadWriteContextExtensionsData, E> registeredExtensionData = new RegisteredExtensionDataImpl<>();
readWriteContextExtensionsDataClasses.put(extensionDataClass, registeredExtensionData);
return registeredExtensionData;
}
};
entryReaderMiddlewareContainer = new DefaultMiddlewareContainer<>();
entryWriterMiddlewareContainer = new DefaultMiddlewareContainer<>();
for (TweedExtension extension : extensions) {
if (extension instanceof ReadWriteRelatedExtension) {
ReadWriteRelatedExtension rwExtension = (ReadWriteRelatedExtension) extension;
rwExtension.setupReadWriteExtension(setupContext);
if (rwExtension.entryReaderMiddleware() != null) {
entryReaderMiddlewareContainer.register(rwExtension.entryReaderMiddleware());
}
if (rwExtension.entryWriterMiddleware() != null) {
entryWriterMiddlewareContainer.register(rwExtension.entryWriterMiddleware());
}
}
}
entryReaderMiddlewareContainer.seal();
entryWriterMiddlewareContainer.seal();
PatchworkClassCreator<ReadWriteContextExtensionsData> patchworkClassCreator = PatchworkClassCreator.<ReadWriteContextExtensionsData>builder()
.patchworkInterface(ReadWriteContextExtensionsData.class)
.classPackage("de.siphalor.tweed5.data.extension.generated")
.classPrefix("ReadWriteContextExtensionsData$")
.build();
try {
readWriteContextExtensionsDataPatchwork = patchworkClassCreator.createClass(readWriteContextExtensionsDataClasses.keySet());
} catch (PatchworkClassGenerator.GenerationException e) {
throw new IllegalStateException("Failed to generate read write context extensions' data patchwork class", e);
}
}
@Override
public void initEntry(ConfigEntry<?> configEntry) {
TweedEntryReader<?, ?> baseReader;
TweedEntryWriter<?, ?> baseWriter;
if (configEntry.extensionsData().isPatchworkPartSet(EntryReaderWriterDefinition.class)) {
EntryReaderWriterDefinition rwDefintion = (EntryReaderWriterDefinition) configEntry.extensionsData();
baseReader = rwDefintion.reader();
baseWriter = rwDefintion.writer();
} else {
baseReader = TweedEntryReaderWriterImpls.NOOP_READER_WRITER;
baseWriter = TweedEntryReaderWriterImpls.NOOP_READER_WRITER;
}
readWriteEntryDataExtension.set(configEntry.extensionsData(), new ReadWriteEntryDataExtensionImpl(
entryReaderMiddlewareContainer.process(baseReader),
entryWriterMiddlewareContainer.process(baseWriter)
));
}
@Override
public ReadWriteContextExtensionsData createReadWriteContextExtensionsData() {
try {
return (ReadWriteContextExtensionsData) readWriteContextExtensionsDataPatchwork.constructor().invoke();
} catch (Throwable e) {
throw new IllegalStateException("Failed to instantiate read write context extensions' data", e);
}
}
@Override
public <T> T read(TweedDataReader reader, ConfigEntry<T> entry, ReadWriteContextExtensionsData contextExtensionsData) throws TweedEntryReadException {
try {
return getReaderChain(entry).read(reader, entry, new TweedReadWriteContextImpl(contextExtensionsData));
} catch (TweedDataReadException e) {
throw new TweedEntryReadException("Failed to read entry", e);
}
}
@Override
public <T> void write(TweedDataWriter writer, T value, ConfigEntry<T> entry, ReadWriteContextExtensionsData contextExtensionsData) throws TweedEntryWriteException {
try {
getWriterChain(entry).write(writer, value, entry, new TweedReadWriteContextImpl(contextExtensionsData));
} catch (TweedDataWriteException e) {
throw new TweedEntryWriteException("Failed to write entry", e);
}
}
@Value
private static class ReadWriteEntryDataExtensionImpl implements ReadWriteEntryDataExtension {
TweedEntryReader<?, ?> entryReaderChain;
TweedEntryWriter<?, ?> entryWriterChain;
}
@Setter
private static class RegisteredExtensionDataImpl<U extends Patchwork<U>, E> implements RegisteredExtensionData<U, E> {
private MethodHandle setter;
@Override
public void set(U patchwork, E extension) {
try {
setter.invokeWithArguments(patchwork, extension);
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}
}
static <T> TweedEntryReader<T, ConfigEntry<T>> getReaderChain(ConfigEntry<T> elementEntry) {
//noinspection unchecked
return (TweedEntryReader<T, ConfigEntry<T>>) ((ReadWriteEntryDataExtension) elementEntry.extensionsData()).entryReaderChain();
}
static <T> TweedEntryWriter<T, ConfigEntry<T>> getWriterChain(ConfigEntry<T> elementEntry) {
//noinspection unchecked
return (TweedEntryWriter<T, ConfigEntry<T>>) ((ReadWriteEntryDataExtension) elementEntry.extensionsData()).entryWriterChain();
}
}

View File

@@ -0,0 +1,201 @@
package de.siphalor.tweed5.data.extension.impl;
import de.siphalor.tweed5.core.api.entry.CoherentCollectionConfigEntry;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.data.extension.api.*;
import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriter;
import de.siphalor.tweed5.dataapi.api.*;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TweedEntryReaderWriterImpls {
public static final TweedEntryReaderWriter<Boolean, ConfigEntry<Boolean>> BOOLEAN_READER_WRITER = new PrimitiveReaderWriter<>(TweedDataToken::readAsBoolean, TweedDataVisitor::visitBoolean);
public static final TweedEntryReaderWriter<Byte, ConfigEntry<Byte>> BYTE_READER_WRITER = new PrimitiveReaderWriter<>(TweedDataToken::readAsByte, TweedDataVisitor::visitByte);
public static final TweedEntryReaderWriter<Short, ConfigEntry<Short>> SHORT_READER_WRITER = new PrimitiveReaderWriter<>(TweedDataToken::readAsShort, TweedDataVisitor::visitShort);
public static final TweedEntryReaderWriter<Integer, ConfigEntry<Integer>> INT_READER_WRITER = new PrimitiveReaderWriter<>(TweedDataToken::readAsInt, TweedDataVisitor::visitInt);
public static final TweedEntryReaderWriter<Long, ConfigEntry<Long>> LONG_READER_WRITER = new PrimitiveReaderWriter<>(TweedDataToken::readAsLong, TweedDataVisitor::visitLong);
public static final TweedEntryReaderWriter<Float, ConfigEntry<Float>> FLOAT_READER_WRITER = new PrimitiveReaderWriter<>(TweedDataToken::readAsFloat, TweedDataVisitor::visitFloat);
public static final TweedEntryReaderWriter<Double, ConfigEntry<Double>> DOUBLE_READER_WRITER = new PrimitiveReaderWriter<>(TweedDataToken::readAsDouble, TweedDataVisitor::visitDouble);
public static final TweedEntryReaderWriter<String, ConfigEntry<String>> STRING_READER_WRITER = new PrimitiveReaderWriter<>(TweedDataToken::readAsString, TweedDataVisitor::visitString);
public static final TweedEntryReaderWriter<Collection<Object>, CoherentCollectionConfigEntry<Object, Collection<Object>>> COHERENT_COLLECTION_READER_WRITER = new CoherentCollectionReaderWriter<>();
public static final TweedEntryReaderWriter<Object, CompoundConfigEntry<Object>> COMPOUND_READER_WRITER = new CompoundReaderWriter<>();
public static final TweedEntryReaderWriter<Object, ConfigEntry<Object>> NOOP_READER_WRITER = new NoopReaderWriter();
@RequiredArgsConstructor
private static class PrimitiveReaderWriter<T> implements TweedEntryReaderWriter<T, ConfigEntry<T>> {
private final Function<TweedDataToken, T> readerCall;
private final BiConsumer<TweedDataWriter, T> writerCall;
@Override
public T read(TweedDataReader reader, ConfigEntry<T> entry, TweedReadContext context) throws TweedDataReadException {
return readerCall.apply(reader.readToken());
}
@Override
public void write(TweedDataWriter writer, T value, ConfigEntry<T> entry, TweedWriteContext context) throws TweedEntryWriteException, TweedDataWriteException {
requireNonNullWriteValue(value);
writerCall.accept(writer, value);
}
}
public static class CoherentCollectionReaderWriter<T, C extends Collection<T>> implements TweedEntryReaderWriter<C, CoherentCollectionConfigEntry<T, C>> {
@Override
public C read(TweedDataReader reader, CoherentCollectionConfigEntry<T, C> entry, TweedReadContext context) throws TweedEntryReadException, TweedDataReadException {
assertIsToken(reader.readToken(), TweedDataToken::isListStart, "Expected list start");
TweedDataToken token = reader.peekToken();
if (token.isListEnd()) {
return entry.instantiateCollection(0);
}
ConfigEntry<T> elementEntry = entry.elementEntry();
TweedEntryReader<T, ConfigEntry<T>> elementReader = ReadWriteExtensionImpl.getReaderChain(elementEntry);
List<T> list = new ArrayList<>(20);
while (true) {
token = reader.peekToken();
if (token.isListEnd()) {
reader.readToken();
break;
} else if (token.isListValue()) {
list.add(elementReader.read(reader, elementEntry, context));
} else {
throw new TweedEntryReadException("Unexpected token " + token + ": expected next list value or list end");
}
}
C result = entry.instantiateCollection(list.size());
result.addAll(list);
return result;
}
@Override
public void write(TweedDataWriter writer, C value, CoherentCollectionConfigEntry<T, C> entry, TweedWriteContext context) throws TweedEntryWriteException, TweedDataWriteException {
requireNonNullWriteValue(value);
if (value.isEmpty()) {
writer.visitEmptyList();
return;
}
ConfigEntry<T> elementEntry = entry.elementEntry();
TweedEntryWriter<T, ConfigEntry<T>> elementWriter = ReadWriteExtensionImpl.getWriterChain(elementEntry);
writer.visitListStart();
for (T element : value) {
elementWriter.write(writer, element, elementEntry, context);
}
writer.visitListEnd();
}
}
public static class CompoundReaderWriter<T> implements TweedEntryReaderWriter<T, CompoundConfigEntry<T>> {
@Override
public T read(TweedDataReader reader, CompoundConfigEntry<T> entry, TweedReadContext context) throws TweedEntryReadException, TweedDataReadException {
assertIsToken(reader.readToken(), TweedDataToken::isMapStart, "Expected map start");
Map<String, ConfigEntry<?>> compoundEntries = entry.subEntries();
T compoundValue = entry.instantiateCompoundValue();
while (true) {
TweedDataToken token = reader.readToken();
if (token.isMapEnd()) {
break;
} else if (token.isMapEntryKey()) {
String key = token.readAsString();
//noinspection unchecked
ConfigEntry<Object> subEntry = (ConfigEntry<Object>) compoundEntries.get(key);
TweedEntryReader<Object, ConfigEntry<Object>> subEntryReaderChain = ReadWriteExtensionImpl.getReaderChain(subEntry);
Object subEntryValue = subEntryReaderChain.read(reader, subEntry, context);
entry.set(compoundValue, key, subEntryValue);
} else {
throw new TweedEntryReadException("Unexpected token " + token + ": Expected map key or map end");
}
}
return compoundValue;
}
@Override
public void write(TweedDataWriter writer, T value, CompoundConfigEntry<T> entry, TweedWriteContext context) throws TweedEntryWriteException, TweedDataWriteException {
requireNonNullWriteValue(value);
writer.visitMapStart();
//noinspection unchecked
Map<String, ConfigEntry<Object>> compoundEntries = (Map<String, ConfigEntry<Object>>)(Map<?, ?>) entry.subEntries();
for (Map.Entry<String, ConfigEntry<Object>> e : compoundEntries.entrySet()) {
String key = e.getKey();
ConfigEntry<Object> subEntry = e.getValue();
writer.visitMapEntryKey(key);
TweedEntryWriter<Object, ConfigEntry<Object>> subEntryWriterChain = ReadWriteExtensionImpl.getWriterChain(subEntry);
subEntryWriterChain.write(writer, entry.get(value, key), subEntry, context);
}
writer.visitMapEnd();
}
}
public static class NoopReaderWriter implements TweedEntryReaderWriter<Object, ConfigEntry<Object>> {
@Override
public Object read(TweedDataReader reader, ConfigEntry<Object> entry, TweedReadContext context) throws TweedDataReadException {
TweedDataToken token = reader.readToken();
if (!token.isListStart() && !token.isMapStart()) {
return null;
}
ArrayDeque<Context> stack = new ArrayDeque<>(20);
if (token.isListStart()) {
stack.push(Context.LIST);
} else if (token.isMapStart()) {
stack.push(Context.MAP);
}
while (true) {
token = reader.readToken();
if (token.isListStart()) {
stack.push(Context.LIST);
} else if (token.isMapStart()) {
stack.push(Context.MAP);
} else if (token.isListEnd() || token.isMapEnd()) {
stack.pop();
}
if (stack.isEmpty()) {
return null;
}
}
}
@Override
public void write(TweedDataWriter writer, Object value, ConfigEntry<Object> entry, TweedWriteContext context) throws TweedDataWriteException {
writer.visitNull();
}
private enum Context {
LIST, MAP,
}
}
private static <T> void requireNonNullWriteValue(T value) throws TweedEntryWriteException {
if (value == null) {
throw new TweedEntryWriteException("Unable to write null value");
}
}
private static void assertIsToken(TweedDataToken token, Predicate<TweedDataToken> isToken, String description) throws TweedEntryReadException {
if (!isToken.test(token)) {
throw new TweedEntryReadException("Unexpected token " + token + ": " + description);
}
}
}

View File

@@ -0,0 +1,11 @@
package de.siphalor.tweed5.data.extension.impl;
import de.siphalor.tweed5.data.extension.api.TweedReadContext;
import de.siphalor.tweed5.data.extension.api.TweedWriteContext;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteContextExtensionsData;
import lombok.Value;
@Value
public class TweedReadWriteContextImpl implements TweedReadContext, TweedWriteContext {
ReadWriteContextExtensionsData extensionsData;
}

View File

@@ -0,0 +1,4 @@
@ApiStatus.Internal
package de.siphalor.tweed5.data.extension.impl;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -0,0 +1,118 @@
package de.siphalor.tweed5.data.extension.impl;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import de.siphalor.tweed5.core.impl.DefaultConfigContainer;
import de.siphalor.tweed5.core.impl.entry.CoherentCollectionConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.StaticMapCompoundConfigEntryImpl;
import de.siphalor.tweed5.data.extension.api.EntryReaderWriterDefinition;
import de.siphalor.tweed5.data.extension.api.TweedEntryReader;
import de.siphalor.tweed5.data.extension.api.TweedEntryWriter;
import de.siphalor.tweed5.data.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriter;
import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters;
import de.siphalor.tweed5.data.hjson.HjsonLexer;
import de.siphalor.tweed5.data.hjson.HjsonReader;
import de.siphalor.tweed5.data.hjson.HjsonWriter;
import de.siphalor.tweed5.dataapi.api.TweedDataWriter;
import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.*;
import java.util.function.Function;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ReadWriteExtensionImplTest {
private StringWriter stringWriter;
private StaticMapCompoundConfigEntryImpl<Map<String, Object>> rootEntry;
private ReadWriteExtension readWriteExtension;
@SuppressWarnings("unchecked")
@BeforeEach
void setUp() {
ConfigContainer<Map<String, Object>> configContainer = new DefaultConfigContainer<>();
readWriteExtension = new ReadWriteExtensionImpl();
configContainer.registerExtension(readWriteExtension);
configContainer.finishExtensionSetup();
rootEntry = new StaticMapCompoundConfigEntryImpl<>(((Class<Map<String, Object>>) (Class<?>) Map.class), LinkedHashMap::new);
SimpleConfigEntryImpl<Integer> intEntry = new SimpleConfigEntryImpl<>(Integer.class);
rootEntry.addSubEntry("int", intEntry);
CoherentCollectionConfigEntryImpl<Boolean, List<Boolean>> listEntry = new CoherentCollectionConfigEntryImpl<>((Class<List<Boolean>>) (Class<?>) List.class, ArrayList::new);
rootEntry.addSubEntry("list", listEntry);
SimpleConfigEntryImpl<Boolean> booleanEntry = new SimpleConfigEntryImpl<>(Boolean.class);
listEntry.elementEntry(booleanEntry);
configContainer.attachAndSealTree(rootEntry);
RegisteredExtensionData<EntryExtensionsData, EntryReaderWriterDefinition> readerWriterData = (RegisteredExtensionData<EntryExtensionsData, EntryReaderWriterDefinition>) configContainer.entryDataExtensions().get(EntryReaderWriterDefinition.class);
readerWriterData.set(rootEntry.extensionsData(), new TrivialEntryReaderWriterDefinition(TweedEntryReaderWriters.compoundReaderWriter()));
readerWriterData.set(intEntry.extensionsData(), new TrivialEntryReaderWriterDefinition(TweedEntryReaderWriters.intReaderWriter()));
readerWriterData.set(listEntry.extensionsData(), new TrivialEntryReaderWriterDefinition(TweedEntryReaderWriters.coherentCollectionReaderWriter()));
readerWriterData.set(booleanEntry.extensionsData(), new TrivialEntryReaderWriterDefinition(TweedEntryReaderWriters.booleanReaderWriter()));
configContainer.initialize();
}
@Test
void write() {
Map<String, Object> value = new HashMap<>();
value.put("int", 123);
value.put("list", Arrays.asList(true, false, true));
assertDoesNotThrow(() -> readWriteExtension.write(
setupWriter(writer -> new HjsonWriter(writer, new HjsonWriter.Options())),
value,
rootEntry,
readWriteExtension.createReadWriteContextExtensionsData()
));
assertEquals("{\n\tint: 123\n\tlist: [\n\t\ttrue\n\t\tfalse\n\t\ttrue\n\t]\n}\n", stringWriter.toString());
}
@Test
void read() {
Map<String, Object> result = assertDoesNotThrow(() -> readWriteExtension.read(
new HjsonReader(new HjsonLexer(new StringReader("{\n\tint: 123\n\tlist: [\n\t\ttrue\n\t\tfalse\n\t\ttrue\n\t]\n}\n"))),
rootEntry,
readWriteExtension.createReadWriteContextExtensionsData()
));
assertEquals(2, result.size());
assertEquals(123, result.get("int"));
assertEquals(Arrays.asList(true, false, true), result.get("list"));
}
private TweedDataWriter setupWriter(Function<Writer, TweedDataWriter> writerFactory) {
stringWriter = new StringWriter();
return writerFactory.apply(stringWriter);
}
@RequiredArgsConstructor
private static class TrivialEntryReaderWriterDefinition implements EntryReaderWriterDefinition {
private final TweedEntryReaderWriter<?, ?> readerWriter;
@Override
public TweedEntryReader<?, ?> reader() {
return readerWriter;
}
@Override
public TweedEntryWriter<?, ?> writer() {
return readerWriter;
}
}
}

View File

@@ -0,0 +1,3 @@
dependencies {
api(project(":tweed5-serde-api"))
}

View File

@@ -0,0 +1,15 @@
package de.siphalor.tweed5.data.hjson;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
public enum HjsonCommentType {
HASH(false),
SLASHES(false),
BLOCK(true),
;
private final boolean block;
}

View File

@@ -0,0 +1,487 @@
package de.siphalor.tweed5.data.hjson;
import de.siphalor.tweed5.dataapi.api.TweedDataReadException;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.Reader;
import java.util.PrimitiveIterator;
@ApiStatus.Internal
@RequiredArgsConstructor
public class HjsonLexer {
private static final int EMPTY_CODEPOINT = -2;
private final Reader reader;
private final HjsonReadPosition currentPos = new HjsonReadPosition();
private int peekedCodePoint = EMPTY_CODEPOINT;
private int peeked2CodePoint = EMPTY_CODEPOINT;
public HjsonLexerToken nextGeneralToken() throws TweedDataReadException {
chompInlineWhitespaceAndComments();
int codePoint = eatCodePoint();
HjsonLexerToken.Type terminalTokenType = getTerminalTokenType(codePoint);
if (terminalTokenType != null) {
return createTerminalToken(terminalTokenType);
}
HjsonLexerToken token = tryReadQuotedString(codePoint);
if (token != null) {
return token;
}
return readQuotelessLiteral(codePoint);
}
public HjsonLexerToken nextInnerObjectToken() throws TweedDataReadException {
chompInlineWhitespaceAndComments();
int codePoint = eatCodePoint();
HjsonLexerToken.Type terminalTokenType = getTerminalTokenType(codePoint);
if (terminalTokenType != null) {
return createTerminalToken(terminalTokenType);
}
if (codePoint == '"') {
return readJsonQuotedString(codePoint);
} else if (codePoint == '\'') {
return readJsonQuotedString(codePoint);
} else if (codePoint < 0x21) {
throw TweedDataReadException.builder().message("Illegal character \"" + String.copyValueOf(Character.toChars(codePoint)) + "\"").build();
} else {
return readQuotelessMemberName(codePoint);
}
}
@Nullable
private HjsonLexerToken.Type getTerminalTokenType(int codePoint) {
switch (codePoint) {
case -1: return HjsonLexerToken.Type.EOF;
case '[': return HjsonLexerToken.Type.BRACKET_OPEN;
case ']': return HjsonLexerToken.Type.BRACKET_CLOSE;
case '{': return HjsonLexerToken.Type.BRACE_OPEN;
case '}': return HjsonLexerToken.Type.BRACE_CLOSE;
case ':': return HjsonLexerToken.Type.COLON;
case ',': return HjsonLexerToken.Type.COMMA;
case '\n': return HjsonLexerToken.Type.LINE_FEED;
default: return null;
}
}
private HjsonLexerToken createTerminalToken(HjsonLexerToken.Type tokenType) {
HjsonReadPosition position = currentPos.copy();
return new HjsonLexerToken(tokenType, position, position, null);
}
@Nullable
private HjsonLexerToken tryReadQuotedString(int codePoint) throws TweedDataReadException {
if (codePoint == '"') {
return readJsonQuotedString('"');
} else if (codePoint == '\'') {
int peek = peekCodePoint();
if (peek == '\'') {
int peek2 = peek2CodePoint();
if (peek2 == '\'') {
return readMultilineString();
} else {
HjsonReadPosition beginPos = currentPos.copy();
eatCodePoint();
return new HjsonLexerToken(
HjsonLexerToken.Type.JSON_STRING,
beginPos,
currentPos.copy(),
"''"
);
}
} else {
return readJsonQuotedString('\'');
}
} else {
return null;
}
}
private HjsonLexerToken readJsonQuotedString(int quoteCodePoint) throws TweedDataReadException {
HjsonReadPosition beginPos = currentPos.copy();
StringBuilder tokenBuffer = new StringBuilder();
tokenBuffer.appendCodePoint(quoteCodePoint);
while (true) {
int codePoint = eatCodePoint();
if (codePoint == -1) {
throw TweedDataReadException.builder().message("Unterminated quoted string at " + currentPos + ", started at " + beginPos).build();
} else if (codePoint == quoteCodePoint) {
tokenBuffer.appendCodePoint(codePoint);
return new HjsonLexerToken(
HjsonLexerToken.Type.JSON_STRING,
beginPos,
currentPos.copy(),
tokenBuffer
);
} else if (codePoint == '\\') {
tokenBuffer.appendCodePoint(codePoint);
tokenBuffer.appendCodePoint(eatCodePoint());
} else {
tokenBuffer.appendCodePoint(codePoint);
}
}
}
private HjsonLexerToken readMultilineString() throws TweedDataReadException {
HjsonReadPosition beginPos = currentPos.copy();
int indentToChomp = beginPos.index() - 1;
eatCodePoint();
eatCodePoint();
StringBuilder tokenBuffer = new StringBuilder();
tokenBuffer.append("'''");
boolean chompIndent = false;
while (true) {
int codePoint = eatCodePoint();
if (codePoint == -1) {
throw TweedDataReadException.builder().message("Unexpected end of multiline string at " + currentPos + ", started at " + beginPos).build();
} else if (isInlineWhitespace(codePoint)) {
tokenBuffer.appendCodePoint(codePoint);
} else {
if (codePoint == '\n') {
chompIndent = true;
tokenBuffer.setLength(3);
}
break;
}
}
while (true) {
if (chompIndent) {
chompMultilineStringIndent(indentToChomp);
} else {
chompIndent = true;
}
int singleQuoteCount = 0;
while (true) {
int codePoint = eatCodePoint();
if (codePoint == -1) {
throw TweedDataReadException.builder().message("Unexpected end of multiline string at " + currentPos + ", started at " + beginPos).build();
}
if (codePoint == '\'') {
singleQuoteCount++;
if (singleQuoteCount == 3) {
char lastActualChar = tokenBuffer.charAt(tokenBuffer.length() - 3);
if (lastActualChar == '\n') {
tokenBuffer.delete(tokenBuffer.length() - 3, tokenBuffer.length() - 2);
}
tokenBuffer.append('\'');
return new HjsonLexerToken(
HjsonLexerToken.Type.MULTILINE_STRING,
beginPos,
currentPos.copy(),
tokenBuffer
);
}
} else {
singleQuoteCount = 0;
}
tokenBuffer.appendCodePoint(codePoint);
if (codePoint == '\n') {
break;
}
}
}
}
private HjsonLexerToken readQuotelessMemberName(int codepoint) throws TweedDataReadException {
HjsonReadPosition beginPos = currentPos.copy();
StringBuilder tokenBuffer = new StringBuilder();
tokenBuffer.appendCodePoint(codepoint);
while (true) {
int peek = peekCodePoint();
if (peek == -1 || peek == '\n' || isPunctuator(peek)) {
break;
}
tokenBuffer.appendCodePoint(eatCodePoint());
}
return new HjsonLexerToken(
HjsonLexerToken.Type.QUOTELESS_STRING,
beginPos,
currentPos.copy(),
tokenBuffer
);
}
private HjsonLexerToken readQuotelessLiteral(int codePoint) throws TweedDataReadException {
if (codePoint == 'n') {
return readConstantOrQuotelessString(codePoint, "null", HjsonLexerToken.Type.NULL);
} else if (codePoint == 't') {
return readConstantOrQuotelessString(codePoint, "true", HjsonLexerToken.Type.TRUE);
} else if (codePoint == 'f') {
return readConstantOrQuotelessString(codePoint, "false", HjsonLexerToken.Type.FALSE);
} else if (codePoint == '-' || isDigit(codePoint)) {
return readNumberLiteralOrQuotelessString(codePoint);
} else {
StringBuilder tokenBuffer = new StringBuilder();
tokenBuffer.appendCodePoint(codePoint);
return readQuotelessStringToEndOfLine(currentPos.copy(), tokenBuffer);
}
}
private HjsonLexerToken readConstantOrQuotelessString(
int firstCodePoint,
String rest,
HjsonLexerToken.Type tokenType
) throws TweedDataReadException {
HjsonReadPosition beginPos = currentPos.copy();
StringBuilder tokenBuffer = new StringBuilder();
tokenBuffer.appendCodePoint(firstCodePoint);
PrimitiveIterator.OfInt restIterator = rest.codePoints().iterator();
restIterator.nextInt(); // skip first, as already checked and consumed
while (restIterator.hasNext()) {
int codePoint = eatCodePoint();
tokenBuffer.appendCodePoint(codePoint);
if (codePoint != restIterator.nextInt()) {
return readQuotelessStringToEndOfLine(beginPos, tokenBuffer);
}
}
return chompAfterLiteralOrReadToQuotelessString(tokenType, beginPos, tokenBuffer);
}
private HjsonLexerToken readNumberLiteralOrQuotelessString(int firstCodePoint) throws TweedDataReadException {
HjsonReadPosition beginPos = currentPos.copy();
StringBuilder tokenBuffer = new StringBuilder();
tokenBuffer.appendCodePoint(firstCodePoint);
int codePoint = firstCodePoint;
if (codePoint == '-') {
codePoint = eatCodePoint();
if (codePoint == -1) {
throw TweedDataReadException.builder().message("Unexpected end of number at " + currentPos).build();
}
tokenBuffer.appendCodePoint(codePoint);
}
if (!isDigit(codePoint)) {
return readQuotelessStringToEndOfLine(beginPos, tokenBuffer);
}
boolean startsWithZero = codePoint == '0';
codePoint = peekCodePoint();
if (startsWithZero && isDigit(codePoint)) {
return readQuotelessStringToEndOfLine(beginPos, tokenBuffer);
}
eatManyDigitsToBuffer(tokenBuffer);
if (peekCodePoint() == '.') {
tokenBuffer.appendCodePoint(eatCodePoint());
codePoint = eatCodePoint();
tokenBuffer.appendCodePoint(codePoint);
if (!isDigit(codePoint)) {
return readQuotelessStringToEndOfLine(beginPos, tokenBuffer);
}
eatManyDigitsToBuffer(tokenBuffer);
}
if (peekCodePoint() == 'e' || peekCodePoint() == 'E') {
tokenBuffer.appendCodePoint(eatCodePoint());
codePoint = eatCodePoint();
tokenBuffer.appendCodePoint(codePoint);
if (codePoint == '+' || codePoint == '-') {
codePoint = eatCodePoint();
tokenBuffer.appendCodePoint(codePoint);
}
if (!isDigit(codePoint)) {
return readQuotelessStringToEndOfLine(beginPos, tokenBuffer);
}
eatManyDigitsToBuffer(tokenBuffer);
}
return chompAfterLiteralOrReadToQuotelessString(HjsonLexerToken.Type.NUMBER, beginPos, tokenBuffer);
}
private void eatManyDigitsToBuffer(StringBuilder buffer) throws TweedDataReadException {
while (true) {
int codePoint = peekCodePoint();
if (!isDigit(codePoint)) {
break;
}
buffer.appendCodePoint(eatCodePoint());
}
}
private HjsonLexerToken chompAfterLiteralOrReadToQuotelessString(
HjsonLexerToken.Type tokenType,
HjsonReadPosition beginPos,
StringBuilder tokenBuffer
) throws TweedDataReadException {
int literalEndLength = tokenBuffer.length();
HjsonReadPosition literalEndPos = currentPos.copy();
while (true) {
int peek = peekCodePoint();
if (peek == -1 || peek == ',' || peek == '\n' || peek == '#' || peek == ']' || peek == '}') {
tokenBuffer.setLength(literalEndLength);
return new HjsonLexerToken(tokenType, beginPos, literalEndPos, tokenBuffer);
} else if (peek == '/') {
int peek2 = peek2CodePoint();
if (peek2 == '/' || peek2 == '*') {
tokenBuffer.setLength(literalEndLength);
return new HjsonLexerToken(tokenType, beginPos, literalEndPos, tokenBuffer);
} else {
return readQuotelessStringToEndOfLine(beginPos, tokenBuffer);
}
} else if (!isInlineWhitespace(peek)) {
return readQuotelessStringToEndOfLine(beginPos, tokenBuffer);
}
tokenBuffer.appendCodePoint(eatCodePoint());
}
}
private HjsonLexerToken readQuotelessStringToEndOfLine(
HjsonReadPosition beginPos,
StringBuilder tokenBuffer
) throws TweedDataReadException {
int lastNonWhitespaceLength = tokenBuffer.length();
while (true) {
int codePoint = peekCodePoint();
if (codePoint == -1 || codePoint == '\n') {
tokenBuffer.setLength(lastNonWhitespaceLength);
return new HjsonLexerToken(
HjsonLexerToken.Type.QUOTELESS_STRING,
beginPos,
currentPos.copy(),
tokenBuffer
);
} else {
tokenBuffer.appendCodePoint(eatCodePoint());
if (!isInlineWhitespace(codePoint)) {
lastNonWhitespaceLength = tokenBuffer.length();
}
}
}
}
private void chompMultilineStringIndent(int count) throws TweedDataReadException {
for (int i = 0; i < count; i++) {
int codePoint = eatCodePoint();
if (codePoint == -1) {
return;
} else if (!isInlineWhitespace(codePoint)) {
throw TweedDataReadException.builder().message("Illegal indent at " + currentPos + ", expected " + count + " whitespace characters").build();
}
}
}
private void chompInlineWhitespaceAndComments() throws TweedDataReadException {
while (true) {
int peek = peekCodePoint();
if (isInlineWhitespace(peek)) {
eatCodePoint();
} else if (peek == '#') {
eatCodePoint();
chompToEndOfLine();
} else if (peek == '/') {
int peek2 = peek2CodePoint();
if (peek2 == '/') {
eatCodePoint();
eatCodePoint();
chompToEndOfLine();
} else if (peek2 == '*') {
eatCodePoint();
eatCodePoint();
chompToEndOfBlockComment();
}
} else {
break;
}
}
}
private void chompToEndOfLine() throws TweedDataReadException {
while (true) {
int codePoint = eatCodePoint();
if (codePoint == -1 || codePoint == '\n') {
break;
}
}
}
private void chompToEndOfBlockComment() throws TweedDataReadException {
boolean lastWasAsterisk = false;
while (true) {
int codePoint = eatCodePoint();
if (codePoint == -1) {
throw TweedDataReadException.builder().message("Unterminated block comment at end of file " + currentPos).build();
} else if (codePoint == '*') {
lastWasAsterisk = true;
} else if (lastWasAsterisk && codePoint == '/') {
break;
}
}
}
private boolean isPunctuator(int codePoint) {
return codePoint == ',' || codePoint == ':' || codePoint == '[' || codePoint == ']' || codePoint == '{' || codePoint == '}';
}
private boolean isDigit(int codePoint) {
return codePoint >= '0' && codePoint <= '9';
}
private boolean isInlineWhitespace(int codePoint) {
return codePoint == ' ' || codePoint == '\t' || codePoint == '\r';
}
private int peek2CodePoint() throws TweedDataReadException {
if (peeked2CodePoint == EMPTY_CODEPOINT) {
if (peekedCodePoint == EMPTY_CODEPOINT) {
peekedCodePoint = readCodePoint();
}
peeked2CodePoint = readCodePoint();
}
return peeked2CodePoint;
}
private int peekCodePoint() throws TweedDataReadException {
if (peekedCodePoint == EMPTY_CODEPOINT) {
peekedCodePoint = readCodePoint();
}
return peekedCodePoint;
}
private int eatCodePoint() throws TweedDataReadException {
int codePoint;
if (peekedCodePoint != EMPTY_CODEPOINT) {
codePoint = peekedCodePoint;
peekedCodePoint = peeked2CodePoint;
peeked2CodePoint = EMPTY_CODEPOINT;
} else {
codePoint = readCodePoint();
}
if (codePoint == '\n') {
currentPos.nextLine();
} else {
currentPos.nextCodepoint();
}
return codePoint;
}
private int readCodePoint() throws TweedDataReadException {
try {
return reader.read();
} catch (IOException e) {
throw TweedDataReadException.builder().message("Failed to read character from input at " + currentPos).cause(e).build();
}
}
}

View File

@@ -0,0 +1,61 @@
package de.siphalor.tweed5.data.hjson;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
@ApiStatus.Internal
@Value
public class HjsonLexerToken {
Type type;
HjsonReadPosition begin;
HjsonReadPosition end;
@EqualsAndHashCode.Exclude
@Nullable
CharSequence content;
@EqualsAndHashCode.Include
@Nullable
public String contentString() {
return content == null ? null : content.toString();
}
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(type.toString());
if (content != null) {
stringBuilder.append(" (\"");
stringBuilder.append(content);
stringBuilder.append("\")");
}
stringBuilder.append(" at ");
stringBuilder.append(begin);
if (!begin.equals(end)) {
stringBuilder.append(" to ");
stringBuilder.append(end);
}
return stringBuilder.toString();
}
enum Type {
EOF,
BRACKET_OPEN,
BRACKET_CLOSE,
BRACE_OPEN,
BRACE_CLOSE,
COMMA,
COLON,
LINE_FEED,
NULL,
TRUE,
FALSE,
NUMBER,
QUOTELESS_STRING,
JSON_STRING,
MULTILINE_STRING,
}
}

View File

@@ -0,0 +1,32 @@
package de.siphalor.tweed5.data.hjson;
import lombok.*;
import org.jetbrains.annotations.ApiStatus;
@ApiStatus.Internal
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@Getter
class HjsonReadPosition {
private int line = 1;
private int index;
public void nextCodepoint() {
index++;
}
public void nextLine() {
line++;
index = 0;
}
public HjsonReadPosition copy() {
return new HjsonReadPosition(line, index);
}
@Override
public String toString() {
return line + ":" + index;
}
}

View File

@@ -0,0 +1,642 @@
package de.siphalor.tweed5.data.hjson;
import de.siphalor.tweed5.dataapi.api.*;
import java.util.*;
import java.util.stream.Collectors;
public class HjsonReader implements TweedDataReader {
private final HjsonLexer lexer;
private final Deque<Context> contexts;
private State state = State.BEFORE_VALUE;
private HjsonLexerToken peekedLexerToken;
private TweedDataToken peekedToken;
public HjsonReader(HjsonLexer lexer) {
this.lexer = lexer;
this.contexts = new LinkedList<>();
this.contexts.push(Context.VALUE);
}
@Override
public TweedDataToken peekToken() throws TweedDataReadException {
if (peekedToken == null) {
peekedToken = nextToken();
}
return peekedToken;
}
@Override
public TweedDataToken readToken() throws TweedDataReadException {
if (peekedToken != null) {
TweedDataToken token = peekedToken;
peekedToken = null;
return token;
}
return nextToken();
}
private TweedDataToken nextToken() throws TweedDataReadException {
Context currentContext = currentContext();
switch (currentContext) {
case OBJECT:
return nextObjectToken();
case LIST:
return nextListToken();
case VALUE:
return nextValueToken();
}
// unreachable
throw new IllegalStateException("Illegal context " + currentContext);
}
private TweedDataToken nextObjectToken() throws TweedDataReadException {
if (state == State.AFTER_OBJECT_KEY) {
chompLineFeedTokensInGeneral();
HjsonLexerToken lexerToken = eatGeneralLexerToken();
if (lexerToken.type() == HjsonLexerToken.Type.COLON) {
state = State.BEFORE_VALUE;
} else {
throw createIllegalTokenException(lexerToken, HjsonLexerToken.Type.COLON);
}
}
if (state == State.BEFORE_VALUE) {
return TweedDataTokens.asMapEntryValue(nextValueToken());
}
if (state == State.AFTER_VALUE) {
HjsonLexerToken lexerToken = eatGeneralLexerToken();
if (lexerToken.type() == HjsonLexerToken.Type.BRACE_CLOSE) {
contexts.pop();
state = State.AFTER_VALUE;
return TweedDataTokens.getMapEnd();
} else if (lexerToken.type() == HjsonLexerToken.Type.LINE_FEED || lexerToken.type() == HjsonLexerToken.Type.COMMA) {
state = State.BEFORE_OBJECT_KEY;
} else {
throw createIllegalTokenException(lexerToken, HjsonLexerToken.Type.BRACE_CLOSE, HjsonLexerToken.Type.LINE_FEED, HjsonLexerToken.Type.COMMA);
}
}
if (state == State.BEFORE_OBJECT_KEY) {
chompLineFeedTokensInObject();
HjsonLexerToken lexerToken = eatObjectLexerToken();
if (lexerToken.type() == HjsonLexerToken.Type.BRACE_CLOSE) {
contexts.pop();
state = State.AFTER_VALUE;
return TweedDataTokens.getMapEnd();
} else if (lexerToken.type() == HjsonLexerToken.Type.QUOTELESS_STRING || lexerToken.type() == HjsonLexerToken.Type.JSON_STRING) {
state = State.AFTER_OBJECT_KEY;
return TweedDataTokens.asMapEntryKey(createStringToken(lexerToken));
} else {
throw createIllegalTokenException(lexerToken, HjsonLexerToken.Type.BRACE_CLOSE, HjsonLexerToken.Type.QUOTELESS_STRING, HjsonLexerToken.Type.JSON_STRING);
}
}
throw createIllegalStateException();
}
private TweedDataToken nextListToken() throws TweedDataReadException {
if (state == State.AFTER_VALUE) {
HjsonLexerToken lexerToken = eatGeneralLexerToken();
if (lexerToken.type() == HjsonLexerToken.Type.BRACKET_CLOSE) {
contexts.pop();
state = State.AFTER_VALUE;
return TweedDataTokens.getListEnd();
} else if (lexerToken.type() == HjsonLexerToken.Type.COMMA || lexerToken.type() == HjsonLexerToken.Type.LINE_FEED) {
state = State.BEFORE_VALUE;
} else {
throw createIllegalTokenException(lexerToken, HjsonLexerToken.Type.BRACKET_CLOSE, HjsonLexerToken.Type.COMMA, HjsonLexerToken.Type.LINE_FEED);
}
}
if (state == State.BEFORE_VALUE) {
chompLineFeedTokensInGeneral();
HjsonLexerToken lexerToken = peekGeneralLexerToken();
if (lexerToken.type() == HjsonLexerToken.Type.BRACKET_CLOSE) {
eatGeneralLexerToken();
contexts.pop();
state = State.AFTER_VALUE;
return TweedDataTokens.getListEnd();
}
return TweedDataTokens.asListValue(nextValueToken());
}
throw createIllegalStateException();
}
private TweedDataToken nextValueToken() throws TweedDataReadException {
chompLineFeedTokensInGeneral();
HjsonLexerToken lexerToken = eatGeneralLexerToken();
switch (lexerToken.type()) {
case NULL:
state = State.AFTER_VALUE;
return TweedDataTokens.getNull();
case TRUE:
case FALSE:
state = State.AFTER_VALUE;
return createBooleanToken(lexerToken);
case NUMBER:
state = State.AFTER_VALUE;
return createNumberToken(lexerToken);
case QUOTELESS_STRING:
case JSON_STRING:
case MULTILINE_STRING:
state = State.AFTER_VALUE;
return createStringToken(lexerToken);
case BRACKET_OPEN:
state = State.BEFORE_VALUE;
contexts.push(Context.LIST);
return TweedDataTokens.getListStart();
case BRACE_OPEN:
state = State.BEFORE_OBJECT_KEY;
contexts.push(Context.OBJECT);
return TweedDataTokens.getMapStart();
default:
throw createIllegalTokenException(
lexerToken,
HjsonLexerToken.Type.NULL,
HjsonLexerToken.Type.TRUE,
HjsonLexerToken.Type.FALSE,
HjsonLexerToken.Type.NUMBER,
HjsonLexerToken.Type.QUOTELESS_STRING,
HjsonLexerToken.Type.JSON_STRING,
HjsonLexerToken.Type.MULTILINE_STRING,
HjsonLexerToken.Type.BRACKET_OPEN,
HjsonLexerToken.Type.BRACE_OPEN
);
}
}
private void chompLineFeedTokensInGeneral() throws TweedDataReadException {
while (peekGeneralLexerToken().type() == HjsonLexerToken.Type.LINE_FEED) {
eatGeneralLexerToken();
}
}
private void chompLineFeedTokensInObject() throws TweedDataReadException {
while (peekObjectLexerToken().type() == HjsonLexerToken.Type.LINE_FEED) {
eatObjectLexerToken();
}
}
private TweedDataToken createBooleanToken(HjsonLexerToken lexerToken) {
return new TweedDataToken() {
@Override
public boolean canReadAsBoolean() {
return true;
}
@Override
public boolean readAsBoolean() {
return lexerToken.type() == HjsonLexerToken.Type.TRUE;
}
@Override
public String toString() {
return "HJSON boolean token [" + lexerToken + "]";
}
};
}
private TweedDataToken createNumberToken(HjsonLexerToken lexerToken) {
assert lexerToken.content() != null;
return new TweedDataToken() {
private Long tryLong;
private Double tryDouble;
private boolean fraction;
private boolean mantissaTooLarge;
private boolean exponentTooLarge;
@Override
public boolean canReadAsByte() {
tryReadLong();
return isValidIntegerValue(Byte.MIN_VALUE, Byte.MAX_VALUE);
}
@Override
public byte readAsByte() throws TweedDataReadException {
tryReadLong();
requireValidIntegerValue(Byte.MIN_VALUE, Byte.MAX_VALUE);
return tryLong.byteValue();
}
@Override
public boolean canReadAsShort() {
tryReadLong();
return isValidIntegerValue(Short.MIN_VALUE, Short.MAX_VALUE);
}
@Override
public short readAsShort() throws TweedDataReadException {
tryReadLong();
requireValidIntegerValue(Short.MIN_VALUE, Short.MAX_VALUE);
return tryLong.shortValue();
}
@Override
public boolean canReadAsInt() {
tryReadLong();
return isValidIntegerValue(Integer.MIN_VALUE, Integer.MAX_VALUE);
}
@Override
public int readAsInt() throws TweedDataReadException {
tryReadLong();
requireValidIntegerValue(Integer.MIN_VALUE, Integer.MAX_VALUE);
return tryLong.intValue();
}
@Override
public boolean canReadAsLong() {
tryReadLong();
return !mantissaTooLarge && !exponentTooLarge && !fraction;
}
@Override
public long readAsLong() throws TweedDataReadException {
tryReadLong();
requireValidIntegerValue(Long.MIN_VALUE, Long.MAX_VALUE);
return tryLong;
}
private boolean isValidIntegerValue(long min, long max) {
return !mantissaTooLarge && !exponentTooLarge && !fraction && tryLong != null && tryLong >= min && tryLong <= max;
}
private void requireValidIntegerValue(long min, long max) throws TweedDataReadException {
if (mantissaTooLarge) {
throw TweedDataReadException.builder()
.message("Mantissa of number is too large! (" + lexerToken + ")")
.recoverable(TweedDataReaderRecoverMode.SKIP)
.build();
}
if (exponentTooLarge) {
throw TweedDataReadException.builder()
.message("Exponent of number is too large! (" + lexerToken + ")")
.recoverable(TweedDataReaderRecoverMode.SKIP)
.build();
}
if (fraction) {
throw TweedDataReadException.builder()
.message("Fractional number cannot be read as non-fractional! (" + lexerToken + ")")
.recoverable(TweedDataReaderRecoverMode.SKIP)
.build();
}
if (tryLong < min) {
throw TweedDataReadException.builder()
.message("Number is too low for data type, minimum is " + min + " at " + lexerToken)
.recoverable(TweedDataReaderRecoverMode.SKIP)
.build();
}
if (tryLong > max) {
throw TweedDataReadException.builder()
.message("Number is too large for data type, maximum is " + max + " at " + lexerToken)
.recoverable(TweedDataReaderRecoverMode.SKIP)
.build();
}
}
private void tryReadLong() {
if (tryLong != null) {
return;
}
PrimitiveIterator.OfInt iterator = lexerToken.content().codePoints().iterator();
long sign = 1;
int codePoint = iterator.nextInt();
if (codePoint == '-') {
sign = -1;
codePoint = iterator.nextInt();
}
int fractionDigits = 0;
try {
tryLong = 0L;
boolean inFraction = false;
do {
tryLong = Math.addExact(Math.multiplyExact(tryLong, 10L), (long) (codePoint - '0'));
if (inFraction) {
fractionDigits++;
}
if (!iterator.hasNext()) {
tryLong *= sign;
if (fractionDigits > 0) {
fraction = true;
}
return;
}
codePoint = iterator.nextInt();
if (!inFraction && codePoint == '.') {
inFraction = true;
codePoint = iterator.nextInt();
}
} while (isDigit(codePoint));
tryLong *= sign;
} catch (ArithmeticException ignored) {
mantissaTooLarge = true;
return;
}
int exponent = 0;
if (codePoint == 'e' || codePoint == 'E') {
codePoint = iterator.nextInt();
boolean negativeExponent = false;
if (codePoint == '+') {
codePoint = iterator.nextInt();
} else if (codePoint == '-') {
codePoint = iterator.nextInt();
negativeExponent = true;
}
try {
while (true) {
exponent = Math.addExact(Math.multiplyExact(exponent, 10), codePoint - '0');
if (!iterator.hasNext()) {
break;
}
codePoint = iterator.nextInt();
}
if (negativeExponent) {
exponent = -exponent;
}
} catch (ArithmeticException ignored) {
exponentTooLarge = true;
}
}
exponent -= fractionDigits;
applyLongExponent(exponent);
}
private void applyLongExponent(int exponent) {
if (exponent < 0) {
long factor = 1L;
while (exponent < 0) {
factor *= 10L;
exponent++;
}
if (tryLong != tryLong / factor * factor) {
fraction = true;
return;
}
tryLong /= factor;
} else {
try {
while (exponent > 0) {
tryLong = Math.multiplyExact(tryLong, 10L);
exponent--;
}
} catch (ArithmeticException ignored) {
exponentTooLarge = true;
}
}
}
@Override
public boolean canReadAsFloat() {
tryReadDouble();
return Float.isFinite(tryDouble.floatValue());
}
@Override
public float readAsFloat() throws TweedDataReadException {
tryReadDouble();
float value = tryDouble.floatValue();
if (Float.isInfinite(value)) {
throw TweedDataReadException.builder()
.message("Number is out of range from " + (-Float.MAX_VALUE) + " to " + Float.MAX_VALUE + " at " + lexerToken)
.recoverable(TweedDataReaderRecoverMode.SKIP)
.build();
}
return value;
}
@Override
public boolean canReadAsDouble() {
tryReadDouble();
return Double.isFinite(tryDouble);
}
@Override
public double readAsDouble() throws TweedDataReadException {
tryReadDouble();
if (Double.isInfinite(tryDouble)) {
throw TweedDataReadException.builder()
.message("Number is out of range form " + (-Double.MAX_VALUE) + " to " + Double.MAX_VALUE + " at " + lexerToken)
.recoverable(TweedDataReaderRecoverMode.SKIP)
.build();
}
return tryDouble;
}
private void tryReadDouble() {
if (tryDouble != null) {
return;
}
boolean negative = false;
PrimitiveIterator.OfInt iterator = lexerToken.content().codePoints().iterator();
int codePoint = iterator.nextInt();
if (codePoint == '-') {
negative = true;
codePoint = iterator.nextInt();
}
double value = 0;
while (isDigit(codePoint)) {
value = value * 10 + (codePoint - '0');
if (!iterator.hasNext()) {
tryDouble = negative ? -1D * value : value;
return;
}
codePoint = iterator.nextInt();
}
if (codePoint == '.') {
double factor = 0.1;
do {
codePoint = iterator.nextInt();
if (!isDigit(codePoint)) {
break;
}
value += factor * (codePoint - '0');
factor /= 10;
} while (iterator.hasNext());
}
if (codePoint == 'e' || codePoint == 'E') {
codePoint = iterator.nextInt();
double factor = 10D;
if (codePoint == '-') {
factor = 0.1D;
codePoint = iterator.nextInt();
} else if (codePoint == '+') {
codePoint = iterator.nextInt();
}
double exponent = 0D;
while (isDigit(codePoint)) {
exponent = exponent * 10 + (codePoint - '0');
if (!iterator.hasNext()) {
break;
}
codePoint = iterator.nextInt();
}
factor = Math.pow(factor, exponent);
value *= factor;
}
tryDouble = value;
}
private boolean isDigit(int codePoint) {
return codePoint >= '0' && codePoint <= '9';
}
@Override
public String toString() {
return "HJSON numeric token [" + lexerToken + "]";
}
};
}
private TweedDataToken createStringToken(HjsonLexerToken lexerToken) {
assert lexerToken.content() != null;
return new TweedDataToken() {
@Override
public boolean canReadAsString() {
return true;
}
@Override
public String readAsString() throws TweedDataReadException {
if (lexerToken.type() == HjsonLexerToken.Type.QUOTELESS_STRING || lexerToken.type() == HjsonLexerToken.Type.MULTILINE_STRING) {
return lexerToken.contentString();
} else if (lexerToken.type() == HjsonLexerToken.Type.JSON_STRING) {
return readJsonString(lexerToken.content());
}
throw TweedDataReadException.builder().message("Unrecognized string token").recoverable(TweedDataReaderRecoverMode.SKIP).build();
}
private String readJsonString(CharSequence input) throws TweedDataReadException {
PrimitiveIterator.OfInt iterator = input.codePoints().iterator();
int quoteCodePoint = iterator.nextInt();
boolean escaped = false;
StringBuilder stringBuilder = new StringBuilder();
while (true) {
int codePoint = iterator.nextInt();
if (escaped) {
escaped = false;
codePoint = getUnescapedCodePoint(codePoint);
} else if (codePoint == quoteCodePoint) {
break;
} else if (codePoint == '\\') {
escaped = true;
}
stringBuilder.appendCodePoint(codePoint);
}
return stringBuilder.toString();
}
private int getUnescapedCodePoint(int codePoint) throws TweedDataReadException {
switch (codePoint) {
case 'n':
return '\n';
case 'r':
return '\r';
case 't':
return '\t';
case 'f':
return '\f';
case 'b':
return '\b';
case '\\':
case '/':
case '"':
case '\'':
return codePoint;
default:
throw TweedDataReadException.builder()
.message("Illegal escape sequence \"\\" + String.copyValueOf(Character.toChars(codePoint)) + "\" in string " + lexerToken)
.recoverable(TweedDataReaderRecoverMode.SKIP)
.build();
}
}
@Override
public String toString() {
return "HJSON string token [" + lexerToken + "]";
}
};
}
private TweedDataReadException createIllegalTokenException(
HjsonLexerToken actualToken,
HjsonLexerToken.Type... expected
) {
return TweedDataReadException.builder().message(
"Illegal token " + actualToken + ", expected any of " +
Arrays.stream(expected).map(Objects::toString).collect(Collectors.joining(", "))
).build();
}
private TweedDataReadException createIllegalStateException() {
return TweedDataReadException.builder().message(
"Internal Error: Parser is in illegal state " + state + " in context " + currentContext()
).build();
}
private HjsonLexerToken peekGeneralLexerToken() throws TweedDataReadException {
if (peekedLexerToken == null) {
peekedLexerToken = lexer.nextGeneralToken();
}
return peekedLexerToken;
}
private HjsonLexerToken peekObjectLexerToken() throws TweedDataReadException {
if (peekedLexerToken == null) {
peekedLexerToken = lexer.nextInnerObjectToken();
}
return peekedLexerToken;
}
private HjsonLexerToken eatGeneralLexerToken() throws TweedDataReadException {
if (peekedLexerToken != null) {
HjsonLexerToken token = peekedLexerToken;
peekedLexerToken = null;
return token;
}
return lexer.nextGeneralToken();
}
private HjsonLexerToken eatObjectLexerToken() throws TweedDataReadException {
if (peekedLexerToken != null) {
HjsonLexerToken token = peekedLexerToken;
peekedLexerToken = null;
return token;
}
return lexer.nextInnerObjectToken();
}
private Context currentContext() {
return contexts.peek();
}
private enum Context {
VALUE,
LIST,
OBJECT,
}
private enum State {
BEFORE_VALUE,
AFTER_VALUE,
BEFORE_OBJECT_KEY,
AFTER_OBJECT_KEY,
}
}

View File

@@ -0,0 +1,26 @@
package de.siphalor.tweed5.data.hjson;
import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
import de.siphalor.tweed5.dataapi.api.TweedSerde;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class HjsonSerde implements TweedSerde {
@Override
public TweedDataReader createReader(InputStream inputStream) {
return null;
}
@Override
public TweedDataVisitor createWriter(OutputStream outputStream) throws IOException {
return null;
}
@Override
public String getPreferredFileExtension() {
return "";
}
}

View File

@@ -0,0 +1,8 @@
package de.siphalor.tweed5.data.hjson;
public enum HjsonStringType {
INLINE_QUOTELESS,
INLINE_DOUBLE_QUOTE,
INLINE_SINGLE_QUOTE,
MULTILINE_SINGLE_QUOTE,
}

View File

@@ -0,0 +1,595 @@
package de.siphalor.tweed5.data.hjson;
import de.siphalor.tweed5.dataapi.api.TweedDataWriteException;
import de.siphalor.tweed5.dataapi.api.TweedDataWriter;
import lombok.Data;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.Writer;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class HjsonWriter implements TweedDataWriter {
private static final int PREFILL_INDENT = 10;
private static final Pattern LINE_FEED_PATTERN = Pattern.compile("\\n|\\r\\n");
private final Writer writer;
private final Options options;
private final Deque<Context> contexts;
private final StringBuilder indentBuffer;
private int currentIndentLevel;
private int currentIndentLength;
public HjsonWriter(Writer writer, Options options) {
this.writer = writer;
this.options = options;
contexts = new LinkedList<>(Collections.singleton(Context.ROOT));
indentBuffer = new StringBuilder(options.indent.length() * PREFILL_INDENT);
for (int i = 0; i < PREFILL_INDENT; i++) {
indentBuffer.append(options.indent);
}
}
@Override
public void visitNull() {
beforeValueWrite();
write("null");
afterValueWrite();
}
@Override
public void visitBoolean(boolean value) {
beforeValueWrite();
write(value ? "true" : "false");
afterValueWrite();
}
@Override
public void visitByte(byte value) {
beforeValueWrite();
write(Byte.toString(value));
afterValueWrite();
}
@Override
public void visitShort(short value) {
beforeValueWrite();
write(Short.toString(value));
afterValueWrite();
}
@Override
public void visitInt(int value) {
beforeValueWrite();
write(Integer.toString(value));
afterValueWrite();
}
@Override
public void visitLong(long value) {
beforeValueWrite();
write(Long.toString(value));
afterValueWrite();
}
@Override
public void visitFloat(float value) {
beforeValueWrite();
write(Float.toString(value));
afterValueWrite();
}
@Override
public void visitDouble(double value) {
beforeValueWrite();
write(Double.toString(value));
afterValueWrite();
}
@Override
public void visitString(@NotNull String value) {
beforeValueWrite();
writeStringValue(getValueStringStringType(value), value);
afterValueWrite();
}
private HjsonStringType getValueStringStringType(String value) {
if (value.isEmpty() || "true".equals(value) || "false".equals(value) || "null".equals(value)) {
return HjsonStringType.INLINE_DOUBLE_QUOTE;
}
int firstCodePoint = value.codePointAt(0);
if (Character.isDigit(firstCodePoint) || Character.isWhitespace(firstCodePoint)) {
return HjsonStringType.INLINE_DOUBLE_QUOTE;
}
int lastCodePoint = value.codePointBefore(value.length());
if (Character.isWhitespace(lastCodePoint)) {
return HjsonStringType.INLINE_DOUBLE_QUOTE;
}
boolean singleQuoteFound = false;
boolean doubleQuoteFound = false;
int singleQuoteCount = 0;
boolean tripleSingleQuoteFound = false;
boolean punctuatorFound = false;
boolean newLineFound = false;
boolean escapeRequiredFound = false;
boolean tabFound = false;
PrimitiveIterator.OfInt codePointIterator = value.codePoints().iterator();
while (codePointIterator.hasNext()) {
int codePoint = codePointIterator.nextInt();
if (codePoint == '\'') {
singleQuoteFound = true;
if (++singleQuoteCount >= 3) {
tripleSingleQuoteFound = true;
}
} else {
singleQuoteCount = 0;
if (codePoint == '"') {
doubleQuoteFound = true;
} else if (codePoint == '\n' || codePoint == '\r') {
newLineFound = true;
} else if (!punctuatorFound && isPunctuatorCodePoint(codePoint)) {
punctuatorFound = true;
} else if (codePoint == '\t') {
tabFound = true;
} else if (codePoint == '\\' || codePoint < 0x20) {
escapeRequiredFound = true;
}
}
}
if (!punctuatorFound && !newLineFound && !tabFound && !escapeRequiredFound) {
return HjsonStringType.INLINE_QUOTELESS;
}
if (newLineFound && !tripleSingleQuoteFound) {
return HjsonStringType.MULTILINE_SINGLE_QUOTE;
}
if (singleQuoteFound || !doubleQuoteFound) {
return HjsonStringType.INLINE_DOUBLE_QUOTE;
}
return HjsonStringType.INLINE_SINGLE_QUOTE;
}
@Override
public void visitEmptyList() {
beforeValueWrite();
write("[]");
afterValueWrite();
}
@Override
public void visitListStart() {
beforeValueWrite();
write("[");
writeLineFeed();
pushContext(Context.LIST);
}
@Override
public void visitListEnd() {
requireContext(Context.LIST);
popContext();
writeCurrentIndent();
write("]");
afterValueWrite();
}
@Override
public void visitEmptyMap() {
beforeValueWrite();
write("{}");
afterValueWrite();
}
@Override
public void visitMapStart() {
beforeValueWrite();
write("{");
writeLineFeed();
pushContext(Context.MAP);
}
@Override
public void visitMapEntryKey(String key) {
requireContext(Context.MAP);
writeCurrentIndent();
writeStringValue(getMapEntryKeyStringType(key), key);
write(": ");
pushContext(Context.MAP_ENTRY);
}
private HjsonStringType getMapEntryKeyStringType(String key) {
int firstCodePoint = key.codePointAt(0);
if (firstCodePoint == '\'') {
return HjsonStringType.INLINE_DOUBLE_QUOTE;
} else if (firstCodePoint == '"'){
return HjsonStringType.INLINE_SINGLE_QUOTE;
}
if (key.codePoints().allMatch(this::isValidMapEntryKeyCodePoint)) {
return HjsonStringType.INLINE_QUOTELESS;
}
return HjsonStringType.INLINE_DOUBLE_QUOTE;
}
private boolean isValidMapEntryKeyCodePoint(int codePoint) {
if (codePoint < 0x21) {
return false;
}
return !isPunctuatorCodePoint(codePoint);
}
private boolean isPunctuatorCodePoint(int codePoint) {
return codePoint == ',' || codePoint == ':' || codePoint == '[' || codePoint == ']' || codePoint == '{' || codePoint == '}';
}
@Override
public void visitMapEnd() {
requireContext(Context.MAP);
popContext();
writeCurrentIndent();
write("}");
afterValueWrite();
}
@Override
public void visitComment(String comment) {
Matcher lineFeedMatcher = LINE_FEED_PATTERN.matcher(comment);
if (lineFeedMatcher.find()) {
// Multiline
writeMultilineCommentStart(options.multilineCommentType);
int begin = 0;
do {
writeCommentLine(options.multilineCommentType, comment, begin, lineFeedMatcher.start());
begin = lineFeedMatcher.end();
} while (lineFeedMatcher.find(begin));
writeCommentLine(options.multilineCommentType, comment, begin, comment.length());
writeMultilineCommentEnd(options.multilineCommentType);
} else {
// Inline
writeMultilineCommentStart(options.inlineCommentType);
writeCommentLine(options.inlineCommentType, comment, 0, comment.length());
writeMultilineCommentEnd(options.inlineCommentType);
}
}
private void writeMultilineCommentStart(HjsonCommentType commentType) {
if (commentType == HjsonCommentType.BLOCK) {
writeCurrentIndentIfApplicable();
write("/*");
writeLineFeed();
}
}
private void writeMultilineCommentEnd(HjsonCommentType commentType) {
if (commentType == HjsonCommentType.BLOCK) {
writeCurrentIndent();
write(" */");
writeLineFeed();
if (isInInlineContext()) {
writeCurrentIndent();
}
}
}
private void writeCommentLine(HjsonCommentType commentType, CharSequence text, int begin, int end) {
writeCurrentIndentIfApplicable();
write(getCommentLineStart(commentType));
write(text, begin, end);
writeLineFeed();
}
private CharSequence getCommentLineStart(HjsonCommentType commentType) {
switch (commentType) {
case HASH:
return "# ";
case SLASHES:
return "// ";
case BLOCK:
return " * ";
default:
throw new IllegalStateException("Unknown comment type: " + commentType);
}
}
private void beforeValueWrite() {
requireValueContext();
writeCurrentIndentIfApplicable();
}
private void afterValueWrite() {
switch (currentContext()) {
case ROOT:
case LIST:
writeLineFeed();
break;
case MAP_ENTRY:
popContext();
writeLineFeed();
break;
default:
break;
}
}
private void writeStringValue(HjsonStringType stringType, String text) {
switch (stringType) {
case INLINE_QUOTELESS:
write(text);
break;
case INLINE_DOUBLE_QUOTE:
writeJsonString(text, '"');
break;
case INLINE_SINGLE_QUOTE:
writeJsonString(text, '\'');
break;
case MULTILINE_SINGLE_QUOTE:
writeMultilineString(text);
break;
}
}
private void writeJsonString(String text, int quoteCodepoint) {
writeCodepoint(quoteCodepoint);
text.codePoints().forEach(codepoint -> {
if (codepoint == quoteCodepoint) {
write("\\");
writeCodepoint(codepoint);
} else {
writeJsonStringQuotePoint(codepoint);
}
});
writeCodepoint(quoteCodepoint);
}
private void writeJsonStringQuotePoint(int codepoint) {
switch (codepoint) {
case '\\':
write("\\\\");
break;
case '\b':
write("\\b");
break;
case '\f':
write("\\f");
break;
case '\n':
write("\\n");
break;
case '\r':
write("\\r");
break;
case '\t':
write("\\t");
break;
default:
if (isValidJsonStringCodepoint(codepoint)) {
writeCodepoint(codepoint);
} else {
write(codepointToHexEscape(codepoint));
}
break;
}
}
private String codepointToHexEscape(int codepoint) {
StringBuilder hexEscape = new StringBuilder("\\u0000");
hexEscape.replace(5, 6, nibbleToHex(codepoint & 0xF));
codepoint >>= 4;
hexEscape.replace(4, 5, nibbleToHex(codepoint & 0xF));
codepoint >>= 4;
hexEscape.replace(3, 4, nibbleToHex(codepoint & 0xF));
codepoint >>= 4;
hexEscape.replace(2, 3, nibbleToHex(codepoint & 0xF));
return hexEscape.toString();
}
private String nibbleToHex(int value) {
switch (value) {
case 0x0: return "0";
case 0x1: return "1";
case 0x2: return "2";
case 0x3: return "3";
case 0x4: return "4";
case 0x5: return "5";
case 0x6: return "6";
case 0x7: return "7";
case 0x8: return "8";
case 0x9: return "9";
case 0xA: return "A";
case 0xB: return "B";
case 0xC: return "C";
case 0xD: return "D";
case 0xE: return "E";
case 0xF: return "F";
default:
throw new IllegalArgumentException("Invalid nibble value");
}
}
private boolean isValidJsonStringCodepoint(int codepoint) {
return codepoint >= 0x20 && codepoint <= 0x10FFFF && codepoint != 0x21 && codepoint != 0x5C;
}
private void writeMultilineString(String text) {
boolean inInlineContext = isInInlineContext();
if (inInlineContext) {
writeLineFeed();
increaseIndent();
}
write("'''");
writeLineFeed();
Matcher matcher = LINE_FEED_PATTERN.matcher(text);
int begin = 0;
while (matcher.find(begin)) {
writeCurrentIndent();
write(text, begin, matcher.start());
writeLineFeed();
begin = matcher.end();
}
writeCurrentIndent();
write(text, begin, text.length());
writeLineFeed();
writeCurrentIndent();
write("'''");
if (inInlineContext) {
decreaseIndent();
}
}
private boolean isInInlineContext() {
return currentContext() == Context.MAP_ENTRY;
}
private void writeCurrentIndentIfApplicable() {
if (shouldWriteIndentInContext(currentContext())) {
writeCurrentIndent();
}
}
private boolean shouldWriteIndentInContext(Context context) {
return context == Context.ROOT || context == Context.LIST || context == Context.MAP;
}
private void requireValueContext() {
requireContext(Context.ROOT, Context.LIST, Context.MAP_ENTRY);
}
private void requireContext(Context... allowedContexts) {
Context currentContext = currentContext();
for (Context allowedContext : allowedContexts) {
if (currentContext == allowedContext) {
return;
}
}
throw new TweedDataWriteException(
"Writer is not in correct context, expected any of " + Arrays.toString(allowedContexts) +
" but currently in " + currentContext
);
}
private Context currentContext() {
Context currentContext = contexts.peek();
if (currentContext == null) {
throw new IllegalStateException("Writing has terminated");
}
return currentContext;
}
private void pushContext(Context context) {
switch (context) {
case ROOT:
throw new IllegalArgumentException("Root context may not be pushed");
case LIST:
case MAP:
increaseIndent();
break;
default:
break;
}
contexts.push(context);
}
private void popContext() {
switch (currentContext()) {
case LIST:
case MAP:
decreaseIndent();
break;
default:
break;
}
contexts.pop();
}
private void increaseIndent() {
currentIndentLevel++;
currentIndentLength = currentIndentLevel * options.indent.length();
ensureIndentBufferLength();
}
private void ensureIndentBufferLength() {
while (currentIndentLength > indentBuffer.length()) {
indentBuffer.append(options.indent);
}
}
private void decreaseIndent() {
if (currentIndentLevel == 0) {
throw new IllegalStateException("Cannot decrease indent level, already at 0");
}
currentIndentLevel--;
currentIndentLength = currentIndentLevel * options.indent.length();
}
private void writeCurrentIndent() {
write(indentBuffer, 0, currentIndentLength);
}
private void writeLineFeed() {
write(options.lineFeed);
}
private void write(CharSequence text, int begin, int end) {
try {
writer.append(text, begin, end);
} catch (IOException e) {
throw createExceptionForIOException(e);
}
}
private void write(CharSequence text) {
try {
writer.append(text);
} catch (IOException e) {
throw createExceptionForIOException(e);
}
}
private void writeCodepoint(int codepoint) {
try {
writer.write(codepoint);
} catch (IOException e) {
throw createExceptionForIOException(e);
}
}
private TweedDataWriteException createExceptionForIOException(IOException e) {
return new TweedDataWriteException("Writing Hjson failed", e);
}
private enum Context {
ROOT,
LIST,
MAP,
MAP_ENTRY,
}
@Data
public static class Options {
private boolean doubleQuotedInlineStrings = true;
private String indent = "\t";
private String lineFeed = "\n";
private HjsonCommentType inlineCommentType = HjsonCommentType.SLASHES;
private HjsonCommentType multilineCommentType = HjsonCommentType.BLOCK;
private HjsonStringType preferredInlineStringType = HjsonStringType.INLINE_QUOTELESS;
public void inlineCommentType(HjsonCommentType commentType) {
if (commentType.block()) {
throw new IllegalArgumentException("Inline comment type must not be a block comment type: " + commentType);
}
this.inlineCommentType = commentType;
}
}
}

View File

@@ -0,0 +1,141 @@
package de.siphalor.tweed5.data.hjson;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import java.io.StringReader;
import static org.junit.jupiter.api.Assertions.*;
@Timeout(value = 10, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
class HjsonLexerTest {
@Test
void generalEof() {
HjsonLexer lexer = createLexer("");
assertGeneralEof(lexer, new HjsonReadPosition(1, 1));
}
@Test
void innerObjectEof() {
HjsonLexer lexer = createLexer("");
assertEquals(
new HjsonLexerToken(
HjsonLexerToken.Type.EOF,
new HjsonReadPosition(1, 1),
new HjsonReadPosition(1, 1),
null
),
assertDoesNotThrow(lexer::nextGeneralToken)
);
}
@ParameterizedTest
@CsvSource(
delimiter = ';',
value = {
"[;BRACKET_OPEN",
"];BRACKET_CLOSE",
"{;BRACE_OPEN",
"};BRACE_CLOSE",
":;COLON",
",;COMMA",
}
)
void generalTerminalToken(String input, HjsonLexerToken.Type tokenType) {
HjsonLexer lexer = createLexer(input);
assertEquals(new HjsonLexerToken(
tokenType,
new HjsonReadPosition(1, 1),
new HjsonReadPosition(1, 1),
null
), assertDoesNotThrow(lexer::nextGeneralToken));
assertGeneralEof(lexer, new HjsonReadPosition(1, 2));
}
@ParameterizedTest
@CsvSource(
delimiter = ';',
value = {
"[;BRACKET_OPEN",
"];BRACKET_CLOSE",
"{;BRACE_OPEN",
"};BRACE_CLOSE",
":;COLON",
",;COMMA",
}
)
void innerObjectTerminalToken(String input, HjsonLexerToken.Type tokenType) {
HjsonLexer lexer = createLexer(input);
assertEquals(new HjsonLexerToken(
tokenType,
new HjsonReadPosition(1, 1),
new HjsonReadPosition(1, 1),
null
), assertDoesNotThrow(lexer::nextInnerObjectToken));
assertGeneralEof(lexer, new HjsonReadPosition(1, 2));
}
@ParameterizedTest
@CsvSource({
"null,NULL",
"true,TRUE",
"false,FALSE",
})
void generalConstants(String constant, HjsonLexerToken.Type tokenType) {
HjsonLexer lexer = createLexer(constant);
assertEquals(new HjsonLexerToken(
tokenType,
new HjsonReadPosition(1, 1),
new HjsonReadPosition(1, constant.length()),
constant
), assertDoesNotThrow(lexer::nextGeneralToken));
assertGeneralEof(lexer, new HjsonReadPosition(1, constant.length() + 1));
}
@ParameterizedTest
@CsvSource(
value = {
"123,0,3",
" 123 ,2,5",
"123.45,0,6",
"500e8,0,5",
"500E8,0,5",
" 789.45e-9 ,1,10",
"-45e+8,0,6",
" -12.34E-81 ,2,12",
},
ignoreLeadingAndTrailingWhitespace = false
)
void generalNumber(String input, int begin, int end) {
HjsonLexer lexer = createLexer(input);
assertEquals(new HjsonLexerToken(
HjsonLexerToken.Type.NUMBER,
new HjsonReadPosition(1, begin + 1),
new HjsonReadPosition(1, end),
input.substring(begin, end)
), assertDoesNotThrow(lexer::nextGeneralToken));
assertGeneralEof(lexer, new HjsonReadPosition(1, input.length() + 1));
}
private HjsonLexer createLexer(String input) {
return new HjsonLexer(new StringReader(input));
}
private static void assertGeneralEof(HjsonLexer lexer, HjsonReadPosition position) {
assertEquals(
new HjsonLexerToken(HjsonLexerToken.Type.EOF, position, position, null),
assertDoesNotThrow(lexer::nextGeneralToken)
);
}
}

View File

@@ -0,0 +1,175 @@
package de.siphalor.tweed5.data.hjson;
import de.siphalor.tweed5.dataapi.api.TweedDataReadException;
import de.siphalor.tweed5.dataapi.api.TweedDataToken;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import java.io.StringReader;
import static org.junit.jupiter.api.Assertions.*;
@Timeout(value = 10, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
class HjsonReaderTest {
private static final double DOUBLE_PRECISION = 0.000000001D;
@ParameterizedTest
@CsvSource({
"127,127",
"-128,-128",
"1.23e2,123",
"1230E-1,123",
})
void testByte(String input, byte expected) {
HjsonReader hjsonReader = setupReaderWithLexer(input);
TweedDataToken token = assertDoesNotThrow(hjsonReader::readToken);
assertEquals(expected, assertDoesNotThrow(token::readAsByte));
assertTrue(token.canReadAsByte());
}
@ParameterizedTest
@ValueSource(strings = {
"128",
"1.23",
"-129",
"1.23e3",
"123E-1",
})
void testByteIllegal(String input) {
HjsonReader hjsonReader = setupReaderWithLexer(input);
TweedDataToken token = assertDoesNotThrow(hjsonReader::readToken);
assertThrows(TweedDataReadException.class, token::readAsByte);
assertFalse(token.canReadAsByte());
}
@ParameterizedTest
@CsvSource(value = {
"123,123",
"123e4,1230000",
"9.87e3,9870",
"45670E-1,4567",
"-123.56E+5,-12356000",
})
void testInteger(String input, int expected) {
HjsonReader hjsonReader = setupReaderWithLexer(input);
TweedDataToken token = assertDoesNotThrow(hjsonReader::readToken);
assertEquals(expected, assertDoesNotThrow(token::readAsInt));
assertTrue(token.canReadAsInt());
}
@ParameterizedTest
@CsvSource(
ignoreLeadingAndTrailingWhitespace = false,
value = {
"123,123",
"12.34,12.34",
"123456789.123456789,123456789.123456789",
"1234.057e8,123405700000",
"987.654E-5,0.00987654",
}
)
void testDouble(String input, double expected) {
HjsonReader hjsonReader = setupReaderWithLexer(input);
TweedDataToken token = assertDoesNotThrow(hjsonReader::readToken);
assertEquals(expected, assertDoesNotThrow(token::readAsDouble), DOUBLE_PRECISION);
assertTrue(token.canReadAsDouble());
}
@ParameterizedTest
@ValueSource(strings = {
"{test:abc\ncdef:123\na:true}",
"\n{test: abc \ncdef:123,a\n:\ntrue\n}",
"// \n{\n\ttest:abc\ncdef:123e0,a: true ,}",
})
void testObject(String input) {
HjsonReader hjsonReader = setupReaderWithLexer(input);
TweedDataToken token;
token = assertDoesNotThrow(hjsonReader::readToken);
assertTrue(token.isMapStart());
token = assertDoesNotThrow(hjsonReader::readToken);
assertTrue(token.isMapEntryKey());
assertTrue(token.canReadAsString());
assertEquals("test", assertDoesNotThrow(token::readAsString));
token = assertDoesNotThrow(hjsonReader::readToken);
assertTrue(token.isMapEntryValue());
assertTrue(token.canReadAsString());
assertEquals("abc", assertDoesNotThrow(token::readAsString));
token = assertDoesNotThrow(hjsonReader::readToken);
assertTrue(token.isMapEntryKey());
assertTrue(token.canReadAsString());
assertEquals("cdef", assertDoesNotThrow(token::readAsString));
token = assertDoesNotThrow(hjsonReader::readToken);
assertTrue(token.isMapEntryValue());
assertTrue(token.canReadAsInt());
assertEquals(123, assertDoesNotThrow(token::readAsInt));
token = assertDoesNotThrow(hjsonReader::readToken);
assertTrue(token.isMapEntryKey());
assertTrue(token.canReadAsString());
assertEquals("a", assertDoesNotThrow(token::readAsString));
token = assertDoesNotThrow(hjsonReader::readToken);
assertTrue(token.isMapEntryValue());
assertTrue(token.canReadAsBoolean());
assertEquals(true, assertDoesNotThrow(token::readAsBoolean));
token = assertDoesNotThrow(hjsonReader::readToken);
assertTrue(token.isMapEnd());
}
@ParameterizedTest
@ValueSource(strings = {
"[12,34,56]",
"[12\n34\n\t56]",
"[\n12\n\t\t34\n\t56\n]",
"[\n12,34\n\t56\n]",
})
void testArray(String input) {
HjsonReader hjsonReader = setupReaderWithLexer(input);
TweedDataToken token;
token = assertDoesNotThrow(hjsonReader::readToken);
assertTrue(token.isListStart());
token = assertDoesNotThrow(hjsonReader::readToken);
assertTrue(token.isListValue());
assertTrue(token.canReadAsInt());
assertEquals(12, assertDoesNotThrow(token::readAsInt));
token = assertDoesNotThrow(hjsonReader::readToken);
assertTrue(token.isListValue());
assertTrue(token.canReadAsInt());
assertEquals(34, assertDoesNotThrow(token::readAsInt));
token = assertDoesNotThrow(hjsonReader::readToken);
assertTrue(token.isListValue());
assertTrue(token.canReadAsInt());
assertEquals(56, assertDoesNotThrow(token::readAsInt));
token = assertDoesNotThrow(hjsonReader::readToken);
assertTrue(token.isListEnd());
}
@ParameterizedTest
@ValueSource(strings = {
"[]",
"[\n\n]",
"[ ]",
"[\n\t\t]",
})
void testEmptyArray(String input) {
HjsonReader hjsonReader = setupReaderWithLexer(input);
TweedDataToken token;
token = assertDoesNotThrow(hjsonReader::readToken);
assertTrue(token.isListStart());
token = assertDoesNotThrow(hjsonReader::readToken);
assertTrue(token.isListEnd());
}
private HjsonReader setupReaderWithLexer(String input) {
return new HjsonReader(new HjsonLexer(new StringReader(input)));
}
}

View File

@@ -0,0 +1,57 @@
package de.siphalor.tweed5.data.hjson;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.StringWriter;
import static org.junit.jupiter.api.Assertions.assertEquals;
class HjsonWriterTest {
private HjsonWriter writer;
private StringWriter stringWriter;
@BeforeEach
void setUp() {
stringWriter = new StringWriter();
}
@Test
void complex() {
setUpHjsonWriter(new HjsonWriter.Options());
writer.visitMapStart();
writer.visitMapEntryKey("test");
writer.visitBoolean(false);
writer.visitMapEntryKey("null");
writer.visitNull();
writer.visitMapEntryKey("a list");
writer.visitListStart();
writer.visitInt(12);
writer.visitInt(34);
writer.visitString("Testing\n multiline\nstuff");
writer.visitListEnd();
writer.visitMapEnd();
assertEquals(
"{\n" +
"\ttest: false\n" +
"\tnull: null\n" +
"\t\"a list\": [\n" +
"\t\t12\n" +
"\t\t34\n" +
"\t\t'''\n" +
"\t\tTesting\n" +
"\t\t multiline\n" +
"\t\tstuff\n" +
"\t\t'''\n" +
"\t]\n" +
"}\n",
stringWriter.toString()
);
}
void setUpHjsonWriter(HjsonWriter.Options options) {
writer = new HjsonWriter(stringWriter, options);
}
}