Initial commit
That's a lotta stuff for an initial commit, but well...
This commit is contained in:
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal 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
37
build.gradle.kts
Normal 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
3
gradle.properties
Normal 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
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
234
gradlew
vendored
Normal 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
89
gradlew.bat
vendored
Normal 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
1
lombok.config
Normal file
@@ -0,0 +1 @@
|
||||
lombok.accessors.fluent = true
|
||||
8
settings.gradle.kts
Normal file
8
settings.gradle.kts
Normal 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")
|
||||
3
tweed5-core/build.gradle.kts
Normal file
3
tweed5-core/build.gradle.kts
Normal file
@@ -0,0 +1,3 @@
|
||||
dependencies {
|
||||
api(project(":tweed5-patchwork"))
|
||||
}
|
||||
0
tweed5-core/gradle.properties
Normal file
0
tweed5-core/gradle.properties
Normal 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();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.siphalor.tweed5.core.api.container;
|
||||
|
||||
public enum ConfigContainerSetupPhase {
|
||||
EXTENSIONS_SETUP,
|
||||
TREE_SETUP,
|
||||
SEALING_TREE,
|
||||
TREE_SEALED,
|
||||
READY,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package de.siphalor.tweed5.core.api.entry;
|
||||
|
||||
public interface SimpleConfigEntry<T> extends ConfigEntry<T> {
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.siphalor.tweed5.core.api.extension;
|
||||
|
||||
import de.siphalor.tweed5.patchwork.api.Patchwork;
|
||||
|
||||
public interface EntryExtensionsData extends Patchwork<EntryExtensionsData> {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package de.siphalor.tweed5.core.generated;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package de.siphalor.tweed5.core.impl;
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
6
tweed5-default-extensions/build.gradle.kts
Normal file
6
tweed5-default-extensions/build.gradle.kts
Normal file
@@ -0,0 +1,6 @@
|
||||
dependencies {
|
||||
api(project(":tweed5-core"))
|
||||
api(project(":tweed5-serde-extension"))
|
||||
|
||||
testImplementation(project(":tweed5-serde-hjson"))
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package de.siphalor.tweed5.defaultextensions.comment.impl;
|
||||
|
||||
import de.siphalor.tweed5.defaultextensions.comment.api.CommentProducer;
|
||||
|
||||
public interface InternalCommentEntryData {
|
||||
CommentProducer commentProducer();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@ApiStatus.Internal
|
||||
package de.siphalor.tweed5.defaultextensions.comment.impl;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
4
tweed5-patchwork/build.gradle.kts
Normal file
4
tweed5-patchwork/build.gradle.kts
Normal file
@@ -0,0 +1,4 @@
|
||||
dependencies {
|
||||
implementation("org.ow2.asm:asm:${properties["asm.version"]}")
|
||||
implementation("org.ow2.asm:asm-commons:${properties["asm.version"]}")
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package de.siphalor.tweed5.patchwork.api;
|
||||
|
||||
public class PatchworkPartIsNullException extends RuntimeException {
|
||||
public PatchworkPartIsNullException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.siphalor.tweed5.dataapi.api;
|
||||
|
||||
public interface TweedDataReader {
|
||||
TweedDataToken peekToken() throws TweedDataReadException;
|
||||
TweedDataToken readToken() throws TweedDataReadException;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.siphalor.tweed5.dataapi.api;
|
||||
|
||||
public enum TweedDataReaderRecoverMode {
|
||||
SKIP,
|
||||
REPEAT,
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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)";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.siphalor.tweed5.dataapi.api;
|
||||
|
||||
public interface TweedDataWriter extends TweedDataVisitor {
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
7
tweed5-serde-extension/build.gradle.kts
Normal file
7
tweed5-serde-extension/build.gradle.kts
Normal file
@@ -0,0 +1,7 @@
|
||||
dependencies {
|
||||
api(project(":tweed5-core"))
|
||||
api(project(":tweed5-patchwork"))
|
||||
api(project(":tweed5-serde-api"))
|
||||
|
||||
testImplementation(project(":tweed5-serde-hjson"))
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.siphalor.tweed5.data.extension.api;
|
||||
|
||||
public interface EntryReaderWriterDefinition {
|
||||
TweedEntryReader<?, ?> reader();
|
||||
TweedEntryWriter<?, ?> writer();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.siphalor.tweed5.data.extension.api;
|
||||
|
||||
public interface ReadWriteEntryDataExtension {
|
||||
TweedEntryReader<?, ?> entryReaderChain();
|
||||
TweedEntryWriter<?, ?> entryWriterChain();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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> {}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@ApiStatus.Internal
|
||||
package de.siphalor.tweed5.data.extension.impl;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
tweed5-serde-hjson/build.gradle.kts
Normal file
3
tweed5-serde-hjson/build.gradle.kts
Normal file
@@ -0,0 +1,3 @@
|
||||
dependencies {
|
||||
api(project(":tweed5-serde-api"))
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package de.siphalor.tweed5.data.hjson;
|
||||
|
||||
public enum HjsonStringType {
|
||||
INLINE_QUOTELESS,
|
||||
INLINE_DOUBLE_QUOTE,
|
||||
INLINE_SINGLE_QUOTE,
|
||||
MULTILINE_SINGLE_QUOTE,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user