multi-release-jar and refactor

This commit is contained in:
2021-07-26 21:58:20 +02:00
parent c3ab8c3dd9
commit 99cbb09e58
46 changed files with 285 additions and 318 deletions

View File

@@ -4,8 +4,8 @@ import lombok.SneakyThrows;
import net.woggioni.jwo.Chronometer;
import net.woggioni.jwo.CollectionUtils;
import net.woggioni.jwo.JWO;
import net.woggioni.jwo.io.CircularInputStream;
import net.woggioni.jwo.tuple.Tuple2;
import net.woggioni.jwo.CircularInputStream;
import net.woggioni.jwo.Tuple2;
import java.io.InputStream;
import java.io.InputStreamReader;

View File

@@ -1,5 +1,6 @@
plugins {
id 'maven-publish'
id 'net.woggioni.plugins.multi-release-jar'
id 'net.woggioni.gradle.lombok' apply false
}
@@ -37,20 +38,12 @@ dependencies {
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
withJavadocJar()
withSourcesJar()
}
jar {
manifest{
attributes([
"Automatic-Module-Name": "net.woggioni.jwo"
])
}
}
TaskProvider<Zip> pathClassLoaderTestBundleTask = tasks.register("pathClassLoaderTestBundle", Zip) {
from(configurations.pathClassloaderTest)
archiveBaseName = "pathClassLoaderTestBundle"

View File

@@ -1,4 +1,4 @@
gradle.version = 7.1
gradle.version = 7.1.1
jwo.version=1.0
junitJupiter.version=5.7.0
lombok.version=1.18.16

Binary file not shown.

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

2
gradlew vendored
View File

@@ -72,7 +72,7 @@ case "`uname`" in
Darwin* )
darwin=true
;;
MINGW* )
MSYS* | MINGW* )
msys=true
;;
NONSTOP* )

View File

@@ -8,6 +8,7 @@ pluginManagement {
plugins {
id "net.woggioni.gradle.lombok" version "0.1"
id "net.woggioni.plugins.multi-release-jar" version "0.1"
}
}

View File

@@ -1,4 +1,4 @@
package net.woggioni.jwo.io;
package net.woggioni.jwo;
import lombok.SneakyThrows;

View File

@@ -1,4 +1,4 @@
package net.woggioni.jwo.io;
package net.woggioni.jwo;
import lombok.RequiredArgsConstructor;

View File

@@ -1,4 +1,4 @@
package net.woggioni.jwo.hash;
package net.woggioni.jwo;
import lombok.EqualsAndHashCode;
import lombok.Getter;

View File

@@ -0,0 +1,125 @@
package net.woggioni.jwo;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.SneakyThrows;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class HttpClient {
private SSLSocketFactory socketFactory;
public HttpClient() {}
public HttpClient(final SSLContext sslContext) {
socketFactory = sslContext.getSocketFactory();
}
@SneakyThrows
public HttpsURLConnection call(HttpRequest httpRequest) {
HttpsURLConnection conn = (HttpsURLConnection) httpRequest.url.openConnection();
if(socketFactory != null) {
conn.setSSLSocketFactory(socketFactory);
}
conn.setInstanceFollowRedirects(false);
conn.setRequestMethod(httpRequest.method.text);
httpRequest.headers.forEach((key, value) ->
value.forEach(headerValue -> conn.addRequestProperty(key, headerValue)));
switch (httpRequest.method) {
case PUT:
case POST:
case DELETE:
if (httpRequest.body != null) {
conn.setDoOutput(true);
byte[] buffer = new byte[1024];
OutputStream os = conn.getOutputStream();
while (true) {
int read = httpRequest.body.read(buffer, 0, buffer.length);
if (read < 0) break;
os.write(buffer, 0, read);
}
}
break;
case GET:
case HEAD:
case OPTIONS:
break;
}
return conn;
}
public enum HttpMethod {
PUT("PUT"),
GET("GET"),
POST("POST"),
DELETE("DELETE"),
HEAD("HEAD"),
OPTIONS("OPTIONS");
public final String text;
HttpMethod(String text) {
this.text = text;
}
}
public enum HttpStatus {
OK(200),
INTERNAL_SERVER_ERROR(500),
BAD_REQUEST(400),
UNAUTHORIZED(401),
FORBIDDEN(403),
NOT_FOUND(404),
CONFLICT(409);
public final int code;
HttpStatus(int code) {
this.code = code;
}
}
@Builder(builderMethodName = "privateBuilder", access = AccessLevel.PUBLIC)
public static class HttpRequest {
final URL url;
final HttpMethod method = HttpMethod.GET;
final Map<String, List<String>> headers = Collections.emptyMap();
final InputStream body = null;
public static HttpRequestBuilder builder(URL url) {
return HttpRequest.privateBuilder().url(url);
}
public static class Builder {
private final URL url;
private HttpMethod method = HttpMethod.GET;
private Map<String, List<String>> headers = Collections.emptyMap();
private InputStream body = null;
private Builder(URL url) {
this.url = url;
}
public Builder method(HttpMethod method) {
this.method = method;
return this;
}
}
}
}

View File

@@ -2,7 +2,6 @@ package net.woggioni.jwo;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import net.woggioni.jwo.tuple.Tuple2;
import javax.net.ssl.*;
import java.io.*;

View File

@@ -1,6 +1,4 @@
package net.woggioni.jwo.io;
import net.woggioni.jwo.JWO;
package net.woggioni.jwo;
import java.io.IOException;
import java.io.InputStream;
@@ -13,11 +11,10 @@ import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
/**
* Input stream that extract a jar archive in the provided {@param destination} while reading it
* Input stream that extract a jar archive in the provided destination while reading it
*/
class JarExtractorInputStream extends JarInputStream {
public class JarExtractorInputStream extends JarInputStream {
private final String sourceLocation;
private final Path destination;
private OutputStream currentFile = null;
@@ -26,7 +23,6 @@ class JarExtractorInputStream extends JarInputStream {
boolean verify,
String sourceLocation) throws IOException {
super(source, verify);
this.sourceLocation = sourceLocation;
this.destination = destination;
Path newFileSystemLocation = destination.resolve(JarFile.MANIFEST_NAME);
Files.createDirectories(newFileSystemLocation.getParent());

View File

@@ -1,4 +1,4 @@
package net.woggioni.jwo.collection;
package net.woggioni.jwo;
import java.util.Comparator;

View File

@@ -1,4 +1,4 @@
package net.woggioni.jwo.io;
package net.woggioni.jwo;
import lombok.SneakyThrows;

View File

@@ -1,4 +1,4 @@
package net.woggioni.jwo.io;
package net.woggioni.jwo;
import lombok.SneakyThrows;

View File

@@ -1,8 +1,6 @@
package net.woggioni.jwo.cache;
package net.woggioni.jwo;
import lombok.Getter;
import net.woggioni.jwo.Chronometer;
import net.woggioni.jwo.CollectionUtils;
import java.util.*;
import java.util.function.Function;

View File

@@ -1,4 +1,4 @@
package net.woggioni.jwo.tuple;
package net.woggioni.jwo;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;

View File

@@ -1,4 +1,4 @@
package net.woggioni.jwo.tuple;
package net.woggioni.jwo;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;

View File

@@ -0,0 +1,11 @@
package net.woggioni.jwo;
import java.io.IOException;
import java.io.InputStream;
public class NullInputStream extends InputStream {
@Override
public int read() throws IOException {
return -1;
}
}

View File

@@ -1,4 +1,4 @@
package net.woggioni.jwo.io;
package net.woggioni.jwo;
import java.io.OutputStream;

View File

@@ -1,13 +1,17 @@
package net.woggioni.jwo.loader;
package net.woggioni.jwo;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URL;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
@@ -94,4 +98,63 @@ public final class PathClassLoader extends ClassLoader {
private static URL toURL(Path path) throws IOException {
return new URL(null, path.toUri().toString(), PathURLStreamHandler.INSTANCE);
}
private static final class PathURLConnection extends URLConnection {
private final Path path;
PathURLConnection(URL url, Path path) {
super(url);
this.path = path;
}
@Override
public void connect() {}
@Override
public long getContentLengthLong() {
try {
return Files.size(this.path);
} catch (IOException e) {
throw new RuntimeException("could not get size of: " + this.path, e);
}
}
@Override
public InputStream getInputStream() throws IOException {
return Files.newInputStream(this.path);
}
@Override
public OutputStream getOutputStream() throws IOException {
return Files.newOutputStream(this.path);
}
@Override
@SneakyThrows
public String getContentType() {
return Files.probeContentType(this.path);
}
@Override
@SneakyThrows
public long getLastModified() {
BasicFileAttributes attributes = Files.readAttributes(this.path, BasicFileAttributes.class);
return attributes.lastModifiedTime().toMillis();
}
}
@NoArgsConstructor(access = AccessLevel.PRIVATE)
private static final class PathURLStreamHandler extends URLStreamHandler {
static final URLStreamHandler INSTANCE = new PathURLStreamHandler();
@Override
@SneakyThrows
protected URLConnection openConnection(URL url) {
URI uri = url.toURI();
Path path = Paths.get(uri);
return new PathURLConnection(url, path);
}
}
}

View File

@@ -1,4 +1,4 @@
package net.woggioni.jwo.signing;
package net.woggioni.jwo;
import java.security.CodeSigner;
import java.security.PublicKey;

View File

@@ -1,5 +1,6 @@
package net.woggioni.jwo.tree;
package net.woggioni.jwo;
import java.util.Iterator;
import java.util.List;
/**
@@ -13,7 +14,35 @@ import java.util.List;
* The last element of the list corresponds to the node currently being traversed.
* @param <T> the type of the context object used
*/
public interface TreeNodeVisitor<NODE extends TreeNode<NODE>, T> {
public interface TreeNodeVisitor<NODE extends TreeNodeVisitor.TreeNode<NODE>, T> {
interface TreeNode<NODE extends TreeNode> {
Iterator<NODE> children();
}
/**
* This interface exposes the methods that are visible to the user of
* {@link TreeWalker}, it allows to
* set/get a custom object in the current stack context or to get the current link's Aci
* @param <T> the type of the context object used
*/
interface StackContext<NODE extends TreeNode, T> {
/**
* @param ctx the user object to set for this stack level
*/
void setContext(T ctx);
/**
* @return the current user object
*/
T getContext();
/**
* @return the current TreeNode
*/
NODE getNode();
}
enum VisitOutcome {
CONTINUE, SKIP, EARLY_EXIT

View File

@@ -1,4 +1,4 @@
package net.woggioni.jwo.tree;
package net.woggioni.jwo;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@@ -13,10 +13,10 @@ import static net.woggioni.jwo.JWO.pop;
import static net.woggioni.jwo.JWO.tail;
@RequiredArgsConstructor
public class TreeWalker<NODE extends TreeNode<NODE>, T> {
public class TreeWalker<NODE extends TreeNodeVisitor.TreeNode<NODE>, T> {
@RequiredArgsConstructor
private static class StackElement<NODE extends TreeNode<NODE>, T> implements StackContext<NODE, T> {
private static class StackElement<NODE extends TreeNodeVisitor.TreeNode<NODE>, T> implements TreeNodeVisitor.StackContext<NODE, T> {
@Getter
final NODE node;
@@ -39,7 +39,7 @@ public class TreeWalker<NODE extends TreeNode<NODE>, T> {
List<StackElement<NODE, T>> stack = new ArrayList<>();
StackElement<NODE, T> rootStackElement = new StackElement<>(root);
stack.add(rootStackElement);
List<StackContext<NODE, T>> publicStack = Collections.unmodifiableList(stack);
List<TreeNodeVisitor.StackContext<NODE, T>> publicStack = Collections.unmodifiableList(stack);
switch (visitor.visitPre(publicStack)) {
case CONTINUE:
rootStackElement.childrenIterator = root.children();

View File

@@ -1,4 +1,4 @@
package net.woggioni.jwo.tuple;
package net.woggioni.jwo;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;

View File

@@ -1,4 +1,4 @@
package net.woggioni.jwo.tuple;
package net.woggioni.jwo;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;

View File

@@ -1,4 +1,4 @@
package net.woggioni.jwo.io;
package net.woggioni.jwo;
import java.io.FilterInputStream;
import java.io.InputStream;

View File

@@ -1,4 +1,4 @@
package net.woggioni.jwo.io;
package net.woggioni.jwo;
import java.io.FilterOutputStream;
import java.io.OutputStream;

View File

@@ -1,4 +1,4 @@
package net.woggioni.jwo.io;
package net.woggioni.jwo;
import java.io.IOException;
import java.io.InputStream;
@@ -9,9 +9,9 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* Input stream that extract a zip archive in the provided {@param destination} while reading it
* Input stream that extract a zip archive in the provided destination while reading it
*/
class ZipExtractorInputStream extends ZipInputStream {
public class ZipExtractorInputStream extends ZipInputStream {
public ZipExtractorInputStream(InputStream source, Path destination) {
super(source);

View File

@@ -1,52 +0,0 @@
package net.woggioni.jwo.http;
import lombok.SneakyThrows;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import java.io.OutputStream;
public class HttpClient {
private SSLSocketFactory socketFactory;
public HttpClient() {}
public HttpClient(final SSLContext sslContext) {
socketFactory = sslContext.getSocketFactory();
}
@SneakyThrows
public HttpsURLConnection call(HttpRequest httpRequest) {
HttpsURLConnection conn = (HttpsURLConnection) httpRequest.url.openConnection();
if(socketFactory != null) {
conn.setSSLSocketFactory(socketFactory);
}
conn.setInstanceFollowRedirects(false);
conn.setRequestMethod(httpRequest.method.text);
httpRequest.headers.forEach((key, value) ->
value.forEach(headerValue -> conn.addRequestProperty(key, headerValue)));
switch (httpRequest.method) {
case PUT:
case POST:
case DELETE:
if (httpRequest.body != null) {
conn.setDoOutput(true);
byte[] buffer = new byte[1024];
OutputStream os = conn.getOutputStream();
while (true) {
int read = httpRequest.body.read(buffer, 0, buffer.length);
if (read < 0) break;
os.write(buffer, 0, read);
}
}
break;
case GET:
case HEAD:
case OPTIONS:
break;
}
return conn;
}
}

View File

@@ -1,16 +0,0 @@
package net.woggioni.jwo.http;
public enum HttpMethod {
PUT("PUT"),
GET("GET"),
POST("POST"),
DELETE("DELETE"),
HEAD("HEAD"),
OPTIONS("OPTIONS");
public final String text;
HttpMethod(String text) {
this.text = text;
}
}

View File

@@ -1,46 +0,0 @@
package net.woggioni.jwo.http;
import lombok.AccessLevel;
import lombok.Builder;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@Builder(builderMethodName = "privateBuilder", access = AccessLevel.PUBLIC)
public class HttpRequest {
final URL url;
final HttpMethod method = HttpMethod.GET;
final Map<String, List<String>> headers = Collections.emptyMap();
final InputStream body = null;
public static HttpRequestBuilder builder(URL url) {
return HttpRequest.privateBuilder().url(url);
}
public static class Builder {
private final URL url;
private HttpMethod method = HttpMethod.GET;
private Map<String, List<String>> headers = Collections.emptyMap();
private InputStream body = null;
private Builder(URL url) {
this.url = url;
}
public Builder method(HttpMethod method) {
this.method = method;
return this;
}
}
}

View File

@@ -1,17 +0,0 @@
package net.woggioni.jwo.http;
public enum HttpStatus {
OK(200),
INTERNAL_SERVER_ERROR(500),
BAD_REQUEST(400),
UNAUTHORIZED(401),
FORBIDDEN(403),
NOT_FOUND(404),
CONFLICT(409);
public final int code;
HttpStatus(int code) {
this.code = code;
}
}

View File

@@ -1,57 +0,0 @@
package net.woggioni.jwo.loader;
import lombok.SneakyThrows;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
final class PathURLConnection extends URLConnection {
private final Path path;
PathURLConnection(URL url, Path path) {
super(url);
this.path = path;
}
@Override
public void connect() {}
@Override
public long getContentLengthLong() {
try {
return Files.size(this.path);
} catch (IOException e) {
throw new RuntimeException("could not get size of: " + this.path, e);
}
}
@Override
public InputStream getInputStream() throws IOException {
return Files.newInputStream(this.path);
}
@Override
public OutputStream getOutputStream() throws IOException {
return Files.newOutputStream(this.path);
}
@Override
@SneakyThrows
public String getContentType() {
return Files.probeContentType(this.path);
}
@Override
@SneakyThrows
public long getLastModified() {
BasicFileAttributes attributes = Files.readAttributes(this.path, BasicFileAttributes.class);
return attributes.lastModifiedTime().toMillis();
}
}

View File

@@ -1,26 +0,0 @@
package net.woggioni.jwo.loader;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.nio.file.Path;
import java.nio.file.Paths;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
final class PathURLStreamHandler extends URLStreamHandler {
static final URLStreamHandler INSTANCE = new PathURLStreamHandler();
@Override
@SneakyThrows
protected URLConnection openConnection(URL url) {
URI uri = url.toURI();
Path path = Paths.get(uri);
return new PathURLConnection(url, path);
}
}

View File

@@ -1,25 +0,0 @@
package net.woggioni.jwo.tree;
/**
* This interface exposes the methods that are visible to the user of
* {@link TreeWalker}, it allows to
* set/get a custom object in the current stack context or to get the current link's Aci
* @param <T> the type of the context object used
*/
public interface StackContext<NODE extends TreeNode, T> {
/**
* @param ctx the user object to set for this stack level
*/
void setContext(T ctx);
/**
* @return the current user object
*/
T getContext();
/**
* @return the current TreeNode
*/
NODE getNode();
}

View File

@@ -1,7 +0,0 @@
package net.woggioni.jwo.tree;
import java.util.Iterator;
public interface TreeNode<NODE extends TreeNode> {
Iterator<NODE> children();
}

View File

@@ -0,0 +1,3 @@
module net.woggioni.jwo {
exports net.woggioni.jwo;
}

View File

@@ -1,6 +1,7 @@
package net.woggioni.jwo.io;
package net.woggioni.jwo;
import lombok.SneakyThrows;
import net.woggioni.jwo.CircularBuffer;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

View File

@@ -1,8 +1,6 @@
package net.woggioni.jwo.io;
package net.woggioni.jwo;
import lombok.SneakyThrows;
import net.woggioni.jwo.JWO;
import net.woggioni.jwo.hash.Hash;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -18,7 +16,6 @@ import java.util.NavigableMap;
import java.util.Objects;
import java.util.TreeMap;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
@@ -26,15 +23,13 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public class ExtractorInputStreamTest {
private Path testDir;
private Path testJar;
private Path referenceExtractionDestination;
private Path testExtractionDestination;
@BeforeEach
void setup(@TempDir Path testDir) {
this.testDir = testDir;
testJar = Path.of(System.getProperty("junit.jupiter.engine.jar"));
testJar = Paths.get(System.getProperty("junit.jupiter.engine.jar"));
referenceExtractionDestination = testDir.resolve("referenceExtraction");
testExtractionDestination = testDir.resolve("testExtraction");
}
@@ -45,7 +40,7 @@ public class ExtractorInputStreamTest {
try(FileSystem fs = FileSystems.newFileSystem(source, null)) {
for(Path root : fs.getRootDirectories()) {
Files.walk(root)
.filter(Predicate.not(Files::isDirectory)).forEach(new Consumer<Path>() {
.filter(it -> !Files.isDirectory(it)).forEach(new Consumer<Path>() {
@Override
@SneakyThrows
public void accept(Path path) {
@@ -62,7 +57,7 @@ public class ExtractorInputStreamTest {
private static NavigableMap<String, Hash> hashFileTree(Path tree) {
NavigableMap<String, Hash> result = new TreeMap<>();
byte[] buffer = new byte[0x1000];
FileVisitor<Path> visitor = new SimpleFileVisitor<>() {
FileVisitor<Path> visitor = new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String key = tree.relativize(file).toString();

View File

@@ -1,4 +1,4 @@
package net.woggioni.jwo.hash;
package net.woggioni.jwo;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;

View File

@@ -1,4 +1,4 @@
package net.woggioni.jwo.collection;
package net.woggioni.jwo;
import org.junit.jupiter.api.Test;

View File

@@ -1,8 +1,7 @@
package net.woggioni.jwo.cache;
package net.woggioni.jwo;
import lombok.EqualsAndHashCode;
import lombok.SneakyThrows;
import net.woggioni.jwo.JWO;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

View File

@@ -1,6 +1,7 @@
package net.woggioni.jwo.loader;
package net.woggioni.jwo;
import lombok.SneakyThrows;
import net.woggioni.jwo.PathClassLoader;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

View File

@@ -1,8 +1,7 @@
package net.woggioni.jwo.tree;
package net.woggioni.jwo;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.woggioni.jwo.tuple.Tuple2;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -12,7 +11,7 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
@RequiredArgsConstructor
class Node implements TreeNode<Node> {
class Node implements TreeNodeVisitor.TreeNode<Node> {
@Getter
private final Integer id;
private final List<Node> children;