From b072e49d856c3a30caa4a52d3167b491100e8a12 Mon Sep 17 00:00:00 2001 From: Taah Date: Wed, 4 May 2022 05:18:07 -0700 Subject: [PATCH] modify toml4j fork to automatically add new fields without overwriting previous ones and setup motd --- build.gradle | 7 + proxy/src/main/java/dev/plex/Plex.java | 74 +++ .../java/dev/plex/command/PlexCommand.java | 104 ++++ .../command/annotation/CommandParameters.java | 39 ++ .../annotation/CommandPermissions.java | 27 + .../command/source/RequiredCommandSource.java | 8 + .../main/java/dev/plex/config/TomlConfig.java | 94 ++++ .../dev/plex/handlers/ListenerHandler.java | 33 ++ .../java/dev/plex/listener/PlexListener.java | 13 + .../plex/listener/impl/ServerListener.java | 42 ++ .../dev/plex/settings/ServerSettings.java | 22 + .../java/dev/plex/toml/ArrayValueReader.java | 77 +++ .../java/dev/plex/toml/ArrayValueWriter.java | 65 +++ .../plex/toml/BooleanValueReaderWriter.java | 48 ++ .../main/java/dev/plex/toml/Container.java | 122 +++++ .../src/main/java/dev/plex/toml/Context.java | 19 + .../main/java/dev/plex/toml/DatePolicy.java | 22 + .../dev/plex/toml/DateValueReaderWriter.java | 160 ++++++ .../main/java/dev/plex/toml/Identifier.java | 331 ++++++++++++ .../dev/plex/toml/IdentifierConverter.java | 62 +++ .../java/dev/plex/toml/IndentationPolicy.java | 30 ++ .../dev/plex/toml/InlineTableValueReader.java | 75 +++ proxy/src/main/java/dev/plex/toml/Keys.java | 69 +++ .../plex/toml/LiteralStringValueReader.java | 47 ++ .../java/dev/plex/toml/MapValueWriter.java | 166 ++++++ .../MultilineLiteralStringValueReader.java | 52 ++ .../plex/toml/MultilineStringValueReader.java | 57 +++ .../plex/toml/NumberValueReaderWriter.java | 105 ++++ .../java/dev/plex/toml/ObjectValueWriter.java | 69 +++ .../plex/toml/PrimitiveArrayValueWriter.java | 55 ++ .../src/main/java/dev/plex/toml/Results.java | 290 +++++++++++ .../plex/toml/StringValueReaderWriter.java | 129 +++++ .../dev/plex/toml/TableArrayValueWriter.java | 33 ++ proxy/src/main/java/dev/plex/toml/Toml.java | 481 ++++++++++++++++++ .../main/java/dev/plex/toml/TomlParser.java | 63 +++ .../main/java/dev/plex/toml/TomlWriter.java | 179 +++++++ .../main/java/dev/plex/toml/ValueReader.java | 21 + .../main/java/dev/plex/toml/ValueReaders.java | 37 ++ .../main/java/dev/plex/toml/ValueWriter.java | 9 + .../main/java/dev/plex/toml/ValueWriters.java | 31 ++ .../java/dev/plex/toml/WriterContext.java | 182 +++++++ .../src/main/java/dev/plex/util/PlexLog.java | 66 +++ .../main/java/dev/plex/util/RandomUtil.java | 34 ++ .../java/dev/plex/util/ReflectionsUtil.java | 58 +++ settings.gradle | 1 + 45 files changed, 3708 insertions(+) create mode 100644 proxy/src/main/java/dev/plex/Plex.java create mode 100644 proxy/src/main/java/dev/plex/command/PlexCommand.java create mode 100644 proxy/src/main/java/dev/plex/command/annotation/CommandParameters.java create mode 100644 proxy/src/main/java/dev/plex/command/annotation/CommandPermissions.java create mode 100644 proxy/src/main/java/dev/plex/command/source/RequiredCommandSource.java create mode 100644 proxy/src/main/java/dev/plex/config/TomlConfig.java create mode 100644 proxy/src/main/java/dev/plex/handlers/ListenerHandler.java create mode 100644 proxy/src/main/java/dev/plex/listener/PlexListener.java create mode 100644 proxy/src/main/java/dev/plex/listener/impl/ServerListener.java create mode 100644 proxy/src/main/java/dev/plex/settings/ServerSettings.java create mode 100644 proxy/src/main/java/dev/plex/toml/ArrayValueReader.java create mode 100644 proxy/src/main/java/dev/plex/toml/ArrayValueWriter.java create mode 100644 proxy/src/main/java/dev/plex/toml/BooleanValueReaderWriter.java create mode 100644 proxy/src/main/java/dev/plex/toml/Container.java create mode 100644 proxy/src/main/java/dev/plex/toml/Context.java create mode 100644 proxy/src/main/java/dev/plex/toml/DatePolicy.java create mode 100644 proxy/src/main/java/dev/plex/toml/DateValueReaderWriter.java create mode 100644 proxy/src/main/java/dev/plex/toml/Identifier.java create mode 100644 proxy/src/main/java/dev/plex/toml/IdentifierConverter.java create mode 100644 proxy/src/main/java/dev/plex/toml/IndentationPolicy.java create mode 100644 proxy/src/main/java/dev/plex/toml/InlineTableValueReader.java create mode 100644 proxy/src/main/java/dev/plex/toml/Keys.java create mode 100644 proxy/src/main/java/dev/plex/toml/LiteralStringValueReader.java create mode 100644 proxy/src/main/java/dev/plex/toml/MapValueWriter.java create mode 100644 proxy/src/main/java/dev/plex/toml/MultilineLiteralStringValueReader.java create mode 100644 proxy/src/main/java/dev/plex/toml/MultilineStringValueReader.java create mode 100644 proxy/src/main/java/dev/plex/toml/NumberValueReaderWriter.java create mode 100644 proxy/src/main/java/dev/plex/toml/ObjectValueWriter.java create mode 100644 proxy/src/main/java/dev/plex/toml/PrimitiveArrayValueWriter.java create mode 100644 proxy/src/main/java/dev/plex/toml/Results.java create mode 100644 proxy/src/main/java/dev/plex/toml/StringValueReaderWriter.java create mode 100644 proxy/src/main/java/dev/plex/toml/TableArrayValueWriter.java create mode 100644 proxy/src/main/java/dev/plex/toml/Toml.java create mode 100644 proxy/src/main/java/dev/plex/toml/TomlParser.java create mode 100644 proxy/src/main/java/dev/plex/toml/TomlWriter.java create mode 100644 proxy/src/main/java/dev/plex/toml/ValueReader.java create mode 100644 proxy/src/main/java/dev/plex/toml/ValueReaders.java create mode 100644 proxy/src/main/java/dev/plex/toml/ValueWriter.java create mode 100644 proxy/src/main/java/dev/plex/toml/ValueWriters.java create mode 100644 proxy/src/main/java/dev/plex/toml/WriterContext.java create mode 100644 proxy/src/main/java/dev/plex/util/PlexLog.java create mode 100644 proxy/src/main/java/dev/plex/util/RandomUtil.java create mode 100644 proxy/src/main/java/dev/plex/util/ReflectionsUtil.java diff --git a/build.gradle b/build.gradle index a10fc6f..61497f6 100644 --- a/build.gradle +++ b/build.gradle @@ -59,7 +59,14 @@ subprojects { } } +clean { + dependsOn(":api:clean") + dependsOn(":server:clean") + dependsOn(":proxy:clean") +} + task copyJars(type: Copy, dependsOn: subprojects.jar) { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE from(subprojects.jar) from(subprojects.shadowJar) into project.file('build/libs') diff --git a/proxy/src/main/java/dev/plex/Plex.java b/proxy/src/main/java/dev/plex/Plex.java new file mode 100644 index 0000000..061fe9e --- /dev/null +++ b/proxy/src/main/java/dev/plex/Plex.java @@ -0,0 +1,74 @@ +package dev.plex; + +import com.google.inject.Inject; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; +import com.velocitypowered.api.plugin.Plugin; +import com.velocitypowered.api.plugin.annotation.DataDirectory; +import com.velocitypowered.api.proxy.ProxyServer; +import dev.plex.config.TomlConfig; +import dev.plex.handlers.ListenerHandler; +import dev.plex.settings.ServerSettings; +import dev.plex.util.PlexLog; +import lombok.Getter; + +import java.io.File; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.logging.Logger; + +@Plugin( + name = "Plex", + id = "plex", + version = "@version@", + url = "https://plex.us.org", + description = "Plex provides a new experience for freedom servers.", + authors = {"Taah"} +) +@Getter +public class Plex +{ + private static Plex plugin; + + private final ProxyServer server; + private final Logger logger; + private final File dataFolder; + + private TomlConfig config; + + @Inject + public Plex(ProxyServer server, Logger logger, @DataDirectory Path folder) + { + plugin = this; + this.server = server; + this.logger = logger; + this.dataFolder = folder.toFile(); + if (!dataFolder.exists()) + { + dataFolder.mkdir(); + } + PlexLog.log("Enabling Plex v0.1"); + } + + @Subscribe + public void onStart(ProxyInitializeEvent event) + { + this.config = new TomlConfig("config.toml"); + this.config.setOnCreate(toml -> + { + PlexLog.log("Created configuration 'config.toml'"); + }); + this.config.setOnLoad(toml -> + { + PlexLog.log("Loaded configuration 'config.toml'"); + }); + this.config.create(false); + this.config.write(new ServerSettings()); + new ListenerHandler(); + } + + public static Plex get() + { + return plugin; + } +} diff --git a/proxy/src/main/java/dev/plex/command/PlexCommand.java b/proxy/src/main/java/dev/plex/command/PlexCommand.java new file mode 100644 index 0000000..cae4a47 --- /dev/null +++ b/proxy/src/main/java/dev/plex/command/PlexCommand.java @@ -0,0 +1,104 @@ +package dev.plex.command; + +import com.velocitypowered.api.command.CommandMeta; +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.command.SimpleCommand; +import com.velocitypowered.api.proxy.ConsoleCommandSource; +import com.velocitypowered.api.proxy.Player; +import dev.plex.Plex; +import dev.plex.command.annotation.CommandParameters; +import dev.plex.command.annotation.CommandPermissions; +import dev.plex.command.source.RequiredCommandSource; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; + +public abstract class PlexCommand implements SimpleCommand +{ + /** + * Returns the instance of the plugin + */ + protected static Plex plugin = Plex.get(); + + /** + * The parameters for the command + */ + private final CommandParameters params; + + /** + * The permissions for the command + */ + private final CommandPermissions perms; + /** + * Required command source fetched from the permissions + */ + private final RequiredCommandSource commandSource; + + public PlexCommand() + { + this.params = getClass().getAnnotation(CommandParameters.class); + this.perms = getClass().getAnnotation(CommandPermissions.class); + this.commandSource = this.perms.source(); + + CommandMeta.Builder meta = plugin.getServer().getCommandManager().metaBuilder(this.params.name()); + meta.aliases(this.params.aliases()); + meta.plugin(Plex.get()); + plugin.getServer().getCommandManager().register(meta.build(), this); + } + + + protected abstract Component execute(@NotNull CommandSource source, @Nullable Player player, @NotNull String[] args); + + @Override + public void execute(Invocation invocation) + { + if (!matches(invocation.alias())) + { + return; + } + + if (commandSource == RequiredCommandSource.CONSOLE && invocation.source() instanceof Player) + { +// sender.sendMessage(messageComponent("noPermissionInGame")); + return; + } + + if (commandSource == RequiredCommandSource.IN_GAME) + { + if (invocation.source() instanceof ConsoleCommandSource) + { +// send(sender, messageComponent("noPermissionConsole")); + return; + } + } + if (!perms.permission().isEmpty()) + { + if (!invocation.source().hasPermission(perms.permission())) + { + return; + } + } + Component component = this.execute(invocation.source(), invocation.source() instanceof ConsoleCommandSource ? null : (Player) invocation.source(), invocation.arguments()); + if (component != null) + { + send(invocation.source(), component); + } + } + + private boolean matches(String label) + { + if (params.name().equalsIgnoreCase(label)) + { + return true; + } + return Arrays.stream(params.aliases()).anyMatch(s -> s.equalsIgnoreCase(label)); + } + + protected void send(Audience audience, Component component) + { + audience.sendMessage(component); + } +} diff --git a/proxy/src/main/java/dev/plex/command/annotation/CommandParameters.java b/proxy/src/main/java/dev/plex/command/annotation/CommandParameters.java new file mode 100644 index 0000000..687b491 --- /dev/null +++ b/proxy/src/main/java/dev/plex/command/annotation/CommandParameters.java @@ -0,0 +1,39 @@ +package dev.plex.command.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Storage for a command's parameters + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface CommandParameters +{ + /** + * The name + * + * @return Name of the command + */ + String name(); + + /** + * The description + * + * @return Description of the command + */ + String description() default ""; + + /** + * The usage (optional) + * + * @return The usage of the command + */ + String usage() default "/"; + + /** + * The aliases (optional) + * + * @return The aliases of the command + */ + String[] aliases() default {}; +} \ No newline at end of file diff --git a/proxy/src/main/java/dev/plex/command/annotation/CommandPermissions.java b/proxy/src/main/java/dev/plex/command/annotation/CommandPermissions.java new file mode 100644 index 0000000..0c67abe --- /dev/null +++ b/proxy/src/main/java/dev/plex/command/annotation/CommandPermissions.java @@ -0,0 +1,27 @@ +package dev.plex.command.annotation; + +import dev.plex.command.source.RequiredCommandSource; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Storage for the command's permissions + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface CommandPermissions +{ + /** + * Required command source + * + * @return The required command source of the command + * @see RequiredCommandSource + */ + RequiredCommandSource source() default RequiredCommandSource.ANY; + + /** + * The permission + * + * @return Permission of the command + */ + String permission() default ""; // No idea what to put here +} \ No newline at end of file diff --git a/proxy/src/main/java/dev/plex/command/source/RequiredCommandSource.java b/proxy/src/main/java/dev/plex/command/source/RequiredCommandSource.java new file mode 100644 index 0000000..2f8ee08 --- /dev/null +++ b/proxy/src/main/java/dev/plex/command/source/RequiredCommandSource.java @@ -0,0 +1,8 @@ +package dev.plex.command.source; + +public enum RequiredCommandSource +{ + IN_GAME, + CONSOLE, + ANY +} \ No newline at end of file diff --git a/proxy/src/main/java/dev/plex/config/TomlConfig.java b/proxy/src/main/java/dev/plex/config/TomlConfig.java new file mode 100644 index 0000000..03e4f44 --- /dev/null +++ b/proxy/src/main/java/dev/plex/config/TomlConfig.java @@ -0,0 +1,94 @@ +package dev.plex.config; + +import dev.plex.Plex; +import dev.plex.toml.Toml; +import dev.plex.toml.TomlWriter; +import lombok.Getter; +import lombok.Setter; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.function.Consumer; + +@Getter +public class TomlConfig +{ + private final File file; + private Toml toml; + + @Setter + private Consumer onCreate; + + @Setter + private Consumer onLoad; + + public TomlConfig(String fileName) + { + this.file = new File(Plex.get().getDataFolder(), fileName); + this.toml = new Toml(); + } + + public void create(boolean loadFromFile) + { + if (loadFromFile) + { + try + { + Files.copy(Plex.get().getClass().getResourceAsStream("/" + this.file.getName()), this.file.toPath()); + this.load(); + if (this.onCreate != null) + { + this.onCreate.accept(this.toml); + } + } catch (IOException e) + { + e.printStackTrace(); + } + return; + } else if (!this.file.exists()) + { + try + { + this.file.createNewFile(); + this.load(); + if (this.onCreate != null) + { + this.onCreate.accept(this.toml); + } + } catch (IOException e) + { + e.printStackTrace(); + } + return; + } + this.load(); + } + + public void load() + { + this.toml = new Toml().read(this.file); + if (onLoad != null) + { + this.onLoad.accept(this.toml); + } + } + + public T as(Class clazz) + { + return this.toml.to(clazz); + } + + public void write(T object) + { + TomlWriter writer = new TomlWriter(); + try + { + writer.write(object, this.file); + this.load(); + } catch (IOException e) + { + e.printStackTrace(); + } + } +} diff --git a/proxy/src/main/java/dev/plex/handlers/ListenerHandler.java b/proxy/src/main/java/dev/plex/handlers/ListenerHandler.java new file mode 100644 index 0000000..88f5450 --- /dev/null +++ b/proxy/src/main/java/dev/plex/handlers/ListenerHandler.java @@ -0,0 +1,33 @@ +package dev.plex.handlers; + +import com.google.common.collect.Lists; +import dev.plex.listener.PlexListener; +import dev.plex.util.PlexLog; +import dev.plex.util.ReflectionsUtil; + +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.Set; + +public class ListenerHandler +{ + public ListenerHandler() + { + Set> listenerSet = ReflectionsUtil.getClassesBySubType("dev.plex.listener.impl", PlexListener.class); + List listeners = Lists.newArrayList(); + + listenerSet.forEach(clazz -> + { + try + { + listeners.add(clazz.getConstructor().newInstance()); + } + catch (InvocationTargetException | InstantiationException | IllegalAccessException | + NoSuchMethodException ex) + { + PlexLog.error("Failed to register " + clazz.getSimpleName() + " as a listener!"); + } + }); + PlexLog.log(String.format("Registered %s listeners from %s classes!", listeners.size(), listenerSet.size())); + } +} diff --git a/proxy/src/main/java/dev/plex/listener/PlexListener.java b/proxy/src/main/java/dev/plex/listener/PlexListener.java new file mode 100644 index 0000000..0113577 --- /dev/null +++ b/proxy/src/main/java/dev/plex/listener/PlexListener.java @@ -0,0 +1,13 @@ +package dev.plex.listener; + +import dev.plex.Plex; + +public class PlexListener +{ + protected final Plex plugin = Plex.get(); + public PlexListener() + { + Plex.get().getServer().getEventManager().register(Plex.get(), this); + } + +} diff --git a/proxy/src/main/java/dev/plex/listener/impl/ServerListener.java b/proxy/src/main/java/dev/plex/listener/impl/ServerListener.java new file mode 100644 index 0000000..af04b8f --- /dev/null +++ b/proxy/src/main/java/dev/plex/listener/impl/ServerListener.java @@ -0,0 +1,42 @@ +package dev.plex.listener.impl; + +import com.velocitypowered.api.event.PostOrder; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.proxy.ProxyPingEvent; +import dev.plex.Plex; +import dev.plex.listener.PlexListener; +import dev.plex.settings.ServerSettings; +import dev.plex.util.PlexLog; +import dev.plex.util.RandomUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; + +import java.util.concurrent.atomic.AtomicReference; + +public class ServerListener extends PlexListener +{ + @Subscribe(order = PostOrder.FIRST) + public void onPing(ProxyPingEvent event) + { + String baseMotd = plugin.getConfig().as(ServerSettings.class).getServer().getMotd(); + baseMotd = baseMotd.replace("\\n", "\n"); + baseMotd = baseMotd.replace("%servername%", plugin.getConfig().as(ServerSettings.class).getServer().getName()); + baseMotd = baseMotd.replace("%mcversion%", plugin.getServer().getVersion().getVersion()); + + PlexLog.log(baseMotd); + + if (plugin.getConfig().as(ServerSettings.class).getServer().isColorizeMotd()) + { + AtomicReference motd = new AtomicReference<>(Component.empty()); + for (final String word : baseMotd.split(" ")) + { + motd.set(motd.get().append(Component.text(word).color(RandomUtil.getRandomColor()))); + motd.set(motd.get().append(Component.space())); + } + event.setPing(event.getPing().asBuilder().description(motd.get()).build()); + } else { + event.setPing(event.getPing().asBuilder().description(MiniMessage.miniMessage().deserialize(baseMotd)).build()); + } + } + +} diff --git a/proxy/src/main/java/dev/plex/settings/ServerSettings.java b/proxy/src/main/java/dev/plex/settings/ServerSettings.java new file mode 100644 index 0000000..52fa66c --- /dev/null +++ b/proxy/src/main/java/dev/plex/settings/ServerSettings.java @@ -0,0 +1,22 @@ +package dev.plex.settings; + +import com.google.common.collect.Lists; +import lombok.Data; +import lombok.Getter; + +import java.util.List; + +@Getter +public class ServerSettings +{ + private final Server server = new Server(); + + @Data + public static class Server { + private String name = "Plexus"; + private String motd = "Test MOTD"; + private boolean colorizeMotd = false; + private boolean debug = false; + private final List sample = Lists.newArrayList("example", "example"); + } +} diff --git a/proxy/src/main/java/dev/plex/toml/ArrayValueReader.java b/proxy/src/main/java/dev/plex/toml/ArrayValueReader.java new file mode 100644 index 0000000..58dc29d --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/ArrayValueReader.java @@ -0,0 +1,77 @@ +package dev.plex.toml; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +public class ArrayValueReader implements ValueReader { + + public static final ArrayValueReader ARRAY_VALUE_READER = new ArrayValueReader(); + @Override + public boolean canRead(String s) { + return s.startsWith("["); + } + + @Override + public Object read(String s, AtomicInteger index, Context context) { + AtomicInteger line = context.line; + int startLine = line.get(); + int startIndex = index.get(); + List arrayItems = new ArrayList(); + boolean terminated = false; + boolean inComment = false; + Results.Errors errors = new Results.Errors(); + + for (int i = index.incrementAndGet(); i < s.length(); i = index.incrementAndGet()) { + + char c = s.charAt(i); + + if (c == '#' && !inComment) { + inComment = true; + } else if (c == '\n') { + inComment = false; + line.incrementAndGet(); + } else if (inComment || Character.isWhitespace(c) || c == ',') { + continue; + } else if (c == '[') { + Object converted = read(s, index, context); + if (converted instanceof Results.Errors) { + errors.add((Results.Errors) converted); + } else if (!isHomogenousArray(converted, arrayItems)) { + errors.heterogenous(context.identifier.getName(), line.get()); + } else { + arrayItems.add(converted); + } + continue; + } else if (c == ']') { + terminated = true; + break; + } else { + Object converted = ValueReaders.VALUE_READERS.convert(s, index, context); + if (converted instanceof Results.Errors) { + errors.add((Results.Errors) converted); + } else if (!isHomogenousArray(converted, arrayItems)) { + errors.heterogenous(context.identifier.getName(), line.get()); + } else { + arrayItems.add(converted); + } + } + } + + if (!terminated) { + errors.unterminated(context.identifier.getName(), s.substring(startIndex, s.length()), startLine); + } + + if (errors.hasErrors()) { + return errors; + } + + return arrayItems; + } + + private boolean isHomogenousArray(Object o, List values) { + return values.isEmpty() || values.get(0).getClass().isAssignableFrom(o.getClass()) || o.getClass().isAssignableFrom(values.get(0).getClass()); + } + + private ArrayValueReader() {} +} diff --git a/proxy/src/main/java/dev/plex/toml/ArrayValueWriter.java b/proxy/src/main/java/dev/plex/toml/ArrayValueWriter.java new file mode 100644 index 0000000..32765d8 --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/ArrayValueWriter.java @@ -0,0 +1,65 @@ +package dev.plex.toml; + + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; + +import static dev.plex.toml.ValueWriters.WRITERS; + +public abstract class ArrayValueWriter implements ValueWriter { + static protected boolean isArrayish(Object value) { + return value instanceof Collection || value.getClass().isArray(); + } + + @Override + public boolean isPrimitiveType() { + return false; + } + + static boolean isArrayOfPrimitive(Object array) { + Object first = peek(array); + if (first != null) { + ValueWriter valueWriter = WRITERS.findWriterFor(first); + return valueWriter.isPrimitiveType() || isArrayish(first); + } + + return true; + } + + @SuppressWarnings("unchecked") + protected Collection normalize(Object value) { + Collection collection; + + if (value.getClass().isArray()) { + // Arrays.asList() interprets an array as a single element, + // so convert it to a list by hand + collection = new ArrayList(Array.getLength(value)); + for (int i = 0; i < Array.getLength(value); i++) { + Object elem = Array.get(value, i); + collection.add(elem); + } + } else { + collection = (Collection) value; + } + + return collection; + } + + private static Object peek(Object value) { + if (value.getClass().isArray()) { + if (Array.getLength(value) > 0) { + return Array.get(value, 0); + } else { + return null; + } + } else { + Collection collection = (Collection) value; + if (collection.size() > 0) { + return collection.iterator().next(); + } + } + + return null; + } +} diff --git a/proxy/src/main/java/dev/plex/toml/BooleanValueReaderWriter.java b/proxy/src/main/java/dev/plex/toml/BooleanValueReaderWriter.java new file mode 100644 index 0000000..f6cd074 --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/BooleanValueReaderWriter.java @@ -0,0 +1,48 @@ +package dev.plex.toml; + +import java.util.concurrent.atomic.AtomicInteger; + + +class BooleanValueReaderWriter implements ValueReader, ValueWriter { + + static final BooleanValueReaderWriter BOOLEAN_VALUE_READER_WRITER = new BooleanValueReaderWriter(); + + @Override + public boolean canRead(String s) { + return s.startsWith("true") || s.startsWith("false"); + } + + @Override + public Object read(String s, AtomicInteger index, Context context) { + s = s.substring(index.get()); + Boolean b = s.startsWith("true") ? Boolean.TRUE : Boolean.FALSE; + + int endIndex = b == Boolean.TRUE ? 4 : 5; + + index.addAndGet(endIndex - 1); + + return b; + } + + @Override + public boolean canWrite(Object value) { + return Boolean.class.isInstance(value); + } + + @Override + public void write(Object value, WriterContext context) { + context.write(value.toString()); + } + + @Override + public boolean isPrimitiveType() { + return true; + } + + private BooleanValueReaderWriter() {} + + @Override + public String toString() { + return "boolean"; + } +} diff --git a/proxy/src/main/java/dev/plex/toml/Container.java b/proxy/src/main/java/dev/plex/toml/Container.java new file mode 100644 index 0000000..63c8727 --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/Container.java @@ -0,0 +1,122 @@ +package dev.plex.toml; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public abstract class Container { + + abstract boolean accepts(String key); + abstract void put(String key, Object value); + abstract Object get(String key); + abstract boolean isImplicit(); + + static class Table extends Container { + private final Map values = new HashMap(); + final String name; + final boolean implicit; + + Table() { + this(null, false); + } + + public Table(String name) { + this(name, false); + } + + public Table(String tableName, boolean implicit) { + this.name = tableName; + this.implicit = implicit; + } + + @Override + boolean accepts(String key) { + return !values.containsKey(key) || values.get(key) instanceof TableArray; + } + + @Override + void put(String key, Object value) { + values.put(key, value); + } + + @Override + Object get(String key) { + return values.get(key); + } + + boolean isImplicit() { + return implicit; + } + + /** + * This modifies the Table's internal data structure, such that it is no longer usable. + * + * Therefore, this method must only be called when all data has been gathered. + + * @return A Map-and-List-based of the TOML data + */ + Map consume() { + for (Map.Entry entry : values.entrySet()) { + if (entry.getValue() instanceof Table) { + entry.setValue(((Table) entry.getValue()).consume()); + } else if (entry.getValue() instanceof TableArray) { + entry.setValue(((TableArray) entry.getValue()).getValues()); + } + } + + return values; + } + + @Override + public String toString() { + return values.toString(); + } + } + + static class TableArray extends Container { + private final List values = new ArrayList
(); + + TableArray() { + values.add(new Table()); + } + + @Override + boolean accepts(String key) { + return getCurrent().accepts(key); + } + + @Override + void put(String key, Object value) { + values.add((Table) value); + } + + @Override + Object get(String key) { + throw new UnsupportedOperationException(); + } + + boolean isImplicit() { + return false; + } + + List> getValues() { + ArrayList> unwrappedValues = new ArrayList>(); + for (Table table : values) { + unwrappedValues.add(table.consume()); + } + return unwrappedValues; + } + + Table getCurrent() { + return values.get(values.size() - 1); + } + + @Override + public String toString() { + return values.toString(); + } + } + + private Container() {} +} diff --git a/proxy/src/main/java/dev/plex/toml/Context.java b/proxy/src/main/java/dev/plex/toml/Context.java new file mode 100644 index 0000000..c42e81c --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/Context.java @@ -0,0 +1,19 @@ +package dev.plex.toml; + +import java.util.concurrent.atomic.AtomicInteger; + +public class Context { + final dev.plex.toml.Identifier identifier; + final AtomicInteger line; + final Results.Errors errors; + + public Context(dev.plex.toml.Identifier identifier, AtomicInteger line, Results.Errors errors) { + this.identifier = identifier; + this.line = line; + this.errors = errors; + } + + public Context with(dev.plex.toml.Identifier identifier) { + return new Context(identifier, line, errors); + } +} diff --git a/proxy/src/main/java/dev/plex/toml/DatePolicy.java b/proxy/src/main/java/dev/plex/toml/DatePolicy.java new file mode 100644 index 0000000..4dae6ee --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/DatePolicy.java @@ -0,0 +1,22 @@ +package dev.plex.toml; + +import java.util.TimeZone; + +public class DatePolicy { + + private final TimeZone timeZone; + private final boolean showFractionalSeconds; + + DatePolicy(TimeZone timeZone, boolean showFractionalSeconds) { + this.timeZone = timeZone; + this.showFractionalSeconds = showFractionalSeconds; + } + + TimeZone getTimeZone() { + return timeZone; + } + + boolean isShowFractionalSeconds() { + return showFractionalSeconds; + } +} diff --git a/proxy/src/main/java/dev/plex/toml/DateValueReaderWriter.java b/proxy/src/main/java/dev/plex/toml/DateValueReaderWriter.java new file mode 100644 index 0000000..0096a2f --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/DateValueReaderWriter.java @@ -0,0 +1,160 @@ +package dev.plex.toml; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DateValueReaderWriter implements ValueReader, ValueWriter { + + static final DateValueReaderWriter DATE_VALUE_READER_WRITER = new DateValueReaderWriter(); + static final DateValueReaderWriter DATE_PARSER_JDK_6 = new DateConverterJdk6(); + private static final Pattern DATE_REGEX = Pattern.compile("(\\d{4}-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9])(\\.\\d*)?(Z|(?:[+\\-]\\d{2}:\\d{2}))(.*)"); + + @Override + public boolean canRead(String s) { + if (s.length() < 5) { + return false; + } + + for (int i = 0; i < 5; i++) { + char c = s.charAt(i); + + if (i < 4) { + if (!Character.isDigit(c)) { + return false; + } + } else if (c != '-') { + return false; + } + } + + return true; + } + + @Override + public Object read(String original, AtomicInteger index, Context context) { + StringBuilder sb = new StringBuilder(); + + for (int i = index.get(); i < original.length(); i = index.incrementAndGet()) { + char c = original.charAt(i); + if (Character.isDigit(c) || c == '-' || c == '+' || c == ':' || c == '.' || c == 'T' || c == 'Z') { + sb.append(c); + } else { + index.decrementAndGet(); + break; + } + } + + String s = sb.toString(); + Matcher matcher = DATE_REGEX.matcher(s); + + if (!matcher.matches()) { + Results.Errors errors = new Results.Errors(); + errors.invalidValue(context.identifier.getName(), s, context.line.get()); + return errors; + } + + String dateString = matcher.group(1); + String zone = matcher.group(3); + String fractionalSeconds = matcher.group(2); + String format = "yyyy-MM-dd'T'HH:mm:ss"; + if (fractionalSeconds != null && !fractionalSeconds.isEmpty()) { + format += ".SSS"; + dateString += fractionalSeconds; + } + format += "Z"; + if ("Z".equals(zone)) { + dateString += "+0000"; + } else if (zone.contains(":")) { + dateString += zone.replace(":", ""); + } + + try { + SimpleDateFormat dateFormat = new SimpleDateFormat(format); + dateFormat.setLenient(false); + return dateFormat.parse(dateString); + } catch (Exception e) { + Results.Errors errors = new Results.Errors(); + errors.invalidValue(context.identifier.getName(), s, context.line.get()); + return errors; + } + } + + @Override + public boolean canWrite(Object value) { + return value instanceof Date; + } + + @Override + public void write(Object value, WriterContext context) { + DateFormat formatter = getFormatter(context.getDatePolicy()); + context.write(formatter.format(value)); + } + + @Override + public boolean isPrimitiveType() { + return true; + } + + private DateFormat getFormatter(DatePolicy datePolicy) { + boolean utc = "UTC".equals(datePolicy.getTimeZone().getID()); + String format; + + if (utc && datePolicy.isShowFractionalSeconds()) { + format = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; + } else if (utc) { + format = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + } else if (datePolicy.isShowFractionalSeconds()) { + format = getTimeZoneAndFractionalSecondsFormat(); + } else { + format = getTimeZoneFormat(); + } + SimpleDateFormat formatter = new SimpleDateFormat(format); + formatter.setTimeZone(datePolicy.getTimeZone()); + + return formatter; + } + + String getTimeZoneFormat() { + return "yyyy-MM-dd'T'HH:mm:ssXXX"; + } + + String getTimeZoneAndFractionalSecondsFormat() { + return "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"; + } + + private DateValueReaderWriter() {} + + private static class DateConverterJdk6 extends DateValueReaderWriter { + @Override + public void write(Object value, WriterContext context) { + DateFormat formatter = super.getFormatter(context.getDatePolicy()); + String date = formatter.format(value); + + if ("UTC".equals(context.getDatePolicy().getTimeZone().getID())) { + context.write(date); + } else { + int insertionIndex = date.length() - 2; + context.write(date.substring(0, insertionIndex)).write(':').write(date.substring(insertionIndex)); + } + } + + @Override + String getTimeZoneAndFractionalSecondsFormat() { + return "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; + } + + @Override + String getTimeZoneFormat() { + return "yyyy-MM-dd'T'HH:mm:ssZ"; + } + } + + @Override + public String toString() { + return "datetime"; + } +} diff --git a/proxy/src/main/java/dev/plex/toml/Identifier.java b/proxy/src/main/java/dev/plex/toml/Identifier.java new file mode 100644 index 0000000..d0abcc2 --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/Identifier.java @@ -0,0 +1,331 @@ +package dev.plex.toml; + +public class Identifier +{ + + static final Identifier INVALID = new Identifier("", null); + + private static final String ALLOWED_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890_-"; + + private final String name; + private final Type type; + + static Identifier from(String name, Context context) + { + Type type; + boolean valid; + name = name.trim(); + if (name.startsWith("[[")) + { + type = Type.TABLE_ARRAY; + valid = isValidTableArray(name, context); + } else if (name.startsWith("[")) + { + type = Type.TABLE; + valid = isValidTable(name, context); + } else + { + type = Type.KEY; + valid = isValidKey(name, context); + } + + if (!valid) + { + return Identifier.INVALID; + } + + return new Identifier(extractName(name), type); + } + + private Identifier(String name, Type type) + { + this.name = name; + this.type = type; + } + + String getName() + { + return name; + } + + String getBareName() + { + if (isKey()) + { + return name; + } + + if (isTable()) + { + return name.substring(1, name.length() - 1); + } + + return name.substring(2, name.length() - 2); + } + + boolean isKey() + { + return type == Type.KEY; + } + + boolean isTable() + { + return type == Type.TABLE; + } + + boolean isTableArray() + { + return type == Type.TABLE_ARRAY; + } + + private static enum Type + { + KEY, TABLE, TABLE_ARRAY; + } + + private static String extractName(String raw) + { + boolean quoted = false; + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < raw.length(); i++) + { + char c = raw.charAt(i); + if (c == '"' && (i == 0 || raw.charAt(i - 1) != '\\')) + { + quoted = !quoted; + sb.append('"'); + } else if (quoted || !Character.isWhitespace(c)) + { + sb.append(c); + } + } + + return StringValueReaderWriter.STRING_VALUE_READER_WRITER.replaceUnicodeCharacters(sb.toString()); + } + + private static boolean isValidKey(String name, Context context) + { + if (name.trim().isEmpty()) + { + context.errors.invalidKey(name, context.line.get()); + return false; + } + + boolean quoted = false; + for (int i = 0; i < name.length(); i++) + { + char c = name.charAt(i); + + if (c == '"' && (i == 0 || name.charAt(i - 1) != '\\')) + { + if (!quoted && i > 0 && name.charAt(i - 1) != '.') + { + context.errors.invalidKey(name, context.line.get()); + return false; + } + quoted = !quoted; + } else if (!quoted && (ALLOWED_CHARS.indexOf(c) == -1)) + { + context.errors.invalidKey(name, context.line.get()); + return false; + } + } + + return true; + } + + private static boolean isValidTable(String name, Context context) + { + boolean valid = true; + + if (!name.endsWith("]")) + { + valid = false; + } + + String trimmed = name.substring(1, name.length() - 1).trim(); + if (trimmed.isEmpty() || trimmed.charAt(0) == '.' || trimmed.endsWith(".")) + { + valid = false; + } + + if (!valid) + { + context.errors.invalidTable(name, context.line.get()); + return false; + } + + boolean quoted = false; + boolean dotAllowed = false; + boolean quoteAllowed = true; + boolean charAllowed = true; + + for (int i = 0; i < trimmed.length(); i++) + { + char c = trimmed.charAt(i); + + if (!valid) + { + break; + } + + if (dev.plex.toml.Keys.isQuote(c)) + { + if (!quoteAllowed) + { + valid = false; + } else if (quoted && trimmed.charAt(i - 1) != '\\') + { + charAllowed = false; + dotAllowed = true; + quoteAllowed = false; + } else if (!quoted) + { + quoted = true; + quoteAllowed = true; + } + } else if (quoted) + { + continue; + } else if (c == '.') + { + if (dotAllowed) + { + charAllowed = true; + dotAllowed = false; + quoteAllowed = true; + } else + { + context.errors.emptyImplicitTable(name, context.line.get()); + return false; + } + } else if (Character.isWhitespace(c)) + { + char prev = trimmed.charAt(i - 1); + if (!Character.isWhitespace(prev) && prev != '.') + { + charAllowed = false; + dotAllowed = true; + quoteAllowed = true; + } + } else + { + if (charAllowed && ALLOWED_CHARS.indexOf(c) > -1) + { + charAllowed = true; + dotAllowed = true; + quoteAllowed = false; + } else + { + valid = false; + } + } + } + + if (!valid) + { + context.errors.invalidTable(name, context.line.get()); + return false; + } + + return true; + } + + private static boolean isValidTableArray(String line, Context context) + { + boolean valid = true; + + if (!line.endsWith("]]")) + { + valid = false; + } + + String trimmed = line.substring(2, line.length() - 2).trim(); + if (trimmed.isEmpty() || trimmed.charAt(0) == '.' || trimmed.endsWith(".")) + { + valid = false; + } + + if (!valid) + { + context.errors.invalidTableArray(line, context.line.get()); + return false; + } + + boolean quoted = false; + boolean dotAllowed = false; + boolean quoteAllowed = true; + boolean charAllowed = true; + + for (int i = 0; i < trimmed.length(); i++) + { + char c = trimmed.charAt(i); + + if (!valid) + { + break; + } + + if (c == '"') + { + if (!quoteAllowed) + { + valid = false; + } else if (quoted && trimmed.charAt(i - 1) != '\\') + { + charAllowed = false; + dotAllowed = true; + quoteAllowed = false; + } else if (!quoted) + { + quoted = true; + quoteAllowed = true; + } + } else if (quoted) + { + continue; + } else if (c == '.') + { + if (dotAllowed) + { + charAllowed = true; + dotAllowed = false; + quoteAllowed = true; + } else + { + context.errors.emptyImplicitTable(line, context.line.get()); + return false; + } + } else if (Character.isWhitespace(c)) + { + char prev = trimmed.charAt(i - 1); + if (!Character.isWhitespace(prev) && prev != '.' && prev != '"') + { + charAllowed = false; + dotAllowed = true; + quoteAllowed = true; + } + } else + { + if (charAllowed && ALLOWED_CHARS.indexOf(c) > -1) + { + charAllowed = true; + dotAllowed = true; + quoteAllowed = false; + } else + { + valid = false; + } + } + } + + if (!valid) + { + context.errors.invalidTableArray(line, context.line.get()); + return false; + } + + return true; + } +} diff --git a/proxy/src/main/java/dev/plex/toml/IdentifierConverter.java b/proxy/src/main/java/dev/plex/toml/IdentifierConverter.java new file mode 100644 index 0000000..655fa69 --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/IdentifierConverter.java @@ -0,0 +1,62 @@ +package dev.plex.toml; + +import java.util.concurrent.atomic.AtomicInteger; + +public class IdentifierConverter { + + static final IdentifierConverter IDENTIFIER_CONVERTER = new IdentifierConverter(); + + dev.plex.toml.Identifier convert(String s, AtomicInteger index, dev.plex.toml.Context context) { + boolean quoted = false; + StringBuilder name = new StringBuilder(); + boolean terminated = false; + boolean isKey = s.charAt(index.get()) != '['; + boolean isTableArray = !isKey && s.length() > index.get() + 1 && s.charAt(index.get() + 1) == '['; + boolean inComment = false; + + for (int i = index.get(); i < s.length(); i = index.incrementAndGet()) { + char c = s.charAt(i); + if (dev.plex.toml.Keys.isQuote(c) && (i == 0 || s.charAt(i - 1) != '\\')) { + quoted = !quoted; + name.append(c); + } else if (c == '\n') { + index.decrementAndGet(); + break; + } else if (quoted) { + name.append(c); + } else if (c == '=' && isKey) { + terminated = true; + break; + } else if (c == ']' && !isKey) { + if (!isTableArray || s.length() > index.get() + 1 && s.charAt(index.get() + 1) == ']') { + terminated = true; + name.append(']'); + if (isTableArray) { + name.append(']'); + } + } + } else if (terminated && c == '#') { + inComment = true; + } else if (terminated && !Character.isWhitespace(c) && !inComment) { + terminated = false; + break; + } else if (!terminated) { + name.append(c); + } + } + + if (!terminated) { + if (isKey) { + context.errors.unterminatedKey(name.toString(), context.line.get()); + } else { + context.errors.invalidKey(name.toString(), context.line.get()); + } + + return dev.plex.toml.Identifier.INVALID; + } + + return dev.plex.toml.Identifier.from(name.toString(), context); + } + + private IdentifierConverter() {} +} diff --git a/proxy/src/main/java/dev/plex/toml/IndentationPolicy.java b/proxy/src/main/java/dev/plex/toml/IndentationPolicy.java new file mode 100644 index 0000000..7a6facd --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/IndentationPolicy.java @@ -0,0 +1,30 @@ +package dev.plex.toml; + +/** + * Controls how a {@link TomlWriter} indents tables and key/value pairs. + * + * The default policy is to not indent. + */ +public class IndentationPolicy { + private final int tableIndent; + private final int keyValueIndent; + private final int arrayDelimiterPadding; + + IndentationPolicy(int keyIndentation, int tableIndentation, int arrayDelimiterPadding) { + this.keyValueIndent = keyIndentation; + this.tableIndent = tableIndentation; + this.arrayDelimiterPadding = arrayDelimiterPadding; + } + + int getTableIndent() { + return tableIndent; + } + + int getKeyValueIndent() { + return keyValueIndent; + } + + int getArrayDelimiterPadding() { + return arrayDelimiterPadding; + } +} diff --git a/proxy/src/main/java/dev/plex/toml/InlineTableValueReader.java b/proxy/src/main/java/dev/plex/toml/InlineTableValueReader.java new file mode 100644 index 0000000..e1154c1 --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/InlineTableValueReader.java @@ -0,0 +1,75 @@ +package dev.plex.toml; + +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicInteger; + +class InlineTableValueReader implements ValueReader { + + static final InlineTableValueReader INLINE_TABLE_VALUE_READER = new InlineTableValueReader(); + + @Override + public boolean canRead(String s) { + return s.startsWith("{"); + } + + @Override + public Object read(String s, AtomicInteger sharedIndex, Context context) { + AtomicInteger line = context.line; + int startLine = line.get(); + int startIndex = sharedIndex.get(); + boolean inKey = true; + boolean inValue = false; + boolean terminated = false; + StringBuilder currentKey = new StringBuilder(); + HashMap results = new HashMap(); + Results.Errors errors = new Results.Errors(); + + for (int i = sharedIndex.incrementAndGet(); sharedIndex.get() < s.length(); i = sharedIndex.incrementAndGet()) { + char c = s.charAt(i); + + if (inValue && !Character.isWhitespace(c)) { + Object converted = ValueReaders.VALUE_READERS.convert(s, sharedIndex, context.with(dev.plex.toml.Identifier.from(currentKey.toString(), context))); + + if (converted instanceof Results.Errors) { + errors.add((Results.Errors) converted); + return errors; + } + + String currentKeyTrimmed = currentKey.toString().trim(); + Object previous = results.put(currentKeyTrimmed, converted); + + if (previous != null) { + errors.duplicateKey(currentKeyTrimmed, context.line.get()); + return errors; + } + + currentKey = new StringBuilder(); + inValue = false; + } else if (c == ',') { + inKey = true; + inValue = false; + currentKey = new StringBuilder(); + } else if (c == '=') { + inKey = false; + inValue = true; + } else if (c == '}') { + terminated = true; + break; + } else if (inKey) { + currentKey.append(c); + } + } + + if (!terminated) { + errors.unterminated(context.identifier.getName(), s.substring(startIndex), startLine); + } + + if (errors.hasErrors()) { + return errors; + } + + return results; + } + + private InlineTableValueReader() {} +} diff --git a/proxy/src/main/java/dev/plex/toml/Keys.java b/proxy/src/main/java/dev/plex/toml/Keys.java new file mode 100644 index 0000000..7274dc5 --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/Keys.java @@ -0,0 +1,69 @@ +package dev.plex.toml; + +import java.util.ArrayList; +import java.util.List; + +class Keys { + + static class Key { + final String name; + final int index; + final String path; + + Key(String name, int index, Key next) { + this.name = name; + this.index = index; + if (next != null) { + this.path = name + "." + next.path; + } else { + this.path = name; + } + } + } + + static Key[] split(String key) { + List splitKey = new ArrayList(); + StringBuilder current = new StringBuilder(); + boolean quoted = false; + boolean indexable = true; + boolean inIndex = false; + int index = -1; + + for (int i = key.length() - 1; i > -1; i--) { + char c = key.charAt(i); + if (c == ']' && indexable) { + inIndex = true; + continue; + } + indexable = false; + if (c == '[' && inIndex) { + inIndex = false; + index = Integer.parseInt(current.toString()); + current = new StringBuilder(); + continue; + } + if (isQuote(c) && (i == 0 || key.charAt(i - 1) != '\\')) { + quoted = !quoted; + indexable = false; + } + if (c != '.' || quoted) { + current.insert(0, c); + } else { + splitKey.add(0, new Key(current.toString(), index, !splitKey.isEmpty() ? splitKey.get(0) : null)); + indexable = true; + index = -1; + current = new StringBuilder(); + } + } + + splitKey.add(0, new Key(current.toString(), index, !splitKey.isEmpty() ? splitKey.get(0) : null)); + + return splitKey.toArray(new Key[0]); + } + + static boolean isQuote(char c) { + return c == '"' || c == '\''; + } + + private Keys() {} +} diff --git a/proxy/src/main/java/dev/plex/toml/LiteralStringValueReader.java b/proxy/src/main/java/dev/plex/toml/LiteralStringValueReader.java new file mode 100644 index 0000000..bc4feb1 --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/LiteralStringValueReader.java @@ -0,0 +1,47 @@ +package dev.plex.toml; + +import java.util.concurrent.atomic.AtomicInteger; + +public class LiteralStringValueReader implements dev.plex.toml.ValueReader +{ + public static final LiteralStringValueReader LITERAL_STRING_VALUE_READER = new LiteralStringValueReader(); + @Override + public boolean canRead(String s) + { + return s.startsWith("'"); + } + + @Override + public Object read(String s, AtomicInteger index, Context context) + { + int startLine = context.line.get(); + boolean terminated = false; + int startIndex = index.incrementAndGet(); + + for (int i = index.get(); i < s.length(); i = index.incrementAndGet()) + { + char c = s.charAt(i); + + if (c == '\'') + { + terminated = true; + break; + } + } + + if (!terminated) + { + Results.Errors errors = new Results.Errors(); + errors.unterminated(context.identifier.getName(), s.substring(startIndex), startLine); + return errors; + } + + String substring = s.substring(startIndex, index.get()); + + return substring; + } + + private LiteralStringValueReader() + { + } +} diff --git a/proxy/src/main/java/dev/plex/toml/MapValueWriter.java b/proxy/src/main/java/dev/plex/toml/MapValueWriter.java new file mode 100644 index 0000000..61313a8 --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/MapValueWriter.java @@ -0,0 +1,166 @@ +package dev.plex.toml; + + +import java.io.File; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class MapValueWriter implements ValueWriter +{ + static final ValueWriter MAP_VALUE_WRITER = new MapValueWriter(); + + private static final Pattern REQUIRED_QUOTING_PATTERN = Pattern.compile("^.*[^A-Za-z\\d_-].*$"); + + @Override + public boolean canWrite(Object value) + { + return value instanceof Map; + } + + @Override + public void write(Object value, WriterContext context) + { + File file = null; + if (context.file != null) + { + file = context.file; + } + + Map from = (Map) value; + + dev.plex.toml.Toml toml = null; + + if (file != null) + { + toml = new Toml().read(file); + } + + if (hasPrimitiveValues(from, context)) + { + if (context.hasRun) + { + if (toml != null) + { + if (!toml.getValues().containsKey(context.key)) + { + context.writeKey(); + } + } else + { + context.writeKey(); + } + } + + } + + + // Render primitive types and arrays of primitive first so they are + // grouped under the same table (if there is one) + for (Map.Entry entry : from.entrySet()) + { + Object key = entry.getKey(); + Object fromValue = entry.getValue(); + if (fromValue == null) + { + continue; + } + + if (context.hasRun && toml != null) + { + if (context.key != null) + { + if (key.toString().equalsIgnoreCase(context.key)) continue; + if (toml.contains(context.key + "." + key)) continue; + } + } + + ValueWriter valueWriter = ValueWriters.WRITERS.findWriterFor(fromValue); + if (valueWriter.isPrimitiveType()) + { + context.indent(); + context.write(quoteKey(key)).write(" = "); + valueWriter.write(fromValue, context); + context.write('\n'); + } else if (valueWriter == PrimitiveArrayValueWriter.PRIMITIVE_ARRAY_VALUE_WRITER) + { + context.setArrayKey(key.toString()); + context.write(quoteKey(key)).write(" = "); + valueWriter.write(fromValue, context); + context.write('\n'); + } + } + + // Now render (sub)tables and arrays of tables + for (Object key : from.keySet()) + { + Object fromValue = from.get(key); + if (fromValue == null) + { + continue; + } + + if (context.hasRun && toml != null) + { + if (context.key != null) + { + if (key.toString().equalsIgnoreCase(context.key)) continue; + if (toml.contains(context.key + "." + key)) continue; + } + } + + ValueWriter valueWriter = ValueWriters.WRITERS.findWriterFor(fromValue); + if (valueWriter == this || valueWriter == ObjectValueWriter.OBJECT_VALUE_WRITER || valueWriter == TableArrayValueWriter.TABLE_ARRAY_VALUE_WRITER) + { + WriterContext context1 = context.pushTable(quoteKey(key)); + context1.parentName = key.toString(); + context1.hasRun = true; + context1.file = context.file; + valueWriter.write(fromValue, context1); + } + } + } + + @Override + public boolean isPrimitiveType() + { + return false; + } + + private static String quoteKey(Object key) + { + String stringKey = key.toString(); + Matcher matcher = REQUIRED_QUOTING_PATTERN.matcher(stringKey); + if (matcher.matches()) + { + stringKey = "\"" + stringKey + "\""; + } + + return stringKey; + } + + private static boolean hasPrimitiveValues(Map values, WriterContext context) + { + for (Object key : values.keySet()) + { + Object fromValue = values.get(key); + if (fromValue == null) + { + continue; + } + + ValueWriter valueWriter = ValueWriters.WRITERS.findWriterFor(fromValue); + if (valueWriter.isPrimitiveType() || valueWriter == PrimitiveArrayValueWriter.PRIMITIVE_ARRAY_VALUE_WRITER) + { + return true; + } + } + + return false; + } + + + private MapValueWriter() + { + } +} diff --git a/proxy/src/main/java/dev/plex/toml/MultilineLiteralStringValueReader.java b/proxy/src/main/java/dev/plex/toml/MultilineLiteralStringValueReader.java new file mode 100644 index 0000000..a953bca --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/MultilineLiteralStringValueReader.java @@ -0,0 +1,52 @@ +package dev.plex.toml; + +import java.util.concurrent.atomic.AtomicInteger; + +class MultilineLiteralStringValueReader implements dev.plex.toml.ValueReader +{ + + static final MultilineLiteralStringValueReader MULTILINE_LITERAL_STRING_VALUE_READER = new MultilineLiteralStringValueReader(); + + @Override + public boolean canRead(String s) { + return s.startsWith("'''"); + } + + @Override + public Object read(String s, AtomicInteger index, dev.plex.toml.Context context) { + AtomicInteger line = context.line; + int startLine = line.get(); + int originalStartIndex = index.get(); + int startIndex = index.addAndGet(3); + int endIndex = -1; + + if (s.charAt(startIndex) == '\n') { + startIndex = index.incrementAndGet(); + line.incrementAndGet(); + } + + for (int i = startIndex; i < s.length(); i = index.incrementAndGet()) { + char c = s.charAt(i); + + if (c == '\n') { + line.incrementAndGet(); + } + + if (c == '\'' && s.length() > i + 2 && s.charAt(i + 1) == '\'' && s.charAt(i + 2) == '\'') { + endIndex = i; + index.addAndGet(2); + break; + } + } + + if (endIndex == -1) { + Results.Errors errors = new Results.Errors(); + errors.unterminated(context.identifier.getName(), s.substring(originalStartIndex), startLine); + return errors; + } + + return s.substring(startIndex, endIndex); + } + + private MultilineLiteralStringValueReader() {} +} diff --git a/proxy/src/main/java/dev/plex/toml/MultilineStringValueReader.java b/proxy/src/main/java/dev/plex/toml/MultilineStringValueReader.java new file mode 100644 index 0000000..24aeca7 --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/MultilineStringValueReader.java @@ -0,0 +1,57 @@ +package dev.plex.toml; + +import java.util.concurrent.atomic.AtomicInteger; + +class MultilineStringValueReader implements dev.plex.toml.ValueReader +{ + + static final MultilineStringValueReader MULTILINE_STRING_VALUE_READER = new MultilineStringValueReader(); + + @Override + public boolean canRead(String s) { + return s.startsWith("\"\"\""); + } + + @Override + public Object read(String s, AtomicInteger index, dev.plex.toml.Context context) { + AtomicInteger line = context.line; + int startLine = line.get(); + int originalStartIndex = index.get(); + int startIndex = index.addAndGet(3); + int endIndex = -1; + + if (s.charAt(startIndex) == '\n') { + startIndex = index.incrementAndGet(); + line.incrementAndGet(); + } + + for (int i = startIndex; i < s.length(); i = index.incrementAndGet()) { + char c = s.charAt(i); + + if (c == '\n') { + line.incrementAndGet(); + } else if (c == '"' && s.length() > i + 2 && s.charAt(i + 1) == '"' && s.charAt(i + 2) == '"') { + endIndex = i; + index.addAndGet(2); + break; + } + } + + if (endIndex == -1) { + dev.plex.toml.Results.Errors errors = new dev.plex.toml.Results.Errors(); + errors.unterminated(context.identifier.getName(), s.substring(originalStartIndex), startLine); + return errors; + } + + s = s.substring(startIndex, endIndex); + s = s.replaceAll("\\\\\\s+", ""); + s = dev.plex.toml.StringValueReaderWriter.STRING_VALUE_READER_WRITER.replaceUnicodeCharacters(s); + s = dev.plex.toml.StringValueReaderWriter.STRING_VALUE_READER_WRITER.replaceSpecialCharacters(s); + + return s; + } + + private MultilineStringValueReader() { + } + +} diff --git a/proxy/src/main/java/dev/plex/toml/NumberValueReaderWriter.java b/proxy/src/main/java/dev/plex/toml/NumberValueReaderWriter.java new file mode 100644 index 0000000..b2873a9 --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/NumberValueReaderWriter.java @@ -0,0 +1,105 @@ +package dev.plex.toml; + +import java.util.concurrent.atomic.AtomicInteger; + +class NumberValueReaderWriter implements ValueReader, ValueWriter { + static final NumberValueReaderWriter NUMBER_VALUE_READER_WRITER = new NumberValueReaderWriter(); + + @Override + public boolean canRead(String s) { + char firstChar = s.charAt(0); + + return firstChar == '+' || firstChar == '-' || Character.isDigit(firstChar); + } + + @Override + public Object read(String s, AtomicInteger index, Context context) { + boolean signable = true; + boolean dottable = false; + boolean exponentable = false; + boolean terminatable = false; + boolean underscorable = false; + String type = ""; + StringBuilder sb = new StringBuilder(); + + for (int i = index.get(); i < s.length(); i = index.incrementAndGet()) { + char c = s.charAt(i); + boolean notLastChar = s.length() > i + 1; + + if (Character.isDigit(c)) { + sb.append(c); + signable = false; + terminatable = true; + if (type.isEmpty()) { + type = "integer"; + dottable = true; + } + underscorable = notLastChar; + exponentable = !type.equals("exponent"); + } else if ((c == '+' || c == '-') && signable && notLastChar) { + signable = false; + terminatable = false; + if (c == '-') { + sb.append('-'); + } + } else if (c == '.' && dottable && notLastChar) { + sb.append('.'); + type = "float"; + terminatable = false; + dottable = false; + exponentable = false; + underscorable = false; + } else if ((c == 'E' || c == 'e') && exponentable && notLastChar) { + sb.append('E'); + type = "exponent"; + terminatable = false; + signable = true; + dottable = false; + exponentable = false; + underscorable = false; + } else if (c == '_' && underscorable && notLastChar && Character.isDigit(s.charAt(i + 1))) { + underscorable = false; + } else { + if (!terminatable) { + type = ""; + } + index.decrementAndGet(); + break; + } + } + + if (type.equals("integer")) { + return Long.valueOf(sb.toString()); + } else if (type.equals("float")) { + return Double.valueOf(sb.toString()); + } else if (type.equals("exponent")) { + String[] exponentString = sb.toString().split("E"); + + return Double.parseDouble(exponentString[0]) * Math.pow(10, Double.parseDouble(exponentString[1])); + } else { + Results.Errors errors = new Results.Errors(); + errors.invalidValue(context.identifier.getName(), sb.toString(), context.line.get()); + return errors; + } + } + + @Override + public boolean canWrite(Object value) { + return Number.class.isInstance(value); + } + + @Override + public void write(Object value, WriterContext context) { + context.write(value.toString()); + } + + @Override + public boolean isPrimitiveType() { + return true; + } + + @Override + public String toString() { + return "number"; + } +} diff --git a/proxy/src/main/java/dev/plex/toml/ObjectValueWriter.java b/proxy/src/main/java/dev/plex/toml/ObjectValueWriter.java new file mode 100644 index 0000000..b87eaa3 --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/ObjectValueWriter.java @@ -0,0 +1,69 @@ +package dev.plex.toml; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.*; + +import static dev.plex.toml.MapValueWriter.MAP_VALUE_WRITER; + +class ObjectValueWriter implements dev.plex.toml.ValueWriter +{ + static final dev.plex.toml.ValueWriter OBJECT_VALUE_WRITER = new ObjectValueWriter(); + + @Override + public boolean canWrite(Object value) { + return true; + } + + @Override + public void write(Object value, dev.plex.toml.WriterContext context) { + Map to = new LinkedHashMap(); + Set fields = getFields(value.getClass()); + for (Field field : fields) { + to.put(field.getName(), getFieldValue(field, value)); + } + + MAP_VALUE_WRITER.write(to, context); + } + + @Override + public boolean isPrimitiveType() { + return false; + } + + private static Set getFields(Class cls) { + Set fields = new LinkedHashSet(Arrays.asList(cls.getDeclaredFields())); + while (cls != Object.class) { + fields.addAll(Arrays.asList(cls.getDeclaredFields())); + cls = cls.getSuperclass(); + } + removeConstantsAndSyntheticFields(fields); + + return fields; + } + + private static void removeConstantsAndSyntheticFields(Set fields) { + Iterator iterator = fields.iterator(); + while (iterator.hasNext()) { + Field field = iterator.next(); + if ((Modifier.isFinal(field.getModifiers()) && Modifier.isStatic(field.getModifiers())) || field.isSynthetic() || Modifier.isTransient(field.getModifiers())) { + iterator.remove(); + } + } + } + + private static Object getFieldValue(Field field, Object o) { + boolean isAccessible = field.isAccessible(); + field.setAccessible(true); + Object value = null; + try { + value = field.get(o); + } catch (IllegalAccessException ignored) { + } + field.setAccessible(isAccessible); + + return value; + } + + private ObjectValueWriter() {} +} diff --git a/proxy/src/main/java/dev/plex/toml/PrimitiveArrayValueWriter.java b/proxy/src/main/java/dev/plex/toml/PrimitiveArrayValueWriter.java new file mode 100644 index 0000000..69a03a6 --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/PrimitiveArrayValueWriter.java @@ -0,0 +1,55 @@ +package dev.plex.toml; + +import java.util.Collection; + +import static dev.plex.toml.ValueWriters.WRITERS; + +class PrimitiveArrayValueWriter extends dev.plex.toml.ArrayValueWriter +{ + static final ValueWriter PRIMITIVE_ARRAY_VALUE_WRITER = new PrimitiveArrayValueWriter(); + + @Override + public boolean canWrite(Object value) { + return isArrayish(value) && isArrayOfPrimitive(value); + } + + @Override + public void write(Object o, WriterContext context) { + Collection values = normalize(o); + + context.write('['); + context.writeArrayDelimiterPadding(); + + boolean first = true; + ValueWriter firstWriter = null; + + for (Object value : values) { + if (first) { + firstWriter = WRITERS.findWriterFor(value); + first = false; + } else { + ValueWriter writer = WRITERS.findWriterFor(value); + if (writer != firstWriter) { + throw new IllegalStateException( + context.getContextPath() + + ": cannot write a heterogeneous array; first element was of type " + firstWriter + + " but found " + writer + ); + } + context.write(", "); + } + + WRITERS.findWriterFor(value).write(value, context); + } + + context.writeArrayDelimiterPadding(); + context.write(']'); + } + + private PrimitiveArrayValueWriter() {} + + @Override + public String toString() { + return "primitive-array"; + } +} diff --git a/proxy/src/main/java/dev/plex/toml/Results.java b/proxy/src/main/java/dev/plex/toml/Results.java new file mode 100644 index 0000000..1de7a11 --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/Results.java @@ -0,0 +1,290 @@ +package dev.plex.toml; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +class Results { + + static class Errors { + + private final StringBuilder sb = new StringBuilder(); + + void duplicateTable(String table, int line) { + sb.append("Duplicate table definition on line ") + .append(line) + .append(": [") + .append(table) + .append("]"); + } + + public void tableDuplicatesKey(String table, AtomicInteger line) { + sb.append("Key already exists for table defined on line ") + .append(line.get()) + .append(": [") + .append(table) + .append("]"); + } + + public void keyDuplicatesTable(String key, AtomicInteger line) { + sb.append("Table already exists for key defined on line ") + .append(line.get()) + .append(": ") + .append(key); + } + + void emptyImplicitTable(String table, int line) { + sb.append("Invalid table definition due to empty implicit table name: ") + .append(table); + } + + void invalidTable(String table, int line) { + sb.append("Invalid table definition on line ") + .append(line) + .append(": ") + .append(table) + .append("]"); + } + + void duplicateKey(String key, int line) { + sb.append("Duplicate key"); + if (line > -1) { + sb.append(" on line ") + .append(line); + } + sb.append(": ") + .append(key); + } + + void invalidTextAfterIdentifier(dev.plex.toml.Identifier identifier, char text, int line) { + sb.append("Invalid text after key ") + .append(identifier.getName()) + .append(" on line ") + .append(line) + .append(". Make sure to terminate the value or add a comment (#)."); + } + + void invalidKey(String key, int line) { + sb.append("Invalid key on line ") + .append(line) + .append(": ") + .append(key); + } + + void invalidTableArray(String tableArray, int line) { + sb.append("Invalid table array definition on line ") + .append(line) + .append(": ") + .append(tableArray); + } + + void invalidValue(String key, String value, int line) { + sb.append("Invalid value on line ") + .append(line) + .append(": ") + .append(key) + .append(" = ") + .append(value); + } + + void unterminatedKey(String key, int line) { + sb.append("Key is not followed by an equals sign on line ") + .append(line) + .append(": ") + .append(key); + } + + void unterminated(String key, String value, int line) { + sb.append("Unterminated value on line ") + .append(line) + .append(": ") + .append(key) + .append(" = ") + .append(value.trim()); + } + + public void heterogenous(String key, int line) { + sb.append(key) + .append(" becomes a heterogeneous array on line ") + .append(line); + } + + boolean hasErrors() { + return sb.length() > 0; + } + + @Override + public String toString() { + return sb.toString(); + } + + public void add(Errors other) { + sb.append(other.sb); + } + } + + final Errors errors = new Errors(); + private final Set tables = new HashSet(); + private final Deque stack = new ArrayDeque(); + + Results() { + stack.push(new dev.plex.toml.Container.Table("")); + } + + void addValue(String key, Object value, AtomicInteger line) { + dev.plex.toml.Container currentTable = stack.peek(); + + if (value instanceof Map) { + String path = getInlineTablePath(key); + if (path == null) { + startTable(key, line); + } else if (path.isEmpty()) { + startTables(dev.plex.toml.Identifier.from(key, null), line); + } else { + startTables(dev.plex.toml.Identifier.from(path, null), line); + } + @SuppressWarnings("unchecked") + Map valueMap = (Map) value; + for (Map.Entry entry : valueMap.entrySet()) { + addValue(entry.getKey(), entry.getValue(), line); + } + stack.pop(); + } else if (currentTable.accepts(key)) { + currentTable.put(key, value); + } else { + if (currentTable.get(key) instanceof dev.plex.toml.Container) { + errors.keyDuplicatesTable(key, line); + } else { + errors.duplicateKey(key, line != null ? line.get() : -1); + } + } + } + + void startTableArray(dev.plex.toml.Identifier identifier, AtomicInteger line) { + String tableName = identifier.getBareName(); + while (stack.size() > 1) { + stack.pop(); + } + + dev.plex.toml.Keys.Key[] tableParts = dev.plex.toml.Keys.split(tableName); + for (int i = 0; i < tableParts.length; i++) { + String tablePart = tableParts[i].name; + dev.plex.toml.Container currentContainer = stack.peek(); + + if (currentContainer.get(tablePart) instanceof dev.plex.toml.Container.TableArray) { + dev.plex.toml.Container.TableArray currentTableArray = (dev.plex.toml.Container.TableArray) currentContainer.get(tablePart); + stack.push(currentTableArray); + + if (i == tableParts.length - 1) { + currentTableArray.put(tablePart, new dev.plex.toml.Container.Table()); + } + + stack.push(currentTableArray.getCurrent()); + currentContainer = stack.peek(); + } else if (currentContainer.get(tablePart) instanceof dev.plex.toml.Container.Table && i < tableParts.length - 1) { + dev.plex.toml.Container nextTable = (dev.plex.toml.Container) currentContainer.get(tablePart); + stack.push(nextTable); + } else if (currentContainer.accepts(tablePart)) { + dev.plex.toml.Container newContainer = i == tableParts.length - 1 ? new dev.plex.toml.Container.TableArray() : new dev.plex.toml.Container.Table(); + addValue(tablePart, newContainer, line); + stack.push(newContainer); + + if (newContainer instanceof dev.plex.toml.Container.TableArray) { + stack.push(((dev.plex.toml.Container.TableArray) newContainer).getCurrent()); + } + } else { + errors.duplicateTable(tableName, line.get()); + break; + } + } + } + + void startTables(dev.plex.toml.Identifier id, AtomicInteger line) { + String tableName = id.getBareName(); + + while (stack.size() > 1) { + stack.pop(); + } + + dev.plex.toml.Keys.Key[] tableParts = dev.plex.toml.Keys.split(tableName); + for (int i = 0; i < tableParts.length; i++) { + String tablePart = tableParts[i].name; + dev.plex.toml.Container currentContainer = stack.peek(); + if (currentContainer.get(tablePart) instanceof dev.plex.toml.Container) { + dev.plex.toml.Container nextTable = (dev.plex.toml.Container) currentContainer.get(tablePart); + if (i == tableParts.length - 1 && !nextTable.isImplicit()) { + errors.duplicateTable(tableName, line.get()); + return; + } + stack.push(nextTable); + if (stack.peek() instanceof dev.plex.toml.Container.TableArray) { + stack.push(((dev.plex.toml.Container.TableArray) stack.peek()).getCurrent()); + } + } else if (currentContainer.accepts(tablePart)) { + startTable(tablePart, i < tableParts.length - 1, line); + } else { + errors.tableDuplicatesKey(tablePart, line); + break; + } + } + } + + /** + * Warning: After this method has been called, this instance is no longer usable. + */ + Map consume() { + dev.plex.toml.Container values = stack.getLast(); + stack.clear(); + + return ((dev.plex.toml.Container.Table) values).consume(); + } + + private dev.plex.toml.Container startTable(String tableName, AtomicInteger line) { + dev.plex.toml.Container newTable = new dev.plex.toml.Container.Table(tableName); + addValue(tableName, newTable, line); + stack.push(newTable); + + return newTable; + } + + private dev.plex.toml.Container startTable(String tableName, boolean implicit, AtomicInteger line) { + dev.plex.toml.Container newTable = new dev.plex.toml.Container.Table(tableName, implicit); + addValue(tableName, newTable, line); + stack.push(newTable); + + return newTable; + } + + private String getInlineTablePath(String key) { + Iterator descendingIterator = stack.descendingIterator(); + StringBuilder sb = new StringBuilder(); + + while (descendingIterator.hasNext()) { + dev.plex.toml.Container next = descendingIterator.next(); + if (next instanceof dev.plex.toml.Container.TableArray) { + return null; + } + + dev.plex.toml.Container.Table table = (dev.plex.toml.Container.Table) next; + + if (table.name == null) { + break; + } + + if (sb.length() > 0) { + sb.append('.'); + } + + sb.append(table.name); + } + + if (sb.length() > 0) { + sb.append('.'); + } + + sb.append(key) + .insert(0, '[') + .append(']'); + + return sb.toString(); + } +} \ No newline at end of file diff --git a/proxy/src/main/java/dev/plex/toml/StringValueReaderWriter.java b/proxy/src/main/java/dev/plex/toml/StringValueReaderWriter.java new file mode 100644 index 0000000..e6bbcfe --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/StringValueReaderWriter.java @@ -0,0 +1,129 @@ +package dev.plex.toml; + +import java.net.URI; +import java.net.URL; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class StringValueReaderWriter implements ValueReader, dev.plex.toml.ValueWriter +{ + + static final StringValueReaderWriter STRING_VALUE_READER_WRITER = new StringValueReaderWriter(); + private static final Pattern UNICODE_REGEX = Pattern.compile("\\\\[uU](.{4})"); + + static private final String[] specialCharacterEscapes = new String[93]; + + static { + specialCharacterEscapes['\b'] = "\\b"; + specialCharacterEscapes['\t'] = "\\t"; + specialCharacterEscapes['\n'] = "\\n"; + specialCharacterEscapes['\f'] = "\\f"; + specialCharacterEscapes['\r'] = "\\r"; + specialCharacterEscapes['"'] = "\\\""; + specialCharacterEscapes['\\'] = "\\\\"; + } + + @Override + public boolean canRead(String s) { + return s.startsWith("\""); + } + + @Override + public Object read(String s, AtomicInteger index, Context context) { + int startIndex = index.incrementAndGet(); + int endIndex = -1; + + for (int i = index.get(); i < s.length(); i = index.incrementAndGet()) { + char ch = s.charAt(i); + if (ch == '"' && s.charAt(i - 1) != '\\') { + endIndex = i; + break; + } + } + + if (endIndex == -1) { + Results.Errors errors = new Results.Errors(); + errors.unterminated(context.identifier.getName(), s.substring(startIndex - 1), context.line.get()); + return errors; + } + + String raw = s.substring(startIndex, endIndex); + s = replaceUnicodeCharacters(raw); + s = replaceSpecialCharacters(s); + + if (s == null) { + Results.Errors errors = new Results.Errors(); + errors.invalidValue(context.identifier.getName(), raw, context.line.get()); + return errors; + } + + return s; + } + + String replaceUnicodeCharacters(String value) { + Matcher unicodeMatcher = UNICODE_REGEX.matcher(value); + + while (unicodeMatcher.find()) { + value = value.replace(unicodeMatcher.group(), new String(Character.toChars(Integer.parseInt(unicodeMatcher.group(1), 16)))); + } + return value; + } + + String replaceSpecialCharacters(String s) { + for (int i = 0; i < s.length() - 1; i++) { + char ch = s.charAt(i); + char next = s.charAt(i + 1); + + if (ch == '\\' && next == '\\') { + i++; + } else if (ch == '\\' && !(next == 'b' || next == 'f' || next == 'n' || next == 't' || next == 'r' || next == '"' || next == '\\')) { + return null; + } + } + + return s.replace("\\n", "\n") + .replace("\\\"", "\"") + .replace("\\t", "\t") + .replace("\\r", "\r") + .replace("\\\\", "\\") + .replace("\\/", "/") + .replace("\\b", "\b") + .replace("\\f", "\f"); + } + + @Override + public boolean canWrite(Object value) { + return value instanceof String || value instanceof Character || value instanceof URL || value instanceof URI || value instanceof Enum; + } + + @Override + public void write(Object value, dev.plex.toml.WriterContext context) { + context.write('"'); + escapeUnicode(value.toString(), context); + context.write('"'); + } + + @Override + public boolean isPrimitiveType() { + return true; + } + + private void escapeUnicode(String in, dev.plex.toml.WriterContext context) { + for (int i = 0; i < in.length(); i++) { + int codePoint = in.codePointAt(i); + if (codePoint < specialCharacterEscapes.length && specialCharacterEscapes[codePoint] != null) { + context.write(specialCharacterEscapes[codePoint]); + } else { + context.write(in.charAt(i)); + } + } + } + + private StringValueReaderWriter() {} + + @Override + public String toString() { + return "string"; + } +} diff --git a/proxy/src/main/java/dev/plex/toml/TableArrayValueWriter.java b/proxy/src/main/java/dev/plex/toml/TableArrayValueWriter.java new file mode 100644 index 0000000..1684ffd --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/TableArrayValueWriter.java @@ -0,0 +1,33 @@ +package dev.plex.toml; + +import java.util.Collection; + +import static dev.plex.toml.ValueWriters.WRITERS; + +class TableArrayValueWriter extends dev.plex.toml.ArrayValueWriter +{ + static final dev.plex.toml.ValueWriter TABLE_ARRAY_VALUE_WRITER = new TableArrayValueWriter(); + + @Override + public boolean canWrite(Object value) { + return isArrayish(value) && !isArrayOfPrimitive(value); + } + + @Override + public void write(Object from, dev.plex.toml.WriterContext context) { + Collection values = normalize(from); + + dev.plex.toml.WriterContext subContext = context.pushTableFromArray(); + + for (Object value : values) { + WRITERS.findWriterFor(value).write(value, subContext); + } + } + + private TableArrayValueWriter() {} + + @Override + public String toString() { + return "table-array"; + } +} diff --git a/proxy/src/main/java/dev/plex/toml/Toml.java b/proxy/src/main/java/dev/plex/toml/Toml.java new file mode 100644 index 0000000..09b49a3 --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/Toml.java @@ -0,0 +1,481 @@ +package dev.plex.toml; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import lombok.Getter; + +import java.io.*; +import java.util.*; + +/** + *

Provides access to the keys and tables in a TOML data source.

+ * + *

All getters can fall back to default values if they have been provided as a constructor argument. + * Getters for simple values (String, Date, etc.) will return null if no matching key exists. + * {@link #getList(String)}, {@link #getTable(String)} and {@link #getTables(String)} return empty values if there is no matching key.

+ * + *

All read methods throw an {@link IllegalStateException} if the TOML is incorrect.

+ * + *

Example usage:

+ *

+ * Toml toml = new Toml().read(getTomlFile());
+ * String name = toml.getString("name");
+ * Long port = toml.getLong("server.ip"); // compound key. Is equivalent to:
+ * Long port2 = toml.getTable("server").getLong("ip");
+ * MyConfig config = toml.to(MyConfig.class);
+ * 
+ */ +public class Toml +{ + + private static final Gson DEFAULT_GSON = new Gson(); + + @Getter + private Map values = new HashMap(); + private final Toml defaults; + + /** + * Creates Toml instance with no defaults. + */ + public Toml() + { + this(null); + } + + /** + * @param defaults fallback values used when the requested key or table is not present in the TOML source that has been read. + */ + public Toml(Toml defaults) + { + this(defaults, new HashMap<>()); + } + + /** + * Populates the current Toml instance with values from file. + * + * @param file The File to be read. Expected to be encoded as UTF-8. + * @return this instance + * @throws IllegalStateException If file contains invalid TOML + */ + public Toml read(File file) + { + try + { + return read(new InputStreamReader(new FileInputStream(file), "UTF8")); + } catch (Exception e) + { + throw new RuntimeException(e); + } + } + + /** + * Populates the current Toml instance with values from inputStream. + * + * @param inputStream Closed after it has been read. + * @return this instance + * @throws IllegalStateException If file contains invalid TOML + */ + public Toml read(InputStream inputStream) + { + return read(new InputStreamReader(inputStream)); + } + + /** + * Populates the current Toml instance with values from reader. + * + * @param reader Closed after it has been read. + * @return this instance + * @throws IllegalStateException If file contains invalid TOML + */ + public Toml read(Reader reader) + { + BufferedReader bufferedReader = null; + try + { + bufferedReader = new BufferedReader(reader); + + StringBuilder w = new StringBuilder(); + String line = bufferedReader.readLine(); + while (line != null) + { + w.append(line).append('\n'); + line = bufferedReader.readLine(); + } + read(w.toString()); + } catch (IOException e) + { + throw new RuntimeException(e); + } finally + { + try + { + bufferedReader.close(); + } catch (IOException e) + { + } + } + return this; + } + + /** + * Populates the current Toml instance with values from otherToml. + * + * @param otherToml + * @return this instance + */ + public Toml read(Toml otherToml) + { + this.values = otherToml.values; + + return this; + } + + /** + * Populates the current Toml instance with values from tomlString. + * + * @param tomlString String to be read. + * @return this instance + * @throws IllegalStateException If tomlString is not valid TOML + */ + public Toml read(String tomlString) throws IllegalStateException + { + Results results = TomlParser.run(tomlString); + if (results.errors.hasErrors()) + { + throw new IllegalStateException(results.errors.toString()); + } + + this.values = results.consume(); + + return this; + } + + public String getString(String key) + { + return (String) get(key); + } + + public String getString(String key, String defaultValue) + { + String val = getString(key); + return val == null ? defaultValue : val; + } + + public Long getLong(String key) + { + return (Long) get(key); + } + + public Long getLong(String key, Long defaultValue) + { + Long val = getLong(key); + return val == null ? defaultValue : val; + } + + /** + * @param key a TOML key + * @param type of list items + * @return null if the key is not found + */ + public List getList(String key) + { + @SuppressWarnings("unchecked") + List list = (List) get(key); + + return list; + } + + /** + * @param key a TOML key + * @param defaultValue a list of default values + * @param type of list items + * @return null is the key is not found + */ + public List getList(String key, List defaultValue) + { + List list = getList(key); + + return list != null ? list : defaultValue; + } + + public Boolean getBoolean(String key) + { + return (Boolean) get(key); + } + + public Boolean getBoolean(String key, Boolean defaultValue) + { + Boolean val = getBoolean(key); + return val == null ? defaultValue : val; + } + + public Date getDate(String key) + { + return (Date) get(key); + } + + public Date getDate(String key, Date defaultValue) + { + Date val = getDate(key); + return val == null ? defaultValue : val; + } + + public Double getDouble(String key) + { + return (Double) get(key); + } + + public Double getDouble(String key, Double defaultValue) + { + Double val = getDouble(key); + return val == null ? defaultValue : val; + } + + /** + * @param key A table name, not including square brackets. + * @return A new Toml instance or null if no value is found for key. + */ + @SuppressWarnings("unchecked") + public Toml getTable(String key) + { + Map map = (Map) get(key); + + return map != null ? new Toml(null, map) : null; + } + + /** + * @param key Name of array of tables, not including square brackets. + * @return A {@link List} of Toml instances or null if no value is found for key. + */ + @SuppressWarnings("unchecked") + public List getTables(String key) + { + List> tableArray = (List>) get(key); + + if (tableArray == null) + { + return null; + } + + ArrayList tables = new ArrayList(); + + for (Map table : tableArray) + { + tables.add(new Toml(null, table)); + } + + return tables; + } + + /** + * @param key a key name, can be compound (eg. a.b.c) + * @return true if key is present + */ + public boolean contains(String key) + { + return get(key) != null; + } + + /** + * @param key a key name, can be compound (eg. a.b.c) + * @return true if key is present and is a primitive + */ + public boolean containsPrimitive(String key) + { + Object object = get(key); + + return object != null && !(object instanceof Map) && !(object instanceof List); + } + + /** + * @param key a key name, can be compound (eg. a.b.c) + * @return true if key is present and is a table + */ + public boolean containsTable(String key) + { + Object object = get(key); + + return object != null && (object instanceof Map); + } + + /** + * @param key a key name, can be compound (eg. a.b.c) + * @return true if key is present and is a table array + */ + public boolean containsTableArray(String key) + { + Object object = get(key); + + return object != null && (object instanceof List); + } + + public boolean isEmpty() + { + return values.isEmpty(); + } + + /** + *

+ * Populates an instance of targetClass with the values of this Toml instance. + * The target's field names must match keys or tables. + * Keys not present in targetClass will be ignored. + *

+ * + *

Tables are recursively converted to custom classes or to {@link Map Map<String, Object>}.

+ * + *

In addition to straight-forward conversion of TOML primitives, the following are also available:

+ * + *
    + *
  • Integer -> int, long (or wrapper), {@link java.math.BigInteger}
  • + *
  • Float -> float, double (or wrapper), {@link java.math.BigDecimal}
  • + *
  • One-letter String -> char, {@link Character}
  • + *
  • String -> {@link String}, enum, {@link java.net.URI}, {@link java.net.URL}
  • + *
  • Multiline and Literal Strings -> {@link String}
  • + *
  • Array -> {@link List}, {@link Set}, array. The generic type can be anything that can be converted.
  • + *
  • Table -> Custom class, {@link Map Map<String, Object>}
  • + *
+ * + * @param targetClass Class to deserialize TOML to. + * @param type of targetClass. + * @return A new instance of targetClass. + */ + public T to(Class targetClass) + { + JsonElement json = DEFAULT_GSON.toJsonTree(toMap()); + + if (targetClass == JsonElement.class) + { + return targetClass.cast(json); + } + + return DEFAULT_GSON.fromJson(json, targetClass); + } + + public Map toMap() + { + HashMap valuesCopy = new HashMap(values); + + if (defaults != null) + { + for (Map.Entry entry : defaults.values.entrySet()) + { + if (!valuesCopy.containsKey(entry.getKey())) + { + valuesCopy.put(entry.getKey(), entry.getValue()); + } + } + } + + return valuesCopy; + } + + /** + * @return a {@link Set} of Map.Entry instances. Modifications to the {@link Set} are not reflected in this Toml instance. Entries are immutable, so {@link Map.Entry#setValue(Object)} throws an UnsupportedOperationException. + */ + public Set> entrySet() + { + Set> entries = new LinkedHashSet>(); + + for (Map.Entry entry : values.entrySet()) + { + Class entryClass = entry.getValue().getClass(); + + if (Map.class.isAssignableFrom(entryClass)) + { + entries.add(new Entry(entry.getKey(), getTable(entry.getKey()))); + } else if (List.class.isAssignableFrom(entryClass)) + { + List value = (List) entry.getValue(); + if (!value.isEmpty() && value.get(0) instanceof Map) + { + entries.add(new Entry(entry.getKey(), getTables(entry.getKey()))); + } else + { + entries.add(new Entry(entry.getKey(), value)); + } + } else + { + entries.add(new Entry(entry.getKey(), entry.getValue())); + } + } + + return entries; + } + + private class Entry implements Map.Entry + { + + private final String key; + private final Object value; + + @Override + public String getKey() + { + return key; + } + + @Override + public Object getValue() + { + return value; + } + + @Override + public Object setValue(Object value) + { + throw new UnsupportedOperationException("TOML entry values cannot be changed."); + } + + private Entry(String key, Object value) + { + this.key = key; + this.value = value; + } + } + + @SuppressWarnings("unchecked") + public Object get(String key) + { + if (values.containsKey(key)) + { + return values.get(key); + } + + Object current = new HashMap<>(values); + + Keys.Key[] keys = Keys.split(key); + + for (Keys.Key k : keys) + { + if (k.index == -1 && current instanceof Map && ((Map) current).containsKey(k.path)) + { + return ((Map) current).get(k.path); + } + + current = ((Map) current).get(k.name); + + if (k.index > -1 && current != null) + { + if (k.index >= ((List) current).size()) + { + return null; + } + + current = ((List) current).get(k.index); + } + + if (current == null) + { + return defaults != null ? defaults.get(key) : null; + } + } + + return current; + } + + private Toml(Toml defaults, Map values) + { + this.values = values; + this.defaults = defaults; + } +} diff --git a/proxy/src/main/java/dev/plex/toml/TomlParser.java b/proxy/src/main/java/dev/plex/toml/TomlParser.java new file mode 100644 index 0000000..7e9ea43 --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/TomlParser.java @@ -0,0 +1,63 @@ +package dev.plex.toml; + +import java.util.concurrent.atomic.AtomicInteger; + +class TomlParser { + + static Results run(String tomlString) { + final Results results = new Results(); + + if (tomlString.isEmpty()) { + return results; + } + + AtomicInteger index = new AtomicInteger(); + boolean inComment = false; + AtomicInteger line = new AtomicInteger(1); + dev.plex.toml.Identifier identifier = null; + Object value = null; + + for (int i = index.get(); i < tomlString.length(); i = index.incrementAndGet()) { + char c = tomlString.charAt(i); + + if (results.errors.hasErrors()) { + break; + } + + if (c == '#' && !inComment) { + inComment = true; + } else if (!Character.isWhitespace(c) && !inComment && identifier == null) { + dev.plex.toml.Identifier id = IdentifierConverter.IDENTIFIER_CONVERTER.convert(tomlString, index, new dev.plex.toml.Context(null, line, results.errors)); + + if (id != dev.plex.toml.Identifier.INVALID) { + if (id.isKey()) { + identifier = id; + } else if (id.isTable()) { + results.startTables(id, line); + } else if (id.isTableArray()) { + results.startTableArray(id, line); + } + } + } else if (c == '\n') { + inComment = false; + identifier = null; + value = null; + line.incrementAndGet(); + } else if (!inComment && identifier != null && identifier.isKey() && value == null && !Character.isWhitespace(c)) { + value = ValueReaders.VALUE_READERS.convert(tomlString, index, new dev.plex.toml.Context(identifier, line, results.errors)); + + if (value instanceof Results.Errors) { + results.errors.add((Results.Errors) value); + } else { + results.addValue(identifier.getName(), value, line); + } + } else if (value != null && !inComment && !Character.isWhitespace(c)) { + results.errors.invalidTextAfterIdentifier(identifier, c, line.get()); + } + } + + return results; + } + + private TomlParser() {} +} diff --git a/proxy/src/main/java/dev/plex/toml/TomlWriter.java b/proxy/src/main/java/dev/plex/toml/TomlWriter.java new file mode 100644 index 0000000..e6366aa --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/TomlWriter.java @@ -0,0 +1,179 @@ +package dev.plex.toml; + +import org.jetbrains.annotations.Nullable; + +import java.io.*; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +import static dev.plex.toml.MapValueWriter.MAP_VALUE_WRITER; +import static dev.plex.toml.ValueWriters.WRITERS; + +/** + *

Converts Objects to TOML

+ * + *

An input Object can comprise arbitrarily nested combinations of Java primitive types, + * other {@link Object}s, {@link Map}s, {@link List}s, and Arrays. {@link Object}s and {@link Map}s + * are output to TOML tables, and {@link List}s and Array to TOML arrays.

+ * + *

Example usage:

+ *

+ * class AClass {
+ *   int anInt = 1;
+ *   int[] anArray = { 2, 3 };
+ * }
+ *
+ * String tomlString = new TomlWriter().write(new AClass());
+ * 
+ */ +public class TomlWriter +{ + + public static class Builder + { + private int keyIndentation; + private int tableIndentation; + private int arrayDelimiterPadding = 0; + private TimeZone timeZone = TimeZone.getTimeZone("UTC"); + private boolean showFractionalSeconds = false; + + public Builder indentValuesBy(int spaces) + { + this.keyIndentation = spaces; + + return this; + } + + public Builder indentTablesBy(int spaces) + { + this.tableIndentation = spaces; + + return this; + } + + public Builder timeZone(TimeZone timeZone) + { + this.timeZone = timeZone; + + return this; + } + + /** + * @param spaces number of spaces to put between opening square bracket and first item and between closing square bracket and last item + * @return this TomlWriter.Builder instance + */ + public Builder padArrayDelimitersBy(int spaces) + { + this.arrayDelimiterPadding = spaces; + + return this; + } + + public TomlWriter build() + { + return new TomlWriter(keyIndentation, tableIndentation, arrayDelimiterPadding, timeZone, showFractionalSeconds); + } + + public Builder showFractionalSeconds() + { + this.showFractionalSeconds = true; + return this; + } + } + + private final dev.plex.toml.IndentationPolicy indentationPolicy; + private final DatePolicy datePolicy; + + /** + * Creates a TomlWriter instance. + */ + public TomlWriter() + { + this(0, 0, 0, TimeZone.getTimeZone("UTC"), false); + } + + private TomlWriter(int keyIndentation, int tableIndentation, int arrayDelimiterPadding, TimeZone timeZone, boolean showFractionalSeconds) + { + this.indentationPolicy = new dev.plex.toml.IndentationPolicy(keyIndentation, tableIndentation, arrayDelimiterPadding); + this.datePolicy = new DatePolicy(timeZone, showFractionalSeconds); + } + + /** + * Write an Object into TOML String. + * + * @param from the object to be written + * @return a string containing the TOML representation of the given Object + */ + public String write(Object from) + { + try + { + StringWriter output = new StringWriter(); + write(from, output, null); + + return output.toString(); + } catch (IOException e) + { + throw new RuntimeException(e); + } + } + + /** + * Write an Object in TOML to a {@link File}. Output is encoded as UTF-8. + * + * @param from the object to be written + * @param target the File to which the TOML will be written + * @throws IOException if any file operations fail + */ + public void write(Object from, File target) throws IOException + { + OutputStream outputStream = new FileOutputStream(target, true); + try + { + write(from, outputStream, target); + } finally + { + outputStream.close(); + } + } + + /** + * Write an Object in TOML to a {@link OutputStream}. Output is encoded as UTF-8. + * + * @param from the object to be written + * @param target the OutputStream to which the TOML will be written. The stream is NOT closed after being written to. + * @throws IOException if target.write() fails + */ + public void write(Object from, OutputStream target, @Nullable File file) throws IOException + { + OutputStreamWriter writer = new OutputStreamWriter(target, "UTF-8"); + write(from, writer, file); + writer.flush(); + } + + /** + * Write an Object in TOML to a {@link Writer}. You MUST ensure that the {@link Writer}s's encoding is set to UTF-8 for the TOML to be valid. + * + * @param from the object to be written. Can be a Map or a custom type. Must not be null. + * @param target the Writer to which TOML will be written. The Writer is not closed. + * @throws IOException if target.write() fails + * @throws IllegalArgumentException if from is of an invalid type + */ + public void write(Object from, Writer target, @Nullable File file) throws IOException + { + ValueWriter valueWriter = WRITERS.findWriterFor(from); + if (valueWriter == MAP_VALUE_WRITER || valueWriter == ObjectValueWriter.OBJECT_VALUE_WRITER) + { + WriterContext context = new WriterContext(indentationPolicy, datePolicy, target); + if (file != null && file.exists()) + { + context.file = file; + } + valueWriter.write(from, context); + } else + { + throw new IllegalArgumentException("An object of class " + from.getClass().getSimpleName() + " cannot produce valid TOML. Please pass in a Map or a custom type."); + } + } +} diff --git a/proxy/src/main/java/dev/plex/toml/ValueReader.java b/proxy/src/main/java/dev/plex/toml/ValueReader.java new file mode 100644 index 0000000..1e4f1e1 --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/ValueReader.java @@ -0,0 +1,21 @@ +package dev.plex.toml; + +import java.util.concurrent.atomic.AtomicInteger; + +interface ValueReader { + + /** + * @param s must already have been trimmed + */ + boolean canRead(String s); + + /** + * Partial validation. Stops after type terminator, rather than at EOI. + * + * @param s must already have been validated by {@link #canRead(String)} + * @param index where to start in s + * @param line current line number, used for error reporting + * @return a value or a {@link Results.Errors} + */ + Object read(String s, AtomicInteger index, Context context); +} diff --git a/proxy/src/main/java/dev/plex/toml/ValueReaders.java b/proxy/src/main/java/dev/plex/toml/ValueReaders.java new file mode 100644 index 0000000..bbb6bad --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/ValueReaders.java @@ -0,0 +1,37 @@ +package dev.plex.toml; + +import java.util.concurrent.atomic.AtomicInteger; + +import static dev.plex.toml.ArrayValueReader.ARRAY_VALUE_READER; +import static dev.plex.toml.BooleanValueReaderWriter.BOOLEAN_VALUE_READER_WRITER; +import static dev.plex.toml.DateValueReaderWriter.DATE_VALUE_READER_WRITER; +import static dev.plex.toml.InlineTableValueReader.INLINE_TABLE_VALUE_READER; +import static dev.plex.toml.LiteralStringValueReader.LITERAL_STRING_VALUE_READER; +import static dev.plex.toml.MultilineLiteralStringValueReader.MULTILINE_LITERAL_STRING_VALUE_READER; +import static dev.plex.toml.MultilineStringValueReader.MULTILINE_STRING_VALUE_READER; +import static dev.plex.toml.NumberValueReaderWriter.NUMBER_VALUE_READER_WRITER; +import static dev.plex.toml.StringValueReaderWriter.STRING_VALUE_READER_WRITER; + +class ValueReaders { + + static final ValueReaders VALUE_READERS = new ValueReaders(); + + Object convert(String value, AtomicInteger index, dev.plex.toml.Context context) { + String substring = value.substring(index.get()); + for (dev.plex.toml.ValueReader valueParser : READERS) { + if (valueParser.canRead(substring)) { + return valueParser.read(value, index, context); + } + } + + dev.plex.toml.Results.Errors errors = new dev.plex.toml.Results.Errors(); + errors.invalidValue(context.identifier.getName(), substring, context.line.get()); + return errors; + } + + private ValueReaders() {} + + private static final dev.plex.toml.ValueReader[] READERS = { + MULTILINE_STRING_VALUE_READER, MULTILINE_LITERAL_STRING_VALUE_READER, LITERAL_STRING_VALUE_READER, STRING_VALUE_READER_WRITER, DATE_VALUE_READER_WRITER, NUMBER_VALUE_READER_WRITER, BOOLEAN_VALUE_READER_WRITER, ARRAY_VALUE_READER, INLINE_TABLE_VALUE_READER + }; +} diff --git a/proxy/src/main/java/dev/plex/toml/ValueWriter.java b/proxy/src/main/java/dev/plex/toml/ValueWriter.java new file mode 100644 index 0000000..3b62e3e --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/ValueWriter.java @@ -0,0 +1,9 @@ +package dev.plex.toml; + +interface ValueWriter { + boolean canWrite(Object value); + + void write(Object value, dev.plex.toml.WriterContext context); + + boolean isPrimitiveType(); +} diff --git a/proxy/src/main/java/dev/plex/toml/ValueWriters.java b/proxy/src/main/java/dev/plex/toml/ValueWriters.java new file mode 100644 index 0000000..29e1fcd --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/ValueWriters.java @@ -0,0 +1,31 @@ +package dev.plex.toml; + +import static dev.plex.toml.MapValueWriter.MAP_VALUE_WRITER; +import static dev.plex.toml.NumberValueReaderWriter.NUMBER_VALUE_READER_WRITER; + +class ValueWriters { + + static final ValueWriters WRITERS = new ValueWriters(); + + ValueWriter findWriterFor(Object value) { + for (ValueWriter valueWriter : VALUE_WRITERS) { + if (valueWriter.canWrite(value)) { + return valueWriter; + } + } + + return ObjectValueWriter.OBJECT_VALUE_WRITER; + } + + private ValueWriters() {} + + private static DateValueReaderWriter getPlatformSpecificDateConverter() { + String specificationVersion = Runtime.class.getPackage().getSpecificationVersion(); + return specificationVersion != null && specificationVersion.startsWith("1.6") ? DateValueReaderWriter.DATE_PARSER_JDK_6 : DateValueReaderWriter.DATE_VALUE_READER_WRITER; + } + + private static final ValueWriter[] VALUE_WRITERS = { + StringValueReaderWriter.STRING_VALUE_READER_WRITER, NUMBER_VALUE_READER_WRITER, BooleanValueReaderWriter.BOOLEAN_VALUE_READER_WRITER, getPlatformSpecificDateConverter(), + MAP_VALUE_WRITER, PrimitiveArrayValueWriter.PRIMITIVE_ARRAY_VALUE_WRITER, TableArrayValueWriter.TABLE_ARRAY_VALUE_WRITER + }; +} diff --git a/proxy/src/main/java/dev/plex/toml/WriterContext.java b/proxy/src/main/java/dev/plex/toml/WriterContext.java new file mode 100644 index 0000000..a7f3981 --- /dev/null +++ b/proxy/src/main/java/dev/plex/toml/WriterContext.java @@ -0,0 +1,182 @@ +package dev.plex.toml; + +import java.io.File; +import java.io.IOException; +import java.io.Writer; +import java.util.Arrays; + +class WriterContext +{ + private String arrayKey = null; + private boolean isArrayOfTable = false; + private boolean empty = true; + public final String key; + private final String currentTableIndent; + private final String currentFieldIndent; + private final Writer output; + private final dev.plex.toml.IndentationPolicy indentationPolicy; + private final DatePolicy datePolicy; + + public File file; + public String parentName; + public boolean hasRun = false; + + WriterContext(dev.plex.toml.IndentationPolicy indentationPolicy, DatePolicy datePolicy, Writer output) + { + this("", "", output, indentationPolicy, datePolicy); + } + + WriterContext pushTable(String newKey) + { + String newIndent = ""; + if (!key.isEmpty()) + { + newIndent = growIndent(indentationPolicy); + } + + String fullKey = key.isEmpty() ? newKey : key + "." + newKey; + + WriterContext subContext = new WriterContext(fullKey, newIndent, output, indentationPolicy, datePolicy); + if (!empty) + { + subContext.empty = false; + } + + return subContext; + } + + WriterContext pushTableFromArray() + { + WriterContext subContext = new WriterContext(key, currentTableIndent, output, indentationPolicy, datePolicy); + if (!empty) + { + subContext.empty = false; + } + subContext.setIsArrayOfTable(true); + + return subContext; + } + + WriterContext write(String s) + { + try + { + output.write(s); + if (empty && !s.isEmpty()) + { + empty = false; + } + + return this; + } catch (IOException e) + { + throw new RuntimeException(e); + } + } + + void write(char[] chars) + { + for (char c : chars) + { + write(c); + } + } + + WriterContext write(char c) + { + try + { + output.write(c); + empty = false; + + return this; + } catch (IOException e) + { + throw new RuntimeException(e); + } + } + + void writeKey() + { + if (key.isEmpty()) + { + return; + } + + if (!empty) + { + write('\n'); + } + + write(currentTableIndent); + + if (isArrayOfTable) + { + write("[[").write(key).write("]]\n"); + } else + { + write('[').write(key).write("]\n"); + } + } + + void writeArrayDelimiterPadding() + { + for (int i = 0; i < indentationPolicy.getArrayDelimiterPadding(); i++) + { + write(' '); + } + } + + void indent() + { + if (!key.isEmpty()) + { + write(currentFieldIndent); + } + } + + DatePolicy getDatePolicy() + { + return datePolicy; + } + + WriterContext setIsArrayOfTable(boolean isArrayOfTable) + { + this.isArrayOfTable = isArrayOfTable; + return this; + } + + WriterContext setArrayKey(String arrayKey) + { + this.arrayKey = arrayKey; + return this; + } + + String getContextPath() + { + return key.isEmpty() ? arrayKey : key + "." + arrayKey; + } + + private String growIndent(dev.plex.toml.IndentationPolicy indentationPolicy) + { + return currentTableIndent + fillStringWithSpaces(indentationPolicy.getTableIndent()); + } + + private String fillStringWithSpaces(int count) + { + char[] chars = new char[count]; + Arrays.fill(chars, ' '); + + return new String(chars); + } + + private WriterContext(String key, String tableIndent, Writer output, dev.plex.toml.IndentationPolicy indentationPolicy, DatePolicy datePolicy) + { + this.key = key; + this.output = output; + this.indentationPolicy = indentationPolicy; + this.currentTableIndent = tableIndent; + this.datePolicy = datePolicy; + this.currentFieldIndent = tableIndent + fillStringWithSpaces(this.indentationPolicy.getKeyValueIndent()); + } +} diff --git a/proxy/src/main/java/dev/plex/util/PlexLog.java b/proxy/src/main/java/dev/plex/util/PlexLog.java new file mode 100644 index 0000000..0c8c7f4 --- /dev/null +++ b/proxy/src/main/java/dev/plex/util/PlexLog.java @@ -0,0 +1,66 @@ +package dev.plex.util; + +import dev.plex.Plex; +import dev.plex.settings.ServerSettings; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.MiniMessage; + +public class PlexLog +{ + public static void log(String message, Object... strings) + { + for (int i = 0; i < strings.length; i++) + { + if (message.contains("{" + i + "}")) + { + message = message.replace("{" + i + "}", strings[i].toString()); + } + } + Plex.get().getServer().getConsoleCommandSource().sendMessage(MiniMessage.miniMessage().deserialize("[Plex] " + message)); + } + + public static void log(Component component) + { + Plex.get().getServer().getConsoleCommandSource().sendMessage(Component.text("[Plex] ").color(NamedTextColor.YELLOW).append(component).colorIfAbsent(NamedTextColor.GRAY)); + } + + public static void error(String message, Object... strings) + { + for (int i = 0; i < strings.length; i++) + { + if (message.contains("{" + i + "}")) + { + message = message.replace("{" + i + "}", strings[i].toString()); + } + } + Plex.get().getServer().getConsoleCommandSource().sendMessage(MiniMessage.miniMessage().deserialize("[Plex Error] " + message)); + } + + public static void warn(String message, Object... strings) + { + for (int i = 0; i < strings.length; i++) + { + if (message.contains("{" + i + "}")) + { + message = message.replace("{" + i + "}", strings[i].toString()); + } + } + Plex.get().getServer().getConsoleCommandSource().sendMessage(MiniMessage.miniMessage().deserialize("<#eb7c0e>[Plex Warning] " + message)); + } + + public static void debug(String message, Object... strings) + { + for (int i = 0; i < strings.length; i++) + { + if (message.contains("{" + i + "}")) + { + message = message.replace("{" + i + "}", strings[i].toString()); + } + } + if (Plex.get().getConfig().as(ServerSettings.class).getServer().isDebug()) + { + Plex.get().getServer().getConsoleCommandSource().sendMessage(MiniMessage.miniMessage().deserialize("[Plex Debug] " + message)); + } + } +} diff --git a/proxy/src/main/java/dev/plex/util/RandomUtil.java b/proxy/src/main/java/dev/plex/util/RandomUtil.java new file mode 100644 index 0000000..6770bf1 --- /dev/null +++ b/proxy/src/main/java/dev/plex/util/RandomUtil.java @@ -0,0 +1,34 @@ +package dev.plex.util; + +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.concurrent.ThreadLocalRandom; + +public class RandomUtil +{ + public static NamedTextColor getRandomColor() + { + NamedTextColor[] colors = NamedTextColor.NAMES.values().stream().filter(namedTextColor -> namedTextColor != NamedTextColor.BLACK && namedTextColor != NamedTextColor.DARK_BLUE).toArray(NamedTextColor[]::new); + return colors[randomNum(colors.length)]; + } + + public static boolean randomBoolean() + { + return ThreadLocalRandom.current().nextBoolean(); + } + + public static int randomNum() + { + return ThreadLocalRandom.current().nextInt(); + } + + public static int randomNum(int limit) + { + return ThreadLocalRandom.current().nextInt(limit); + } + + public static int randomNum(int start, int limit) + { + return ThreadLocalRandom.current().nextInt(start, limit); + } +} diff --git a/proxy/src/main/java/dev/plex/util/ReflectionsUtil.java b/proxy/src/main/java/dev/plex/util/ReflectionsUtil.java new file mode 100644 index 0000000..4d7dc4b --- /dev/null +++ b/proxy/src/main/java/dev/plex/util/ReflectionsUtil.java @@ -0,0 +1,58 @@ +package dev.plex.util; + +import com.google.common.collect.ImmutableSet; +import com.google.common.reflect.ClassPath; +import dev.plex.Plex; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class ReflectionsUtil +{ + @SuppressWarnings("UnstableApiUsage") + public static Set> getClassesFrom(String packageName) + { + Set> classes = new HashSet<>(); + try + { + ClassPath path = ClassPath.from(Plex.class.getClassLoader()); + ImmutableSet infoSet = path.getTopLevelClasses(packageName); + infoSet.forEach(info -> + { + try + { + Class clazz = Class.forName(info.getName()); + classes.add(clazz); + } + catch (ClassNotFoundException ex) + { + PlexLog.error("Unable to find class " + info.getName() + " in " + packageName); + } + }); + } + catch (IOException ex) + { + PlexLog.error("Something went wrong while fetching classes from " + packageName); + throw new RuntimeException(ex); + } + return Collections.unmodifiableSet(classes); + } + + @SuppressWarnings("unchecked") + public static Set> getClassesBySubType(String packageName, Class subType) + { + Set> loadedClasses = getClassesFrom(packageName); + Set> classes = new HashSet<>(); + loadedClasses.forEach(clazz -> + { + if (clazz.getSuperclass() == subType || Arrays.asList(clazz.getInterfaces()).contains(subType)) + { + classes.add((Class)clazz); + } + }); + return Collections.unmodifiableSet(classes); + } +} diff --git a/settings.gradle b/settings.gradle index 4743ccc..fc4a023 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,4 +7,5 @@ pluginManagement { rootProject.name = "Plex" include 'api' include 'server' +include 'proxy'