Compare commits
198 Commits
33b40f0cba
...
medrias
Author | SHA1 | Date | |
---|---|---|---|
27c444f3b4 | |||
c589da2a14 | |||
3fe4a1b244 | |||
1925dd9b36 | |||
d637b92f6c | |||
af4bab0d12 | |||
44dc690736 | |||
c9af5ad308 | |||
27403a6e20 | |||
38a42dcca0 | |||
5782046b7a | |||
2b407d7f27 | |||
8f5f880754 | |||
3d92c3afb6 | |||
5e1f98ab70 | |||
276f5b2dc1 | |||
9c72b8cda4 | |||
ee023b5d2c | |||
974347cbde | |||
e6b77bcad6 | |||
36eb1227cf | |||
4be37945cb | |||
3e6cf96040 | |||
d1a04a7a66 | |||
fcac9af7d1 | |||
e16487431d | |||
50f5ab671a | |||
5a04052f8e | |||
c86855ac16 | |||
001239fe57 | |||
0c7fb9b370 | |||
f416e30d45 | |||
e271ac2964 | |||
2bb09ad42b | |||
4f55890092 | |||
76470b963e | |||
76fc673e98 | |||
307b5132df | |||
ac52e024f3 | |||
bb6d221ced | |||
2acfa53b63 | |||
640b255e1d | |||
ed0db5391d | |||
cef4af80f0 | |||
7f56645ba5 | |||
827c13887c | |||
0ff2ae1296 | |||
e7b528718c | |||
d411618e63 | |||
decf302851 | |||
d3097781bc | |||
2942a030a6 | |||
69af006001 | |||
c60fb613d4 | |||
33e4e053cf | |||
e02ccc2b60 | |||
1c22518dd9 | |||
5d294ea172 | |||
56632867ec | |||
0c074b9354 | |||
bdf60785e8 | |||
ca7a51af2c | |||
d7705d8904 | |||
649e1a56c8 | |||
7d89f0c376 | |||
f494c3bdb3 | |||
23a7b940b7 | |||
bd0e0484cd | |||
92a9afa22c | |||
2393352770 | |||
7795d94dfb | |||
3c6d77f0bb | |||
e4eb6dc9c9 | |||
2d6d905b54 | |||
5a3831ba74 | |||
07f3841ee6 | |||
d84e4c87dc | |||
49a32421c0 | |||
177733950d | |||
8149d8fb54 | |||
eb72633dd8 | |||
ece1bc70bf | |||
90009b8703 | |||
a39f3d8143 | |||
ecc9932f5e | |||
e9b401f43d | |||
77b0a0c73c | |||
93960b83c2 | |||
075468854d | |||
b82b59d57b | |||
b85c5acb21 | |||
ba496b0968 | |||
ecd8b3b0c9 | |||
2f0b59a032 | |||
8f31ea54d1 | |||
e2506d5d53 | |||
0e016881d7 | |||
c7b33132a9 | |||
a24eab67b6 | |||
db06ab1be9 | |||
728961d19f | |||
8b6fe63df1 | |||
da1ee9d882 | |||
455226b762 | |||
3ee806c1ea | |||
69a4f2fe6f | |||
62949948e1 | |||
bd3bea8381 | |||
463a4d7e78 | |||
84298b08b3 | |||
9ac7a98257 | |||
f16389d33d | |||
a49061eb9f | |||
378e79b8ad | |||
ae634ab560 | |||
45ab550d06 | |||
0fcd02c80d | |||
2d950117d3 | |||
2f476ce8f2 | |||
75e292b1b8 | |||
2969d51f72 | |||
c0e0097b7b | |||
d047be35d9 | |||
5fb17be4c7 | |||
d7bb56e0b2 | |||
9e7d89cf70 | |||
79e4bb90f7 | |||
736e0f0c23 | |||
7481b12111 | |||
7c4fd78680 | |||
8c25fb0dd1 | |||
d5a2aa1c30 | |||
7d5060d09b | |||
a46e066669 | |||
a4b33a1af7 | |||
5edd8cdfec | |||
c984b63cee | |||
69b72ef90d | |||
555f5ab65c | |||
9f9fb55726 | |||
98d1a21aab | |||
d59ae22970 | |||
e6fc31e5ca | |||
70c4d59fdc | |||
edd5b06a46 | |||
436c9b9381 | |||
f3f616cdca | |||
20643fec62 | |||
c79d9b8006 | |||
61fb7b3142 | |||
f0a9fca952 | |||
913d5d91dd | |||
1cd3749d7d | |||
e640cbb1a3 | |||
edd4e89a6e | |||
9b83f9699c | |||
3e0297c8af | |||
df8dbe1e61 | |||
d023bcb706 | |||
448ee6c62a | |||
efcb155b3d | |||
d5c9876734 | |||
f036c22a56 | |||
15982cb837 | |||
0453a72587 | |||
ff954a3903 | |||
ba896e689a | |||
4259e5eccd | |||
5b43aed0b4 | |||
cafb220768 | |||
544abd218c | |||
55556b0714 | |||
87b9ffcc37 | |||
95fa33a488 | |||
46653f06ff | |||
2bc60df11c | |||
e4a5bf0eac | |||
5b40c4aabb | |||
ff5d776aa5 | |||
872746b46f | |||
ced9b0eaca | |||
4ec47b5e4b | |||
2fb4775ca7 | |||
fdfb67757f | |||
d4ff95534f | |||
fd828d600e | |||
b6dba62fa4 | |||
b2f5770461 | |||
f1ef4e1927 | |||
df46026457 | |||
a6bde9e191 | |||
6f310de32e | |||
add5d3bcd7 | |||
73d96d0bb7 | |||
bf59617e19 | |||
fb4c62a0bc | |||
dd2b4467ed | |||
6577367c27 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
/.idea
|
||||
/*/target
|
||||
dependency-reduced-pom.xml
|
||||
dependency-reduced-pom.xml
|
||||
|
||||
*.iml
|
@@ -3,7 +3,7 @@
|
||||
### Development library for Minecraft server applications and plugins
|
||||
|
||||
This repository contains a collection of maven modules that are used for the development of our Minecraft server. Those
|
||||
modules are made open source so they can be used by other developpers. Each of them provides different functionalities
|
||||
modules are made open source, so they can be used by other developers. Each of them provides different functionalities
|
||||
that are detailed in their respective Readme file (if any).
|
||||
|
||||
- `pandalib-util` General purpose utility and helper classes;
|
||||
@@ -18,10 +18,10 @@ that are detailed in their respective Readme file (if any).
|
||||
- `pandalib-players` A library to handle classes representing online or offline players;
|
||||
- `pandalib-players-permissible` An extension of `pandalib-players` with support for the permission system `pandalib-permissions`;
|
||||
- `pandalib-netapi` A poorly designed, but working TCP network library;
|
||||
- `pandalib-net` A better-designed, packet-based TCP network library (_still in development_);
|
||||
- `pandalib-config` Utility and helper classes to handle configuration related files and folders;
|
||||
- `pandalib-commands` An abstract command manager working on top of [Brigadier](https://github.com/Mojang/brigadier);
|
||||
- `pandalib-cli` Utility and helper classes for a standalone CLI Java application.
|
||||
- `pandalib-core` A catch-all module for some helper classes that didn’t have their own module yet;
|
||||
- `pandalib-cli` Utility and helper classes for a standalone CLI Java application;
|
||||
- `pandalib-core` A catch-all module for some helper classes that didn't have their own module yet;
|
||||
|
||||
### Use in your projects
|
||||
|
||||
|
1
pandalib-bungee-chat/.gitignore
vendored
Normal file
1
pandalib-bungee-chat/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target/
|
49
pandalib-bungee-chat/pom.xml
Normal file
49
pandalib-bungee-chat/pom.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>pandalib-parent</artifactId>
|
||||
<groupId>fr.pandacube.lib</groupId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>pandalib-bungee-chat</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>bungeecord-repo</id>
|
||||
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>fr.pandacube.lib</groupId>
|
||||
<artifactId>pandalib-util</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>fr.pandacube.lib</groupId>
|
||||
<artifactId>pandalib-chat</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>net.md-5</groupId>
|
||||
<artifactId>bungeecord-chat</artifactId>
|
||||
<version>${bungeecord.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>net.kyori</groupId>
|
||||
<artifactId>adventure-platform-bungeecord</artifactId>
|
||||
<version>4.3.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@@ -0,0 +1,77 @@
|
||||
package fr.pandacube.lib.bungee.chat;
|
||||
|
||||
import fr.pandacube.lib.chat.Chat;
|
||||
import fr.pandacube.lib.chat.Chat.FormatableChat;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.ComponentLike;
|
||||
import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer;
|
||||
import net.md_5.bungee.api.chat.BaseComponent;
|
||||
|
||||
/**
|
||||
* Utility class to ease conversion between our Adventure backed Chat API and BungeeCord chat API.
|
||||
*/
|
||||
public class ChatBungee {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Creates a {@link FormatableChat} from the provided Bungee {@link BaseComponent}.
|
||||
* @param c the {@link BaseComponent}.
|
||||
* @return a new {@link FormatableChat}.
|
||||
*/
|
||||
public static FormatableChat chatComponent(BaseComponent c) {
|
||||
return Chat.chatComponent(toAdventure(c));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Creates a {@link FormatableChat} from the provided Bungee {@link BaseComponent BaseComponent[]}.
|
||||
* @param c the array of {@link BaseComponent}.
|
||||
* @return a new {@link FormatableChat}.
|
||||
*/
|
||||
public static FormatableChat chatComponent(BaseComponent[] c) {
|
||||
return Chat.chatComponent(toAdventure(c));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Converts the Bungee {@link BaseComponent} array into Adventure {@link Component}.
|
||||
* @param components the Bungee {@link BaseComponent} array.
|
||||
* @return a {@link Component}.
|
||||
*/
|
||||
public static Component toAdventure(BaseComponent[] components) {
|
||||
return BungeeComponentSerializer.get().deserialize(components);
|
||||
}
|
||||
/**
|
||||
* Converts the Bungee {@link BaseComponent} into Adventure {@link Component}.
|
||||
* @param component the Bungee {@link BaseComponent}.
|
||||
* @return a {@link Component}.
|
||||
*/
|
||||
public static Component toAdventure(BaseComponent component) {
|
||||
return toAdventure(new BaseComponent[] { component });
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the Adventure {@link Component} into Bungee {@link BaseComponent} array.
|
||||
* @param component the Adventure {@link Component}.
|
||||
* @return a {@link BaseComponent} array.
|
||||
*/
|
||||
public static BaseComponent[] toBungeeArray(ComponentLike component) {
|
||||
return BungeeComponentSerializer.get().serialize(component.asComponent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the Adventure {@link Component} into Bungee {@link BaseComponent}.
|
||||
* @param component the Adventure {@link Component}.
|
||||
* @return a {@link BaseComponent}.
|
||||
*/
|
||||
public static BaseComponent toBungee(ComponentLike component) {
|
||||
BaseComponent[] arr = toBungeeArray(component);
|
||||
return arr.length == 1 ? arr[0] : new net.md_5.bungee.api.chat.TextComponent(arr);
|
||||
}
|
||||
|
||||
|
||||
private ChatBungee() {}
|
||||
}
|
@@ -25,7 +25,6 @@ import java.util.function.Function;
|
||||
*/
|
||||
public class PandalibBungeePermissions implements Listener {
|
||||
|
||||
|
||||
/**
|
||||
* Registers event listener to redirect permission checks to {@code pandalib-permissions}.
|
||||
* @param bungeePlugin a BungeeCord plugin.
|
||||
@@ -35,6 +34,8 @@ public class PandalibBungeePermissions implements Listener {
|
||||
}
|
||||
|
||||
|
||||
private PandalibBungeePermissions() {}
|
||||
|
||||
/**
|
||||
* Event handler called when a plugin asks if a player has a permission.
|
||||
* @param event the permission check event.
|
||||
|
@@ -33,7 +33,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>fr.pandacube.lib</groupId>
|
||||
<artifactId>pandalib-chat</artifactId>
|
||||
<artifactId>pandalib-bungee-chat</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
@@ -1,29 +1,49 @@
|
||||
package fr.pandacube.lib.bungee;
|
||||
|
||||
import fr.pandacube.lib.bungee.util.DailyLogRotateFileHandler;
|
||||
import fr.pandacube.lib.bungee.util.BungeeDailyLogRotateFileHandler;
|
||||
import fr.pandacube.lib.bungee.util.PluginMessagePassthrough;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
|
||||
/**
|
||||
* General class used to initialize some tools of pandalib-bungee, following the bungee plugin's lifecycle.
|
||||
*/
|
||||
public class PandaLibBungee {
|
||||
|
||||
private static Plugin plugin;
|
||||
|
||||
/**
|
||||
* Method to be called in {@link Plugin#onLoad()} method.
|
||||
* @param plugin the plugin instance.
|
||||
*/
|
||||
public static void onLoad(Plugin plugin) {
|
||||
PandaLibBungee.plugin = plugin;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to be called in {@link Plugin#onEnable()} method.
|
||||
*/
|
||||
public static void onEnable() {
|
||||
PluginMessagePassthrough.init(plugin);
|
||||
DailyLogRotateFileHandler.init(true);
|
||||
BungeeDailyLogRotateFileHandler.init(true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Method to be called in {@link Plugin#onDisable()} method.
|
||||
*/
|
||||
public static void disable() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the plugin instance.
|
||||
* @return the plugin instance.
|
||||
*/
|
||||
public static Plugin getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
private PandaLibBungee() {}
|
||||
|
||||
}
|
||||
|
@@ -6,11 +6,40 @@ import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Class that holds the configuration varables for {@link BungeeBackupManager}.
|
||||
*/
|
||||
@SuppressWarnings("CanBeFinal")
|
||||
public class BungeeBackupConfig {
|
||||
/**
|
||||
* Tells if the working directory of the current bungee instance should be backed up.
|
||||
*/
|
||||
public boolean workdirBackupEnabled = true;
|
||||
/**
|
||||
* Tells if the old logs of the current bungee instance should be backed up.
|
||||
*/
|
||||
public boolean logsBackupEnabled = true;
|
||||
public String scheduling = "0 2 * * *"; // cron format, here is everyday at 2am
|
||||
/**
|
||||
* The cron scheduling of when the workdir backup occurs.
|
||||
*/
|
||||
public String scheduling = "0 2 * * *"; // cron format, here is every day at 2am
|
||||
/**
|
||||
* The destination directory for the backups.
|
||||
*/
|
||||
public File backupDirectory = null;
|
||||
/**
|
||||
* The configuration handling the cleaning of the backup directory.
|
||||
*/
|
||||
public BackupCleaner workdirBackupCleaner = BackupCleaner.KEEPING_1_EVERY_N_MONTH(3).merge(BackupCleaner.KEEPING_N_LAST(5));
|
||||
/**
|
||||
* A list of ignored files or directory in the workdir to exclude from the backup.
|
||||
*/
|
||||
public List<String> workdirIgnoreList = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Creates a new {@link BungeeBackupConfig}.
|
||||
*/
|
||||
public BungeeBackupConfig() {
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -6,10 +6,17 @@ import fr.pandacube.lib.core.backup.RotatedLogsBackupProcess;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Handles the backup processes for a Bungeecord instance.
|
||||
*/
|
||||
public class BungeeBackupManager extends BackupManager {
|
||||
|
||||
BungeeBackupConfig config;
|
||||
|
||||
|
||||
/**
|
||||
* Instanciate a new {@link BungeeBackupManager}.
|
||||
* @param config the configuration.
|
||||
*/
|
||||
public BungeeBackupManager(BungeeBackupConfig config) {
|
||||
super(config.backupDirectory);
|
||||
setConfig(config);
|
||||
@@ -24,12 +31,19 @@ public class BungeeBackupManager extends BackupManager {
|
||||
super.addProcess(process);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new configuration for this backup manager.
|
||||
* @param config the new configuration.
|
||||
*/
|
||||
public void setConfig(BungeeBackupConfig config) {
|
||||
this.config = config;
|
||||
backupQueue.forEach(this::updateProcessConfig);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deploys the new configuration to the provided backup process.
|
||||
* @param process the process on which to apply the new config.
|
||||
*/
|
||||
public void updateProcessConfig(BackupProcess process) {
|
||||
if (process instanceof BungeeWorkdirProcess) {
|
||||
process.setEnabled(config.workdirBackupEnabled);
|
||||
|
@@ -1,16 +1,19 @@
|
||||
package fr.pandacube.lib.bungee.backup;
|
||||
|
||||
import fr.pandacube.lib.core.backup.BackupProcess;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.function.BiPredicate;
|
||||
|
||||
/**
|
||||
* The backup process responsible for the working directory of the current Bungeecord instance.
|
||||
*/
|
||||
public class BungeeWorkdirProcess extends BackupProcess {
|
||||
|
||||
|
||||
/**
|
||||
* Instantiates this backup process.
|
||||
* @param bm the backup manager.
|
||||
*/
|
||||
protected BungeeWorkdirProcess(BungeeBackupManager bm) {
|
||||
super(bm, "workdir");
|
||||
}
|
||||
@@ -22,15 +25,12 @@ public class BungeeWorkdirProcess extends BackupProcess {
|
||||
|
||||
|
||||
public BiPredicate<File, String> getFilenameFilter() {
|
||||
return new BiPredicate<File, String>() {
|
||||
@Override
|
||||
public boolean test(File file, String path) {
|
||||
if (new File(getSourceDir(), "logs").equals(file))
|
||||
return false;
|
||||
if (file.isFile() && file.getName().endsWith(".lck"))
|
||||
return false;
|
||||
return BungeeWorkdirProcess.super.getFilenameFilter().test(file, path);
|
||||
}
|
||||
return (file, path) -> {
|
||||
if (new File(getSourceDir(), "logs").equals(file))
|
||||
return false;
|
||||
if (file.isFile() && file.getName().endsWith(".lck"))
|
||||
return false;
|
||||
return BungeeWorkdirProcess.super.getFilenameFilter().test(file, path);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,10 +60,4 @@ public class BungeeWorkdirProcess extends BackupProcess {
|
||||
return "workdir";
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void displayNextSchedule() {
|
||||
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " next backup on "
|
||||
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext())));
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||
import fr.pandacube.lib.commands.BrigadierCommand;
|
||||
import fr.pandacube.lib.commands.SuggestionsSupplier;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
@@ -28,10 +28,10 @@ public abstract class BungeeBrigadierCommand extends BrigadierCommand<CommandSen
|
||||
/**
|
||||
* The command dispatcher.
|
||||
*/
|
||||
protected BungeeBrigadierDispatcher dispatcher = BungeeBrigadierDispatcher.getInstance();
|
||||
protected final BungeeBrigadierDispatcher dispatcher = BungeeBrigadierDispatcher.getInstance();
|
||||
|
||||
/**
|
||||
* Instanciate this command instance.
|
||||
* Instantiate this command instance.
|
||||
*/
|
||||
public BungeeBrigadierCommand() {
|
||||
LiteralCommandNode<CommandSender> commandNode;
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package fr.pandacube.lib.bungee.commands;
|
||||
|
||||
import fr.pandacube.lib.bungee.PandaLibBungee;
|
||||
import fr.pandacube.lib.chat.Chat;
|
||||
import fr.pandacube.lib.bungee.chat.ChatBungee;
|
||||
import fr.pandacube.lib.commands.BrigadierDispatcher;
|
||||
import net.kyori.adventure.text.ComponentLike;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
@@ -21,6 +20,10 @@ public class BungeeBrigadierDispatcher extends BrigadierDispatcher<CommandSender
|
||||
|
||||
private static BungeeBrigadierDispatcher instance = null;
|
||||
|
||||
/**
|
||||
* Gets the instance of {@link BungeeBrigadierDispatcher}.
|
||||
* @return the instance of {@link BungeeBrigadierDispatcher}.
|
||||
*/
|
||||
public static synchronized BungeeBrigadierDispatcher getInstance() {
|
||||
return instance;
|
||||
}
|
||||
@@ -36,7 +39,7 @@ public class BungeeBrigadierDispatcher extends BrigadierDispatcher<CommandSender
|
||||
*/
|
||||
public BungeeBrigadierDispatcher(Plugin pl) {
|
||||
if (instance != null)
|
||||
throw new IllegalStateException("Cannot instanciante more than one BungeeBrigadierDispatcher");
|
||||
throw new IllegalStateException("Cannot instantiate more than one BungeeBrigadierDispatcher");
|
||||
instance = this;
|
||||
plugin = pl;
|
||||
ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
|
||||
@@ -44,7 +47,7 @@ public class BungeeBrigadierDispatcher extends BrigadierDispatcher<CommandSender
|
||||
|
||||
|
||||
/**
|
||||
* Called when a player sends a chat message. Used to gets the typed command and execute it.
|
||||
* Called when a player sends a chat message. Used to get the typed command and execute it.
|
||||
* @param event the event.
|
||||
*/
|
||||
@EventHandler
|
||||
@@ -68,6 +71,6 @@ public class BungeeBrigadierDispatcher extends BrigadierDispatcher<CommandSender
|
||||
|
||||
@Override
|
||||
protected void sendSenderMessage(CommandSender sender, ComponentLike message) {
|
||||
sender.sendMessage(Chat.toBungee(message.asComponent()));
|
||||
sender.sendMessage(ChatBungee.toBungee(message.asComponent()));
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,12 @@
|
||||
package fr.pandacube.lib.bungee.players;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
import fr.pandacube.lib.bungee.chat.ChatBungee;
|
||||
import fr.pandacube.lib.core.mc_version.ProtocolVersion;
|
||||
import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer;
|
||||
import fr.pandacube.lib.reflect.Reflect;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufAllocator;
|
||||
import net.kyori.adventure.identity.Identified;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.ComponentLike;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.SkinConfiguration;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
@@ -18,10 +17,7 @@ import net.md_5.bungee.protocol.DefinedPacket;
|
||||
import net.md_5.bungee.protocol.packet.ClientSettings;
|
||||
import net.md_5.bungee.protocol.packet.PluginMessage;
|
||||
|
||||
import fr.pandacube.lib.chat.Chat;
|
||||
import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer;
|
||||
import fr.pandacube.lib.reflect.Reflect;
|
||||
import fr.pandacube.lib.util.MinecraftVersion;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Represents any online player on a Bungeecord proxy.
|
||||
@@ -53,11 +49,11 @@ public interface BungeeOnlinePlayer extends BungeeOffPlayer, AbstractOnlinePlaye
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minecraft version of this player’s client.
|
||||
* @return the minecraft version of this player’s client.
|
||||
* Gets the protocol version of this player’s client.
|
||||
* @return the protocol version of this player’s client.
|
||||
*/
|
||||
default MinecraftVersion getMinecraftVersion() {
|
||||
return MinecraftVersion.getVersion(getBungeeProxiedPlayer().getPendingConnection().getVersion());
|
||||
default ProtocolVersion getProtocolVersion() {
|
||||
return ProtocolVersion.ofProtocol(getBungeeProxiedPlayer().getPendingConnection().getVersion());
|
||||
}
|
||||
|
||||
|
||||
@@ -88,33 +84,13 @@ public interface BungeeOnlinePlayer extends BungeeOffPlayer, AbstractOnlinePlaye
|
||||
|
||||
@Override
|
||||
default void sendMessage(Component message) {
|
||||
getBungeeProxiedPlayer().sendMessage(Chat.toBungee(message));
|
||||
}
|
||||
|
||||
@Override
|
||||
default void sendMessage(ComponentLike message, UUID sender) {
|
||||
getBungeeProxiedPlayer().sendMessage(sender, Chat.toBungee(message.asComponent()));
|
||||
}
|
||||
|
||||
@Override
|
||||
default void sendMessage(Component message, Identified sender) {
|
||||
getBungeeProxiedPlayer().sendMessage(sender == null ? null : sender.identity().uuid(), Chat.toBungee(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the provided message in the player’s chat, if they allows to display CHAT messages.
|
||||
* @param message the message to send.
|
||||
* @param sender the player causing the send of this message. Client side filtering may occur. May be null if we
|
||||
* don’t want client filtering, but still consider the message as CHAT message.
|
||||
*/
|
||||
default void sendMessage(ComponentLike message, ProxiedPlayer sender) {
|
||||
getBungeeProxiedPlayer().sendMessage(sender == null ? null : sender.getUniqueId(), Chat.toBungee(message.asComponent()));
|
||||
getBungeeProxiedPlayer().sendMessage(ChatBungee.toBungee(message));
|
||||
}
|
||||
|
||||
@Override
|
||||
default void sendTitle(Component title, Component subtitle, int fadeIn, int stay, int fadeOut) {
|
||||
ProxyServer.getInstance().createTitle()
|
||||
.title(Chat.toBungee(title)).subTitle(Chat.toBungee(subtitle))
|
||||
.title(ChatBungee.toBungee(title)).subTitle(ChatBungee.toBungee(subtitle))
|
||||
.fadeIn(fadeIn).stay(stay).fadeOut(fadeOut)
|
||||
.send(getBungeeProxiedPlayer());
|
||||
}
|
||||
|
@@ -0,0 +1,45 @@
|
||||
package fr.pandacube.lib.bungee.util;
|
||||
|
||||
import fr.pandacube.lib.util.log.DailyLogRotateFileHandler;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.log.ConciseFormatter;
|
||||
|
||||
import java.util.logging.Filter;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.LogRecord;
|
||||
|
||||
/**
|
||||
* A log rotate that extends the functionalities of {@link DailyLogRotateFileHandler}
|
||||
* to adapt with bungee specificities.
|
||||
*/
|
||||
public class BungeeDailyLogRotateFileHandler extends DailyLogRotateFileHandler {
|
||||
|
||||
/**
|
||||
* Initialize this file handler.
|
||||
* @param hideInitialHandlerLogEntries true if we want to hide some InitialHandler log entries
|
||||
*/
|
||||
public static void init(boolean hideInitialHandlerLogEntries) {
|
||||
ProxyServer.getInstance().getLogger().addHandler(new BungeeDailyLogRotateFileHandler(hideInitialHandlerLogEntries));
|
||||
}
|
||||
|
||||
private BungeeDailyLogRotateFileHandler(boolean hideInitialHandlerLogEntries) {
|
||||
if (hideInitialHandlerLogEntries)
|
||||
setFilter(new InitialHandlerLogRemover());
|
||||
setFormatter(new ConciseFormatter(false));
|
||||
setLevel(Level.parse(System.getProperty("net.md_5.bungee.file-log-level", "INFO")));
|
||||
}
|
||||
|
||||
|
||||
private class InitialHandlerLogRemover implements Filter {
|
||||
|
||||
@Override
|
||||
public boolean isLoggable(LogRecord record) {
|
||||
String formattedRecord = getFormatter().format(record);
|
||||
return !(
|
||||
formattedRecord.contains("<-> InitialHandler has connected")
|
||||
|| formattedRecord.contains("<-> InitialHandler has pinged")
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -1,147 +0,0 @@
|
||||
package fr.pandacube.lib.bungee.util;
|
||||
|
||||
import com.google.common.io.Files;
|
||||
import fr.pandacube.lib.bungee.PandaLibBungee;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.log.ConciseFormatter;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.logging.ErrorManager;
|
||||
import java.util.logging.Filter;
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.LogRecord;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
public class DailyLogRotateFileHandler extends Handler {
|
||||
|
||||
private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
|
||||
|
||||
|
||||
public static void init(boolean hideInitialHandlerLogEntries) {
|
||||
ProxyServer.getInstance().getLogger().addHandler(new DailyLogRotateFileHandler(hideInitialHandlerLogEntries));
|
||||
}
|
||||
|
||||
|
||||
private BufferedWriter currentFile = null;
|
||||
private String currentFileDate = getCurrentDay();
|
||||
private boolean closed = false;
|
||||
|
||||
private DailyLogRotateFileHandler(boolean hideInitialHandlerLogEntries) {
|
||||
if (hideInitialHandlerLogEntries)
|
||||
setFilter(new InitialHandlerLogRemover());
|
||||
setFormatter(new ConciseFormatter(false));
|
||||
setLevel(Level.parse(System.getProperty("net.md_5.bungee.file-log-level", "INFO")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() throws SecurityException {
|
||||
closed = true;
|
||||
if (currentFile != null) try {
|
||||
currentFile.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void flush() {
|
||||
if (closed) return;
|
||||
if (currentFile == null) return;
|
||||
try {
|
||||
currentFile.flush();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void publish(LogRecord record) {
|
||||
if (closed || !isLoggable(record))
|
||||
return;
|
||||
|
||||
if (currentFile == null || !currentFileDate.equals(getCurrentDay()))
|
||||
changeFile();
|
||||
|
||||
if (currentFile == null)
|
||||
return;
|
||||
|
||||
String formattedMessage;
|
||||
|
||||
try {
|
||||
formattedMessage = getFormatter().format(record);
|
||||
} catch (Exception ex) {
|
||||
reportError(null, ex, ErrorManager.FORMAT_FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
currentFile.write(formattedMessage);
|
||||
currentFile.flush();
|
||||
} catch (Exception ex) {
|
||||
reportError(null, ex, ErrorManager.WRITE_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
private void changeFile() {
|
||||
if (currentFile != null) {
|
||||
try {
|
||||
currentFile.flush();
|
||||
currentFile.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
File fileNewName = new File("logs/" + currentFileDate + ".log");
|
||||
new File("logs/latest.log").renameTo(fileNewName);
|
||||
ProxyServer.getInstance().getScheduler().runAsync(PandaLibBungee.getPlugin(), () -> compress(fileNewName));
|
||||
}
|
||||
|
||||
currentFileDate = getCurrentDay();
|
||||
try {
|
||||
File logDir = new File("logs");
|
||||
logDir.mkdir();
|
||||
currentFile = new BufferedWriter(new FileWriter("logs/latest.log", true));
|
||||
} catch (SecurityException | IOException e) {
|
||||
reportError("Erreur lors de l'initialisation d'un fichier log", e, ErrorManager.OPEN_FAILURE);
|
||||
currentFile = null;
|
||||
}
|
||||
}
|
||||
|
||||
private String getCurrentDay() {
|
||||
return dateFormat.format(new Date());
|
||||
}
|
||||
|
||||
|
||||
private void compress(File sourceFile) {
|
||||
File destFile = new File(sourceFile.getParentFile(), sourceFile.getName() + ".gz");
|
||||
if (destFile.exists())
|
||||
destFile.delete();
|
||||
try (GZIPOutputStream os = new GZIPOutputStream(new FileOutputStream(destFile))) {
|
||||
Files.copy(sourceFile, os);
|
||||
} catch (IOException e) {
|
||||
if (destFile.exists())
|
||||
destFile.delete();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
sourceFile.delete();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private class InitialHandlerLogRemover implements Filter {
|
||||
|
||||
@Override
|
||||
public boolean isLoggable(LogRecord record) {
|
||||
String formattedRecord = getFormatter().format(record);
|
||||
if (formattedRecord.contains("<-> InitialHandler has connected")) return false;
|
||||
if (formattedRecord.contains("<-> InitialHandler has pinged")) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -26,26 +26,35 @@
|
||||
<dependency>
|
||||
<groupId>net.kyori</groupId>
|
||||
<artifactId>adventure-api</artifactId>
|
||||
<version>4.11.0</version>
|
||||
<version>4.15.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.kyori</groupId>
|
||||
<artifactId>adventure-platform-bungeecord</artifactId>
|
||||
<version>4.1.1</version>
|
||||
<artifactId>adventure-text-serializer-gson</artifactId>
|
||||
<version>4.13.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.kyori</groupId>
|
||||
<artifactId>adventure-text-serializer-legacy</artifactId>
|
||||
<version>4.13.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.kyori</groupId>
|
||||
<artifactId>adventure-text-serializer-plain</artifactId>
|
||||
<version>4.11.0</version>
|
||||
<version>4.15.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.kyori</groupId>
|
||||
<artifactId>adventure-text-minimessage</artifactId>
|
||||
<version>4.15.0</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>net.md-5</groupId>
|
||||
<artifactId>bungeecord-chat</artifactId>
|
||||
<version>${bungeecord.version}</version>
|
||||
<scope>compile</scope>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.10.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
@@ -1,15 +1,12 @@
|
||||
package fr.pandacube.lib.chat;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.ComponentBuilder;
|
||||
import net.kyori.adventure.text.ComponentLike;
|
||||
import net.kyori.adventure.text.TextComponent;
|
||||
import net.kyori.adventure.text.TranslationArgument;
|
||||
import net.kyori.adventure.text.TranslationArgumentLike;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
import net.kyori.adventure.text.event.HoverEventSource;
|
||||
@@ -18,11 +15,17 @@ import net.kyori.adventure.text.format.Style;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import net.kyori.adventure.text.format.TextDecoration.State;
|
||||
import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.chat.BaseComponent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
/**
|
||||
* A builder for chat components.
|
||||
@@ -30,10 +33,10 @@ import net.md_5.bungee.api.chat.BaseComponent;
|
||||
* Use one of the provided static methods to create a new instance.
|
||||
* <p>
|
||||
* This class implements {@link ComponentLike} and {@link HoverEventSource} so they can be used directly in
|
||||
* Adventure API and its implentation without using the final methods of this builder.
|
||||
* Adventure API and its implementation without using the final methods of this builder.
|
||||
* <p>
|
||||
* The unique possible concrete subclass of this class, {@link FormatableChat}, takes care of the formating of the
|
||||
* builded component. The rationale for this design is explained in the documentation of {@link FormatableChat}.
|
||||
* The unique possible concrete subclass of this class, {@link FormatableChat}, takes care of the formatting of the
|
||||
* built component. The rationale for this design is explained in the documentation of {@link FormatableChat}.
|
||||
*/
|
||||
public abstract sealed class Chat extends ChatStatic implements HoverEventSource<Component>, ComponentLike {
|
||||
|
||||
@@ -60,61 +63,70 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
|
||||
|
||||
/**
|
||||
* Builds the component into Adventure Component instance.
|
||||
* @return the {@link Component} builded from this {@link Chat} component.
|
||||
* @return the {@link Component} built from this {@link Chat} component.
|
||||
*/
|
||||
public Component getAdv() {
|
||||
public Component get() {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the component into BungeeCord {@link BaseComponent} instance.
|
||||
* @return the {@link BaseComponent} builded from this {@link Chat} component.
|
||||
*/
|
||||
public BaseComponent get() {
|
||||
return toBungee(getAdv());
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the component into BungeeCord {@link BaseComponent} array.
|
||||
* @return the {@link BaseComponent} array builded from this {@link Chat} component.
|
||||
*/
|
||||
public BaseComponent[] getAsArray() {
|
||||
return toBungeeArray(getAdv());
|
||||
}
|
||||
|
||||
private static final LegacyComponentSerializer LEGACY_SERIALIZER_BUNGEE_FIENDLY = LegacyComponentSerializer.builder()
|
||||
private static final LegacyComponentSerializer LEGACY_SERIALIZER_BUNGEE_FRIENDLY = LegacyComponentSerializer.builder()
|
||||
.hexColors()
|
||||
.useUnusualXRepeatedCharacterHexFormat()
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Converts the builded component into legacy text.
|
||||
* Converts the built component into legacy text.
|
||||
* @return the legacy text. RGB colors are in BungeeCord format.
|
||||
*/
|
||||
public String getLegacyText() {
|
||||
return LEGACY_SERIALIZER_BUNGEE_FIENDLY.serialize(getAdv());
|
||||
return LEGACY_SERIALIZER_BUNGEE_FRIENDLY.serialize(get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the builded component into plain text.
|
||||
* Converts the built component into plain text.
|
||||
* @return the plain text of this component.
|
||||
*/
|
||||
public String getPlainText() {
|
||||
return PlainTextComponentSerializer.plainText().serializeOr(getAdv(), "");
|
||||
return PlainTextComponentSerializer.plainText().serializeOr(get(), "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public HoverEvent<Component> asHoverEvent(UnaryOperator<Component> op) {
|
||||
return HoverEvent.showText(op.apply(getAdv()));
|
||||
public @NotNull HoverEvent<Component> asHoverEvent(@NotNull UnaryOperator<Component> op) {
|
||||
return HoverEvent.showText(op.apply(get()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the component into Adventure Component instance.
|
||||
* @return the {@link Component} builded from this {@link Chat} component.
|
||||
* @return the {@link Component} built from this {@link Chat} component.
|
||||
*/
|
||||
@Override
|
||||
public Component asComponent() {
|
||||
return getAdv();
|
||||
public @NotNull Component asComponent() {
|
||||
return get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the component into Adventure Component instance, also down sampling the RGB colors to named colors.
|
||||
* @return the {@link Component} built from this {@link Chat} component, with down-sampled colors.
|
||||
*/
|
||||
public Component getAsDownSampledColorsComponent() {
|
||||
String json = GsonComponentSerializer.colorDownsamplingGson().serialize(get());
|
||||
return GsonComponentSerializer.gson().deserialize(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Chat} consisting of this {@link Chat} instance, with the RGB colors down-sampled to named colors.
|
||||
* @return a new {@link Chat} instance, with down-sampled colors.
|
||||
*/
|
||||
public Chat getAsDownSampledColors() {
|
||||
return chatComponent(getAsDownSampledColorsComponent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a MiniMessage representation of this {@link Chat} component.
|
||||
* @return the MiniMessage representation if this {@link Chat} component.
|
||||
*/
|
||||
public String getMiniMessage() {
|
||||
return MiniMessage.miniMessage().serialize(get());
|
||||
}
|
||||
|
||||
|
||||
@@ -154,15 +166,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a BungeeCord {@link BaseComponent} to this component.
|
||||
* @param comp the component to append.
|
||||
* @return this.
|
||||
*/
|
||||
public Chat then(BaseComponent comp) {
|
||||
return then(toAdventure(comp));
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a component to this component.
|
||||
* @param comp the component to append.
|
||||
@@ -177,15 +180,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
|
||||
return then(comp.asComponent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a BungeeCord {@link BaseComponent} array to this component.
|
||||
* @param comp the components to append.
|
||||
* @return this.
|
||||
*/
|
||||
public Chat then(BaseComponent[] comp) {
|
||||
return then(toAdventure(comp));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -260,7 +254,7 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
|
||||
* @param comp the component.
|
||||
* @return this.
|
||||
*/
|
||||
public Chat thenPlayerName(Component comp) { return then(playerNameComponent(comp)); }
|
||||
public Chat thenPlayerName(ComponentLike comp) { return then(playerNameComponent(comp)); }
|
||||
|
||||
/**
|
||||
* Appends a component consisting of a new line.
|
||||
@@ -269,12 +263,26 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
|
||||
public Chat thenNewLine() { return then(Component.newline()); }
|
||||
|
||||
/**
|
||||
* Appends a component with the provided legacy text as its content.
|
||||
* @param legacyText the legacy text.
|
||||
* Appends a component with the provided legacy text as its content, using the section {@code "§"} character.
|
||||
* @param legacyText the legacy text that uses the {@code "§"} character.
|
||||
* @return this.
|
||||
*/
|
||||
public Chat thenLegacyText(Object legacyText) { return then(legacyText(legacyText)); }
|
||||
|
||||
/**
|
||||
* Appends a component with the provided legacy text as its content, using the ampersand {@code "&"} character.
|
||||
* @param legacyText the legacy text that uses the {@code "&"} character.
|
||||
* @return this.
|
||||
*/
|
||||
public Chat thenLegacyAmpersandText(Object legacyText) { return then(legacyAmpersandText(legacyText)); }
|
||||
|
||||
/**
|
||||
* Appends a component with the provided MiniMessage text as its content.
|
||||
* @param miniMessageText the MiniMessage text.
|
||||
* @return this.
|
||||
*/
|
||||
public Chat thenMiniMessage(String miniMessageText) { return then(miniMessageText(miniMessageText)); }
|
||||
|
||||
/**
|
||||
* Appends a component with the provided translation key and parameters.
|
||||
* @param key the translation key.
|
||||
@@ -284,8 +292,8 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
|
||||
public Chat thenTranslation(String key, Object... with) { return then(translation(key, with)); }
|
||||
|
||||
/**
|
||||
* Appends a component with the provided keybind.
|
||||
* @param key the keybind to display.
|
||||
* Appends a component with the provided keybinding.
|
||||
* @param key the keybinding to display.
|
||||
* @return this.
|
||||
*/
|
||||
public Chat thenKeyBind(String key) { return then(keybind(key)); }
|
||||
@@ -443,50 +451,28 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
|
||||
|
||||
|
||||
/**
|
||||
* Appends a component filling a line of chat (or console) with the configured decoration character and
|
||||
* Appends a component filling a chat line with the configured decoration character and
|
||||
* color and a left-aligned text.
|
||||
* @param leftText the text aligned to the left.
|
||||
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
|
||||
* @return a new {@link FormatableChat} filling a chat line with the configured decoration character
|
||||
* and color and a left-aligned text.
|
||||
*/
|
||||
public Chat thenLeftText(ComponentLike leftText) { return then(leftText(leftText, console)); }
|
||||
|
||||
/**
|
||||
* Appends a component filling a line of chat (or console) with the configured decoration character and
|
||||
* color and a left-aligned text.
|
||||
* @param leftText the text aligned to the left.
|
||||
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
|
||||
* and color and a left-aligned text.
|
||||
* @deprecated uses Bungeecord chat API.
|
||||
*/
|
||||
@Deprecated
|
||||
public Chat thenLeftText(BaseComponent leftText) { return thenLeftText(chatComponent(leftText)); }
|
||||
|
||||
/**
|
||||
* Appends a component filling a line of chat (or console) with the configured decoration character and
|
||||
* Appends a component filling a chat line with the configured decoration character and
|
||||
* color and a right-aligned text.
|
||||
* @param rightText the text aligned to the right.
|
||||
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
|
||||
* @return a new {@link FormatableChat} filling a chat line with the configured decoration character
|
||||
* and color and a right-aligned text.
|
||||
*/
|
||||
public Chat thenRightText(ComponentLike rightText) { return then(rightText(rightText, console)); }
|
||||
|
||||
/**
|
||||
* Appends a component filling a line of chat (or console) with the configured decoration character and
|
||||
* color and a right-aligned text.
|
||||
* @param rightText the text aligned to the right.
|
||||
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
|
||||
* and color and a right-aligned text.
|
||||
* @deprecated uses Bungeecord chat API.
|
||||
*/
|
||||
@Deprecated
|
||||
public Chat thenRightText(BaseComponent rightText) { return thenRightText(chatComponent(rightText)); }
|
||||
|
||||
/**
|
||||
* Appends a component filling a line of chat (or console) with the configured decoration character and
|
||||
* Appends a component filling a chat line with the configured decoration character and
|
||||
* color and a centered text.
|
||||
* @param centerText the text aligned to the center.
|
||||
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
|
||||
* @return a new {@link FormatableChat} filling a chat line with the configured decoration character
|
||||
* and color and a centered text.
|
||||
*/
|
||||
public Chat thenCenterText(ComponentLike centerText) {
|
||||
@@ -494,21 +480,8 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a component filling a line of chat (or console) with the configured decoration character and
|
||||
* color and a centered text.
|
||||
* @param centerText the text aligned to the center.
|
||||
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
|
||||
* and color and a centered text.
|
||||
* @deprecated uses Bungeecord chat API.
|
||||
*/
|
||||
@Deprecated
|
||||
public Chat thenCenterText(BaseComponent centerText) {
|
||||
return thenCenterText(chatComponent(centerText));
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a component filling a line of chat (or console) with the configured decoration character and color.
|
||||
* @return a new {@link FormatableChat} filling a line of chat (or console) with a decoration character and color.
|
||||
* Appends a component filling a chat line with the configured decoration character and color.
|
||||
* @return a new {@link FormatableChat} filling a chat line with a decoration character and color.
|
||||
*/
|
||||
public Chat thenFilledLine() { return then(filledLine(console)); }
|
||||
|
||||
@@ -534,11 +507,11 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
|
||||
* .append("!").color(ChatColor.RED)
|
||||
* .create();
|
||||
* }</pre>
|
||||
* Here, when you call a formating method (like {@code bold(boolean)} or {@code color(ChatColor)}) after the
|
||||
* {@code append(String)} method, the formating apply to the last sub-component appended.
|
||||
* Here, when you call a formatting method (like {@code bold(boolean)} or {@code color(ChatColor)}) after the
|
||||
* {@code append(String)} method, the formatting apply to the last subcomponent appended.
|
||||
* <p>
|
||||
* In our design, we want the formating to apply to the currently builded component, not the last appended one.
|
||||
* The purpose is to make the component structure clearer and have better control of the formating over the
|
||||
* In our design, we want the formatting to apply to the currently built component, not the last appended one.
|
||||
* The purpose is to make the component structure clearer and have better control of the formatting over the
|
||||
* component hierarchy.
|
||||
* Here is the equivalent of the above code, with the {@link Chat} API:
|
||||
* <pre>{@code
|
||||
@@ -547,9 +520,9 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
|
||||
* .thenText("!"); // short for .then(Chat.text("!"))
|
||||
* // the red color for "!" is not needed because the parent component is already red.
|
||||
* }</pre>
|
||||
* When calling {@link #then(Component) #then(...)} on a {@link FormatableChat}, the method returns itself, casted
|
||||
* to {@link Chat}, to prevent future formating (that the programmer would think it formats the previously appended
|
||||
* sub-component). If the formatting of the currently builded component is needed, since {@link Chat} is a sealed
|
||||
* When calling {@link #then(Component) #then(...)} on a {@link FormatableChat}, the method returns itself, cast
|
||||
* to {@link Chat}, to prevent future formatting (that the programmer would think it formats the previously appended
|
||||
* subcomponent). If the formatting of the currently built component is needed, since {@link Chat} is a sealed
|
||||
* class which only subclass is {@link FormatableChat}, you can cast the builder, and use the format methods again.
|
||||
* <pre>{@code
|
||||
* Chat component = Chat.text("Hello ").red()
|
||||
@@ -585,12 +558,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
|
||||
* @return this.
|
||||
*/
|
||||
public FormatableChat color(TextColor c) { builder.color(c); return this; }
|
||||
/**
|
||||
* Sets the color of this component.
|
||||
* @param c the color.
|
||||
* @return this.
|
||||
*/
|
||||
public FormatableChat color(ChatColor c) { return color(c == null ? null : TextColor.color(c.getColor().getRGB())); }
|
||||
/**
|
||||
* Sets the color of this component.
|
||||
* @param c the color.
|
||||
@@ -602,7 +569,16 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
|
||||
* @param c the color.
|
||||
* @return this.
|
||||
*/
|
||||
public FormatableChat color(String c) { return color(c == null ? null : ChatColor.of(c)); }
|
||||
public FormatableChat color(String c) {
|
||||
if (c == null)
|
||||
return color((TextColor) null);
|
||||
TextColor tc = c.startsWith("#")
|
||||
? TextColor.fromCSSHexString(c)
|
||||
: NamedTextColor.NAMES.value(c.toLowerCase(Locale.ROOT));
|
||||
if (tc == null)
|
||||
throw new IllegalArgumentException("Invalid color string '" + c + "'.");
|
||||
return color(tc);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@@ -880,18 +856,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
|
||||
* @return this.
|
||||
*/
|
||||
public FormatableChat hover(ComponentLike v) { return hover(v.asComponent()); }
|
||||
/**
|
||||
* Configure this component to show the provided component when hovered.
|
||||
* @param v the component to show.
|
||||
* @return this.
|
||||
*/
|
||||
public FormatableChat hover(BaseComponent v) { return hover(toAdventure(v)); }
|
||||
/**
|
||||
* Configure this component to show the provided component when hovered.
|
||||
* @param v the component to show.
|
||||
* @return this.
|
||||
*/
|
||||
public FormatableChat hover(BaseComponent[] v) { return hover(toAdventure(v)); }
|
||||
/**
|
||||
* Configure this component to show the provided legacy text when hovered.
|
||||
* @param legacyText the legacy text to show.
|
||||
@@ -919,7 +883,7 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getAdv().hashCode();
|
||||
return get().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -930,63 +894,44 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
|
||||
|
||||
|
||||
|
||||
/* package */ static ComponentLike filterObjToComponentLike(Object v) {
|
||||
return switch (v) {
|
||||
case ComponentLike componentLike -> componentLike;
|
||||
case null, default -> Component.text(Objects.toString(v));
|
||||
};
|
||||
}
|
||||
|
||||
/* package */ static ComponentLike[] filterObjToComponentLike(Object[] values) {
|
||||
if (values == null)
|
||||
return null;
|
||||
ComponentLike[] ret = new ComponentLike[values.length];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
Object v = values[i];
|
||||
if (v instanceof BaseComponent[])
|
||||
ret[i] = toAdventure((BaseComponent[]) v);
|
||||
else if (v instanceof BaseComponent)
|
||||
ret[i] = toAdventure((BaseComponent) v);
|
||||
else if (v instanceof ComponentLike)
|
||||
ret[i] = (ComponentLike) v;
|
||||
else
|
||||
ret[i] = Component.text(Objects.toString(v));
|
||||
ret[i] = filterObjToComponentLike(values[i]);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts the Bungee {@link BaseComponent} array into Adventure {@link Component}.
|
||||
* @param components the Bungee {@link BaseComponent} array.
|
||||
* @return a {@link Component}.
|
||||
*/
|
||||
public static Component toAdventure(BaseComponent[] components) {
|
||||
return BungeeComponentSerializer.get().deserialize(components);
|
||||
}
|
||||
/**
|
||||
* Converts the Bungee {@link BaseComponent} into Adventure {@link Component}.
|
||||
* @param component the Bungee {@link BaseComponent}.
|
||||
* @return a {@link Component}.
|
||||
*/
|
||||
public static Component toAdventure(BaseComponent component) {
|
||||
return toAdventure(new BaseComponent[] { component });
|
||||
/* package */ static TranslationArgumentLike[] filterObjToTranslationArgumentLike(Object[] values) {
|
||||
if (values == null)
|
||||
return null;
|
||||
TranslationArgumentLike[] ret = new TranslationArgumentLike[values.length];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
Object v = values[i];
|
||||
if (v instanceof Number n)
|
||||
ret[i] = TranslationArgument.numeric(n);
|
||||
else if (v instanceof Boolean b)
|
||||
ret[i] = TranslationArgument.bool(b);
|
||||
else
|
||||
ret[i] = TranslationArgument.component(filterObjToComponentLike(values[i]));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the Adventure {@link Component} into Bungee {@link BaseComponent} array.
|
||||
* @param component the Adventure {@link Component}.
|
||||
* @return a {@link BaseComponent} array.
|
||||
*/
|
||||
public static BaseComponent[] toBungeeArray(Component component) {
|
||||
return BungeeComponentSerializer.get().serialize(component);
|
||||
}
|
||||
/**
|
||||
* Converts the Adventure {@link Component} into Bungee {@link BaseComponent}.
|
||||
* @param component the Adventure {@link Component}.
|
||||
* @return a {@link BaseComponent}.
|
||||
*/
|
||||
public static BaseComponent toBungee(Component component) {
|
||||
BaseComponent[] arr = toBungeeArray(component);
|
||||
return arr.length == 1 ? arr[0] : new net.md_5.bungee.api.chat.TextComponent(arr);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Force the italic formating to be set to false if it is not explicitely set in the component.
|
||||
* Force the italic formatting to be set to false if it is not explicitly set in the component.
|
||||
* This is useful for item lores that defaults to italic in the game UI.
|
||||
* @param c the {@link Chat} in which to set the italic property if needed.
|
||||
* @return the provided {@link Chat} instance.
|
||||
|
@@ -4,15 +4,30 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A custom gradient with a least 2 colors in it.
|
||||
* A custom gradient with at least 2 colors in it.
|
||||
*/
|
||||
public class ChatColorGradient {
|
||||
private record GradientColor(float location, TextColor color) { }
|
||||
|
||||
private record GradientColor(
|
||||
float location,
|
||||
TextColor color
|
||||
) implements Comparable<GradientColor> {
|
||||
@Override
|
||||
public int compareTo(@NotNull ChatColorGradient.GradientColor o) {
|
||||
return Float.compare(location(), o.location());
|
||||
}
|
||||
}
|
||||
|
||||
private final List<GradientColor> colors = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Create the custom gradient.
|
||||
*/
|
||||
public ChatColorGradient() {}
|
||||
|
||||
/**
|
||||
* Put a specific color at a specific location in the gradient.
|
||||
* @param gradientLocation the location in the gradient.
|
||||
@@ -21,6 +36,7 @@ public class ChatColorGradient {
|
||||
*/
|
||||
public synchronized ChatColorGradient add(float gradientLocation, TextColor gradientColor) {
|
||||
colors.add(new GradientColor(gradientLocation, gradientColor));
|
||||
colors.sort(null);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -31,25 +47,26 @@ public class ChatColorGradient {
|
||||
*/
|
||||
public synchronized TextColor pickColorAt(float gradientLocation) {
|
||||
if (colors.isEmpty())
|
||||
throw new IllegalStateException("Must define at least one color in this ChatValueGradient instance.");
|
||||
throw new IllegalStateException("Must define at least one color in this ChatColorGradient instance.");
|
||||
if (colors.size() == 1)
|
||||
return colors.get(0).color();
|
||||
return colors.getFirst().color();
|
||||
|
||||
colors.sort((p1, p2) -> Float.compare(p1.location(), p2.location()));
|
||||
|
||||
if (gradientLocation <= colors.get(0).location())
|
||||
return colors.get(0).color();
|
||||
if (gradientLocation >= colors.get(colors.size() - 1).location())
|
||||
return colors.get(colors.size() - 1).color();
|
||||
|
||||
int p1 = 1;
|
||||
for (; p1 < colors.size(); p1++) {
|
||||
if (colors.get(p1).location() >= gradientLocation)
|
||||
int i = 0;
|
||||
for (; i < colors.size(); i++) {
|
||||
if (gradientLocation <= colors.get(i).location())
|
||||
break;
|
||||
}
|
||||
int p0 = p1 - 1;
|
||||
float v0 = colors.get(p0).location(), v1 = colors.get(p1).location();
|
||||
TextColor cc0 = colors.get(p0).color(), cc1 = colors.get(p1).color();
|
||||
return ChatColorUtil.interpolateColor(v0, v1, gradientLocation, cc0, cc1);
|
||||
|
||||
if (i == 0)
|
||||
return colors.get(i).color();
|
||||
if (i == colors.size())
|
||||
return colors.getLast().color();
|
||||
|
||||
int p = i - 1;
|
||||
float pLoc = colors.get(p).location();
|
||||
float iLoc = colors.get(i).location();
|
||||
TextColor pCol = colors.get(p).color();
|
||||
TextColor iCol = colors.get(i).color();
|
||||
return ChatColorUtil.interpolateColor(pLoc, iLoc, gradientLocation, pCol, iCol);
|
||||
}
|
||||
}
|
||||
|
@@ -1,20 +1,20 @@
|
||||
package fr.pandacube.lib.chat;
|
||||
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import net.kyori.adventure.text.format.TextFormat;
|
||||
import net.kyori.adventure.util.RGBLike;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.kyori.adventure.util.RGBLike;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
|
||||
/**
|
||||
* Provides methods to manipulate legacy colors and {@link ChatColor} class.
|
||||
* Provides methods to manipulate legacy colors.
|
||||
*/
|
||||
public class ChatColorUtil {
|
||||
|
||||
|
||||
/**
|
||||
* All characters that represent a colorcode.
|
||||
* All characters that represent a color code.
|
||||
*/
|
||||
public static final String ALL_COLORS = "0123456789AaBbCcDdEeFf";
|
||||
/**
|
||||
@@ -30,7 +30,7 @@ public class ChatColorUtil {
|
||||
* Returns the legacy format needed to reproduce the format at the end of the provided legacy text.
|
||||
* Supports standard chat colors and formats, BungeeCord Chat rgb format and EssentialsX rgb format.
|
||||
* The RGB value from EssentialsX format is converted to BungeeCord Chat when included in the returned value.
|
||||
* @param legacyText the legacy formated text.
|
||||
* @param legacyText the legacy formatted text.
|
||||
* @return the active format at the end of the provided text.
|
||||
*/
|
||||
public static String getLastColors(String legacyText) {
|
||||
@@ -38,12 +38,12 @@ public class ChatColorUtil {
|
||||
int length = legacyText.length();
|
||||
|
||||
for (int index = length - 2; index >= 0; index--) {
|
||||
if (legacyText.charAt(index) == ChatColor.COLOR_CHAR) {
|
||||
if (legacyText.charAt(index) == LegacyChatFormat.COLOR_CHAR) {
|
||||
|
||||
// detection of rgb color §x§0§1§2§3§4§5
|
||||
String rgb;
|
||||
if (index > 11
|
||||
&& legacyText.charAt(index - 12) == ChatColor.COLOR_CHAR
|
||||
&& legacyText.charAt(index - 12) == LegacyChatFormat.COLOR_CHAR
|
||||
&& (legacyText.charAt(index - 11) == 'x'
|
||||
|| legacyText.charAt(index - 11) == 'X')
|
||||
&& HEX_COLOR_PATTERN.matcher(rgb = legacyText.substring(index - 12, index + 2)).matches()) {
|
||||
@@ -64,7 +64,7 @@ public class ChatColorUtil {
|
||||
|
||||
// try detect non-rgb format
|
||||
char colorChar = legacyText.charAt(index + 1);
|
||||
ChatColor legacyColor = getChatColorByChar(colorChar);
|
||||
LegacyChatFormat legacyColor = LegacyChatFormat.of(colorChar);
|
||||
|
||||
if (legacyColor != null) {
|
||||
result.insert(0, legacyColor);
|
||||
@@ -83,15 +83,6 @@ public class ChatColorUtil {
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ChatColor} associated with the provided char, case insensitive.
|
||||
* @param code the case insensitive char code.
|
||||
* @return the corresponding {@link ChatColor}.
|
||||
*/
|
||||
public static ChatColor getChatColorByChar(char code) {
|
||||
return ChatColor.getByChar(Character.toLowerCase(code));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -99,7 +90,7 @@ public class ChatColorUtil {
|
||||
* Translate the color code of the provided string, that uses the alt color char, to the {@code §} color code
|
||||
* format.
|
||||
* <p>
|
||||
* This method is the improved version of {@link ChatColor#translateAlternateColorCodes(char, String)},
|
||||
* This method is the improved version of Bukkit’s {@code ChatColor.translateAlternateColorCodes(char, String)},
|
||||
* because it takes into account essentials RGB color code, and {@code altColorChar} escaping (by doubling it).
|
||||
* Essentials RGB color code are converted to Bungee chat RGB format, so the returned string can be converted
|
||||
* to component (see {@link Chat#legacyText(Object)}).
|
||||
@@ -112,7 +103,7 @@ public class ChatColorUtil {
|
||||
*/
|
||||
public static String translateAlternateColorCodes(char altColorChar, String textToTranslate)
|
||||
{
|
||||
char colorChar = ChatColor.COLOR_CHAR;
|
||||
char colorChar = LegacyChatFormat.COLOR_CHAR;
|
||||
StringBuilder acc = new StringBuilder();
|
||||
char[] b = textToTranslate.toCharArray();
|
||||
for ( int i = 0; i < b.length; i++ )
|
||||
@@ -180,7 +171,7 @@ public class ChatColorUtil {
|
||||
* @return the text fully italic.
|
||||
*/
|
||||
public static String forceItalic(String legacyText) {
|
||||
return forceFormat(legacyText, ChatColor.ITALIC);
|
||||
return forceFormat(legacyText, TextDecoration.ITALIC);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,7 +181,7 @@ public class ChatColorUtil {
|
||||
* @return the text fully bold.
|
||||
*/
|
||||
public static String forceBold(String legacyText) {
|
||||
return forceFormat(legacyText, ChatColor.BOLD);
|
||||
return forceFormat(legacyText, TextDecoration.BOLD);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,7 +191,7 @@ public class ChatColorUtil {
|
||||
* @return the text fully underlined.
|
||||
*/
|
||||
public static String forceUnderline(String legacyText) {
|
||||
return forceFormat(legacyText, ChatColor.UNDERLINE);
|
||||
return forceFormat(legacyText, TextDecoration.UNDERLINED);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,7 +201,7 @@ public class ChatColorUtil {
|
||||
* @return the text fully stroked through.
|
||||
*/
|
||||
public static String forceStrikethrough(String legacyText) {
|
||||
return forceFormat(legacyText, ChatColor.STRIKETHROUGH);
|
||||
return forceFormat(legacyText, TextDecoration.STRIKETHROUGH);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,15 +211,16 @@ public class ChatColorUtil {
|
||||
* @return the text fully obfuscated.
|
||||
*/
|
||||
public static String forceObfuscated(String legacyText) {
|
||||
return forceFormat(legacyText, ChatColor.MAGIC);
|
||||
return forceFormat(legacyText, TextDecoration.OBFUSCATED);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static String forceFormat(String legacyText, ChatColor format) {
|
||||
private static String forceFormat(String legacyText, TextFormat format) {
|
||||
String formatStr = LegacyChatFormat.of(format).toString();
|
||||
return format + legacyText
|
||||
.replace(format.toString(), "") // remove previous tag to make the result cleaner
|
||||
.replaceAll("§([a-frA-FR\\d])", "§$1" + format);
|
||||
.replace(formatStr, "") // remove previous tag to make the result cleaner
|
||||
.replaceAll("§([a-frA-FR\\d])", "§$1" + formatStr);
|
||||
}
|
||||
|
||||
|
||||
@@ -243,40 +235,12 @@ public class ChatColorUtil {
|
||||
* @return the resulting text.
|
||||
*/
|
||||
public static String resetToColor(String legacyText, String color) {
|
||||
return legacyText.replace(ChatColor.RESET.toString(), color);
|
||||
return legacyText.replace(LegacyChatFormat.RESET.toString(), color);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Converts the provided {@link ChatColor} to its Adventure counterpart.
|
||||
* @param bungee a BungeeCord {@link ChatColor} instance.
|
||||
* @return the {@link TextColor} equivalent to the provided {@link ChatColor}.
|
||||
*/
|
||||
public static TextColor toAdventure(ChatColor bungee) {
|
||||
if (bungee == null)
|
||||
return null;
|
||||
if (bungee.getColor() == null)
|
||||
throw new IllegalArgumentException("The provided Bungee ChatColor must be an actual color (not format nor reset).");
|
||||
return TextColor.color(bungee.getColor().getRGB());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided {@link TextColor} to its BungeeCord counterpart.
|
||||
* @param col a Adventure {@link TextColor} instance.
|
||||
* @return the {@link ChatColor} equivalent to the provided {@link TextColor}.
|
||||
*/
|
||||
public static ChatColor toBungee(TextColor col) {
|
||||
if (col == null)
|
||||
return null;
|
||||
if (col instanceof NamedTextColor) {
|
||||
return ChatColor.of(((NamedTextColor) col).toString());
|
||||
}
|
||||
return ChatColor.of(col.asHexString());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a color, interpolating between 2 colors.
|
||||
* @param v0 the value corresponding to color {@code cc0}.
|
||||
@@ -293,4 +257,7 @@ public class ChatColorUtil {
|
||||
}
|
||||
|
||||
|
||||
|
||||
private ChatColorUtil() {}
|
||||
|
||||
}
|
@@ -8,6 +8,7 @@ import net.kyori.adventure.text.format.TextColor;
|
||||
/**
|
||||
* Class holding static configuration values for chat component rendering.
|
||||
*/
|
||||
@SuppressWarnings("CanBeFinal")
|
||||
public class ChatConfig {
|
||||
|
||||
/**
|
||||
@@ -29,7 +30,7 @@ public class ChatConfig {
|
||||
/**
|
||||
* The color used for successful messages.
|
||||
*/
|
||||
public static TextColor successColor = PandaTheme.CHAT_GREEN_SATMAX;
|
||||
public static TextColor successColor = PandaTheme.CHAT_GREEN_MAX_SAT;
|
||||
|
||||
/**
|
||||
* The color used for error/failure messages.
|
||||
@@ -49,7 +50,7 @@ public class ChatConfig {
|
||||
/**
|
||||
* The color used to display data in a message.
|
||||
*/
|
||||
public static TextColor dataColor = PandaTheme.CHAT_GRAY_MID;
|
||||
public static TextColor dataColor = NamedTextColor.GRAY;
|
||||
|
||||
/**
|
||||
* The color used for displayed URLs and clickable URLs.
|
||||
@@ -67,14 +68,14 @@ public class ChatConfig {
|
||||
public static TextColor highlightedCommandColor = NamedTextColor.WHITE;
|
||||
|
||||
/**
|
||||
* The color used for broadcasted messages.
|
||||
* The color used for broadcast messages.
|
||||
* It is often used in combination with {@link #prefix}.
|
||||
*/
|
||||
public static TextColor broadcastColor = NamedTextColor.YELLOW;
|
||||
|
||||
/**
|
||||
* The prefix used for prefixed messages.
|
||||
* It can be a sylized name of the server, like {@code "[Pandacube] "}.
|
||||
* It can be a stylized name of the server, like {@code "[Pandacube] "}.
|
||||
* It is often used in combination with {@link #broadcastColor}.
|
||||
*/
|
||||
public static Supplier<Chat> prefix = PandaTheme::CHAT_MESSAGE_PREFIX;
|
||||
@@ -86,48 +87,69 @@ public class ChatConfig {
|
||||
*/
|
||||
public static int getPrefixWidth(boolean console) {
|
||||
Chat c;
|
||||
return prefix == null ? 0 : (c = prefix.get()) == null ? 0 : ChatUtil.componentWidth(c.getAdv(), console);
|
||||
return prefix == null ? 0 : (c = prefix.get()) == null ? 0 : ChatUtil.componentWidth(c.get(), console);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A set of predefined colors.
|
||||
*/
|
||||
public static class PandaTheme {
|
||||
|
||||
/** Green 1 color. */
|
||||
public static final TextColor CHAT_GREEN_1_NORMAL = TextColor.fromHexString("#3db849"); // h=126 s=50 l=48
|
||||
/** Green 2 color. */
|
||||
public static final TextColor CHAT_GREEN_2 = TextColor.fromHexString("#5ec969"); // h=126 s=50 l=58
|
||||
/** Green 3 color. */
|
||||
public static final TextColor CHAT_GREEN_3 = TextColor.fromHexString("#85d68d"); // h=126 s=50 l=68
|
||||
/** Green 4 color. */
|
||||
public static final TextColor CHAT_GREEN_4 = TextColor.fromHexString("#abe3b0"); // h=126 s=50 l=78
|
||||
|
||||
public static final TextColor CHAT_GREEN_SATMAX = TextColor.fromHexString("#00ff19"); // h=126 s=100 l=50
|
||||
/** Green max saturation color. */
|
||||
public static final TextColor CHAT_GREEN_MAX_SAT = TextColor.fromHexString("#00ff19"); // h=126 s=100 l=50
|
||||
/** Green 1 saturated color. */
|
||||
public static final TextColor CHAT_GREEN_1_SAT = TextColor.fromHexString("#20d532"); // h=126 s=50 l=48
|
||||
/** Green 2 saturated color. */
|
||||
public static final TextColor CHAT_GREEN_2_SAT = TextColor.fromHexString("#45e354"); // h=126 s=50 l=58
|
||||
/** Green 3 saturated color. */
|
||||
public static final TextColor CHAT_GREEN_3_SAT = TextColor.fromHexString("#71ea7d"); // h=126 s=50 l=68
|
||||
/** Green 4 saturated color. */
|
||||
public static final TextColor CHAT_GREEN_4_SAT = TextColor.fromHexString("#9df0a6"); // h=126 s=50 l=78
|
||||
|
||||
/** Brown 1 color. */
|
||||
public static final TextColor CHAT_BROWN_1 = TextColor.fromHexString("#b26d3a"); // h=26 s=51 l=46
|
||||
/** Brown 2 color. */
|
||||
public static final TextColor CHAT_BROWN_2 = TextColor.fromHexString("#cd9265"); // h=26 s=51 l=60
|
||||
/** Brown 3 color. */
|
||||
public static final TextColor CHAT_BROWN_3 = TextColor.fromHexString("#e0bb9f"); // h=26 s=51 l=75
|
||||
|
||||
/** Brown 1 saturated color. */
|
||||
public static final TextColor CHAT_BROWN_1_SAT = TextColor.fromHexString("#b35c19"); // h=26 s=75 l=40
|
||||
/** Brown 2 saturated color. */
|
||||
public static final TextColor CHAT_BROWN_2_SAT = TextColor.fromHexString("#e28136"); // h=26 s=51 l=55
|
||||
/** Brown 3 saturated color. */
|
||||
public static final TextColor CHAT_BROWN_3_SAT = TextColor.fromHexString("#ecab79"); // h=26 s=51 l=70
|
||||
|
||||
/** Gray medium color. */
|
||||
public static final TextColor CHAT_GRAY_MID = TextColor.fromHexString("#888888");
|
||||
|
||||
/** Red failure color. */
|
||||
public static final TextColor CHAT_RED_FAILURE = TextColor.fromHexString("#ff3333");
|
||||
|
||||
|
||||
/** Color used for private message prefix decoration. */
|
||||
public static final TextColor CHAT_PM_PREFIX_DECORATION = CHAT_BROWN_2_SAT;
|
||||
/** Color used for sent message text. */
|
||||
public static final TextColor CHAT_PM_SELF_MESSAGE = CHAT_GREEN_2;
|
||||
/** Color used for received message text. */
|
||||
public static final TextColor CHAT_PM_OTHER_MESSAGE = CHAT_GREEN_4;
|
||||
|
||||
|
||||
/** Discord color. */
|
||||
public static final TextColor CHAT_DISCORD_LINK_COLOR = TextColor.fromHexString("#00aff4");
|
||||
|
||||
|
||||
/**
|
||||
* Generate a prefix for broadcast message.
|
||||
* @return a prefix for broadcast message.
|
||||
*/
|
||||
public static Chat CHAT_MESSAGE_PREFIX() {
|
||||
return Chat.text("[")
|
||||
.broadcastColor()
|
||||
@@ -135,5 +157,9 @@ public class ChatConfig {
|
||||
.thenText("] ");
|
||||
}
|
||||
|
||||
private PandaTheme() {}
|
||||
|
||||
}
|
||||
|
||||
private ChatConfig() {}
|
||||
}
|
||||
|
@@ -59,6 +59,7 @@ public class ChatFilledLine implements ComponentLike {
|
||||
private boolean decorationBold = false;
|
||||
private int nbSide = ChatConfig.nbCharMargin;
|
||||
private boolean spacesAroundText = false;
|
||||
private boolean spacesDecorationRightSide = false;
|
||||
private boolean console = false;
|
||||
private Integer maxWidth = null;
|
||||
|
||||
@@ -116,6 +117,16 @@ public class ChatFilledLine implements ComponentLike {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the {@link #decoChar(char)} is set to space, also add spaces at the right of the text
|
||||
* to reach the desired width.
|
||||
* @return this.
|
||||
*/
|
||||
public ChatFilledLine spacesDecorationRightSide() {
|
||||
spacesDecorationRightSide = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure if the line will be rendered on console or not.
|
||||
* @param console true for console, false for game UI.
|
||||
@@ -140,7 +151,7 @@ public class ChatFilledLine implements ComponentLike {
|
||||
|
||||
/**
|
||||
* Renders this line to a {@link FormatableChat}.
|
||||
* @return a new {@link FormatableChat} builded by this {@link ChatFilledLine}.
|
||||
* @return a new {@link FormatableChat} built by this {@link ChatFilledLine}.
|
||||
*/
|
||||
public FormatableChat toChat() {
|
||||
int maxWidth = (this.maxWidth != null)
|
||||
@@ -184,7 +195,7 @@ public class ChatFilledLine implements ComponentLike {
|
||||
Chat d = Chat.chat()
|
||||
.then(Chat.text(ChatUtil.repeatedChar(decorationChar, nbCharLeft)).color(decorationColor).bold(decorationBold))
|
||||
.then(text);
|
||||
if (decorationChar != ' ')
|
||||
if (decorationChar != ' ' || spacesDecorationRightSide)
|
||||
d.then(Chat.text(ChatUtil.repeatedChar(decorationChar, nbCharRight)).color(decorationColor).bold(decorationBold));
|
||||
return (FormatableChat) d;
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package fr.pandacube.lib.chat;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import fr.pandacube.lib.chat.Chat.FormatableChat;
|
||||
import net.kyori.adventure.text.BlockNBTComponent;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.ComponentBuilder;
|
||||
@@ -16,10 +15,10 @@ import net.kyori.adventure.text.TranslatableComponent;
|
||||
import net.kyori.adventure.text.event.HoverEventSource;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
import net.md_5.bungee.api.chat.BaseComponent;
|
||||
|
||||
import fr.pandacube.lib.chat.Chat.FormatableChat;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Abstract class holding the publicly accessible methods to create an instance of {@link Chat} component.
|
||||
@@ -32,15 +31,6 @@ public abstract class ChatStatic {
|
||||
return new FormatableChat(componentToBuilder(c));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link FormatableChat} from the provided Bungee {@link BaseComponent}.
|
||||
* @param c the {@link BaseComponent}.
|
||||
* @return a new {@link FormatableChat}.
|
||||
*/
|
||||
public static FormatableChat chatComponent(BaseComponent c) {
|
||||
return new FormatableChat(componentToBuilder(Chat.toAdventure(c)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link FormatableChat} from the provided {@link ComponentLike}.
|
||||
* If the provided component is an instance of {@link Chat}, its content will be duplicated, and the provided one
|
||||
@@ -60,15 +50,6 @@ public abstract class ChatStatic {
|
||||
return new FormatableChat(Component.text());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link FormatableChat} from the provided Bungee {@link BaseComponent BaseComponent[]}.
|
||||
* @param c the array of {@link BaseComponent}.
|
||||
* @return a new {@link FormatableChat}.
|
||||
*/
|
||||
public static FormatableChat chatComponent(BaseComponent[] c) {
|
||||
return chatComponent(Chat.toAdventure(c));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -91,18 +72,58 @@ public abstract class ChatStatic {
|
||||
|
||||
|
||||
/**
|
||||
* Creates a {@link FormatableChat} with the provided legacy text as its content.
|
||||
* @param legacyText the legacy text to use as the content.
|
||||
* Creates a {@link FormatableChat} with the provided legacy text as its content, using the section {@code "§"}
|
||||
* character.
|
||||
* @param legacyText the legacy text to use as the content, that uses the {@code "§"} character.
|
||||
* @return a new {@link FormatableChat} with the provided text as its content.
|
||||
* @throws IllegalArgumentException If the {@code plainText} parameter is instance of {@link Chat} or
|
||||
* @throws IllegalArgumentException If the {@code legacyText} parameter is instance of {@link Chat} or
|
||||
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)}
|
||||
* instead.
|
||||
*/
|
||||
public static FormatableChat legacyText(Object legacyText) {
|
||||
return legacyText(legacyText, LegacyComponentSerializer.SECTION_CHAR);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a {@link FormatableChat} with the provided legacy text as its content, using the ampersand {@code "&"}
|
||||
* character.
|
||||
* @param legacyText the legacy text to use as the content, that uses the {@code "&"} character.
|
||||
* @return a new {@link FormatableChat} with the provided text as its content.
|
||||
* @throws IllegalArgumentException If the {@code legacyText} parameter is instance of {@link Chat} or
|
||||
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)}
|
||||
* instead.
|
||||
*/
|
||||
public static FormatableChat legacyAmpersandText(Object legacyText) {
|
||||
return legacyText(legacyText, LegacyComponentSerializer.AMPERSAND_CHAR);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a {@link FormatableChat} with the provided legacy text as its content, using the specified
|
||||
* legacyCharacter.
|
||||
* @param legacyText the legacy text to use as the content.
|
||||
* @param legacyCharacter the character used in the provided text to prefix color and format code.
|
||||
* @return a new {@link FormatableChat} with the provided text as its content.
|
||||
* @throws IllegalArgumentException If the {@code legacyText} parameter is instance of {@link Chat} or
|
||||
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)}
|
||||
* instead.
|
||||
*/
|
||||
private static FormatableChat legacyText(Object legacyText, char legacyCharacter) {
|
||||
if (legacyText instanceof ComponentLike) {
|
||||
throw new IllegalArgumentException("Expected any object except instance of " + ComponentLike.class + ". Received " + legacyText + ". Please use ChatStatic.chatComponent(ComponentLike) instead.");
|
||||
}
|
||||
return chatComponent(LegacyComponentSerializer.legacySection().deserialize(Objects.toString(legacyText)));
|
||||
return chatComponent(LegacyComponentSerializer.legacy(legacyCharacter).deserialize(Objects.toString(legacyText)));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a {@link FormatableChat} with the provided MiniMessage text as its content.
|
||||
* @param miniMessageText the MiniMessage text to use as the content.
|
||||
* @return a new {@link FormatableChat} with the provided text as its content.
|
||||
*/
|
||||
public static FormatableChat miniMessageText(String miniMessageText) {
|
||||
return chatComponent(MiniMessage.miniMessage().deserialize(miniMessageText));
|
||||
}
|
||||
|
||||
|
||||
@@ -207,7 +228,7 @@ public abstract class ChatStatic {
|
||||
* @param c the {@link Component}.
|
||||
* @return a new {@link FormatableChat}.
|
||||
*/
|
||||
public static FormatableChat playerNameComponent(Component c) {
|
||||
public static FormatableChat playerNameComponent(ComponentLike c) {
|
||||
FormatableChat fc = chatComponent(c);
|
||||
fc.builder.colorIfAbsent(NamedTextColor.WHITE);
|
||||
return fc;
|
||||
@@ -223,13 +244,13 @@ public abstract class ChatStatic {
|
||||
* @return a new {@link FormatableChat} with the provided translation key and parameters.
|
||||
*/
|
||||
public static FormatableChat translation(String key, Object... with) {
|
||||
return new FormatableChat(Component.translatable().key(key).args(Chat.filterObjToComponentLike(with)));
|
||||
return new FormatableChat(Component.translatable().key(key).arguments(Chat.filterObjToTranslationArgumentLike(with)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link FormatableChat} with the provided keybind.
|
||||
* @param key the keybind to display.
|
||||
* @return a new {@link FormatableChat} with the provided keybind.
|
||||
* Creates a {@link FormatableChat} with the provided keybinding.
|
||||
* @param key the keybinding to display.
|
||||
* @return a new {@link FormatableChat} with the provided keybinding.
|
||||
*/
|
||||
public static FormatableChat keybind(String key) {
|
||||
return new FormatableChat(Component.keybind().keybind(key));
|
||||
@@ -451,12 +472,12 @@ public abstract class ChatStatic {
|
||||
|
||||
|
||||
/**
|
||||
* Creates a {@link FormatableChat} filling a line of chat (or console) with decoration and a left-aligned text.
|
||||
* Creates a {@link FormatableChat} filling a chat line with decoration and a left-aligned text.
|
||||
* @param text the text aligned to the left.
|
||||
* @param decorationChar the character used for decoration around the text.
|
||||
* @param decorationColor the color used for the decoration characters.
|
||||
* @param console if the line is rendered on console (true) or IG (false).
|
||||
* @return a new {@link FormatableChat} filling a line of chat (or console) with decoration and a left-aligned text.
|
||||
* @return a new {@link FormatableChat} filling a chat line with decoration and a left-aligned text.
|
||||
* @see ChatFilledLine#leftText(ComponentLike)
|
||||
*/
|
||||
public static FormatableChat leftText(ComponentLike text, char decorationChar, TextColor decorationColor, boolean console) {
|
||||
@@ -464,11 +485,11 @@ public abstract class ChatStatic {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link FormatableChat} filling a line of chat (or console) with the configured decoration character and
|
||||
* Creates a {@link FormatableChat} filling a chat line with the configured decoration character and
|
||||
* color and a left-aligned text.
|
||||
* @param text the text aligned to the left.
|
||||
* @param console if the line is rendered on console (true) or IG (false).
|
||||
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
|
||||
* @return a new {@link FormatableChat} filling a chat line with the configured decoration character
|
||||
* and color and a left-aligned text.
|
||||
* @see ChatFilledLine#leftText(ComponentLike)
|
||||
* @see ChatConfig#decorationChar
|
||||
@@ -479,12 +500,12 @@ public abstract class ChatStatic {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link FormatableChat} filling a line of chat (or console) with decoration and a right-aligned text.
|
||||
* Creates a {@link FormatableChat} filling a chat line with decoration and a right-aligned text.
|
||||
* @param text the text aligned to the right.
|
||||
* @param decorationChar the character used for decoration around the text.
|
||||
* @param decorationColor the color used for the decoration characters.
|
||||
* @param console if the line is rendered on console (true) or IG (false).
|
||||
* @return a new {@link FormatableChat} filling a line of chat (or console) with decoration and a right-aligned
|
||||
* @return a new {@link FormatableChat} filling a chat line with decoration and a right-aligned
|
||||
* text.
|
||||
* @see ChatFilledLine#rightText(ComponentLike)
|
||||
*/
|
||||
@@ -493,11 +514,11 @@ public abstract class ChatStatic {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link FormatableChat} filling a line of chat (or console) with the configured decoration character and
|
||||
* Creates a {@link FormatableChat} filling a chat line with the configured decoration character and
|
||||
* color and a right-aligned text.
|
||||
* @param text the text aligned to the right.
|
||||
* @param console if the line is rendered on console (true) or IG (false).
|
||||
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
|
||||
* @return a new {@link FormatableChat} filling a chat line with the configured decoration character
|
||||
* and color and a right-aligned text.
|
||||
* @see ChatFilledLine#rightText(ComponentLike)
|
||||
* @see ChatConfig#decorationChar
|
||||
@@ -508,12 +529,12 @@ public abstract class ChatStatic {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link FormatableChat} filling a line of chat (or console) with decoration and a centered text.
|
||||
* Creates a {@link FormatableChat} filling a chat line with decoration and a centered text.
|
||||
* @param text the text aligned to the center.
|
||||
* @param decorationChar the character used for decoration around the text.
|
||||
* @param decorationColor the color used for the decoration characters.
|
||||
* @param console if the line is rendered on console (true) or IG (false).
|
||||
* @return a new {@link FormatableChat} filling a line of chat (or console) with decoration and a centered text.
|
||||
* @return a new {@link FormatableChat} filling a chat line with decoration and a centered text.
|
||||
* @see ChatFilledLine#centerText(ComponentLike)
|
||||
*/
|
||||
public static FormatableChat centerText(ComponentLike text, char decorationChar, TextColor decorationColor, boolean console) {
|
||||
@@ -521,11 +542,11 @@ public abstract class ChatStatic {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link FormatableChat} filling a line of chat (or console) with the configured decoration character and
|
||||
* Creates a {@link FormatableChat} filling a chat line with the configured decoration character and
|
||||
* color and a centered text.
|
||||
* @param text the text aligned to the center.
|
||||
* @param console if the line is rendered on console (true) or IG (false).
|
||||
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
|
||||
* @return a new {@link FormatableChat} filling a chat line with the configured decoration character
|
||||
* and color and a centered text.
|
||||
* @see ChatFilledLine#centerText(ComponentLike)
|
||||
* @see ChatConfig#decorationChar
|
||||
@@ -536,11 +557,11 @@ public abstract class ChatStatic {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link FormatableChat} filling a line of chat (or console) with a decoration character and color.
|
||||
* Creates a {@link FormatableChat} filling a chat line with a decoration character and color.
|
||||
* @param decorationChar the character used for decoration.
|
||||
* @param decorationColor the color used for the decoration characters.
|
||||
* @param console if the line is rendered on console (true) or IG (false).
|
||||
* @return a new {@link FormatableChat} filling a line of chat (or console) with a decoration character and color.
|
||||
* @return a new {@link FormatableChat} filling a chat line with a decoration character and color.
|
||||
* @see ChatFilledLine#filled()
|
||||
*/
|
||||
public static FormatableChat filledLine(char decorationChar, TextColor decorationColor, boolean console) {
|
||||
@@ -548,10 +569,10 @@ public abstract class ChatStatic {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link FormatableChat} filling a line of chat (or console) with the configured decoration character and
|
||||
* Creates a {@link FormatableChat} filling a chat line with the configured decoration character and
|
||||
* color.
|
||||
* @param console if the line is rendered on console (true) or IG (false).
|
||||
* @return a new {@link FormatableChat} filling a line of chat (or console) with a decoration character and color.
|
||||
* @return a new {@link FormatableChat} filling a chat line with a decoration character and color.
|
||||
* @see ChatFilledLine#filled()
|
||||
* @see ChatConfig#decorationChar
|
||||
* @see ChatConfig#decorationColor
|
||||
@@ -591,52 +612,39 @@ public abstract class ChatStatic {
|
||||
|
||||
|
||||
private static ComponentBuilder<?, ?> componentToBuilder(Component c) {
|
||||
ComponentBuilder<?, ?> builder;
|
||||
if (c instanceof TextComponent) {
|
||||
builder = Component.text()
|
||||
.content(((TextComponent) c).content());
|
||||
}
|
||||
else if (c instanceof TranslatableComponent) {
|
||||
builder = Component.translatable()
|
||||
.key(((TranslatableComponent) c).key())
|
||||
.args(((TranslatableComponent) c).args());
|
||||
}
|
||||
else if (c instanceof SelectorComponent) {
|
||||
builder = Component.selector()
|
||||
.pattern(((SelectorComponent) c).pattern());
|
||||
}
|
||||
else if (c instanceof ScoreComponent) {
|
||||
builder = Component.score()
|
||||
.name(((ScoreComponent) c).name())
|
||||
.objective(((ScoreComponent) c).objective());
|
||||
}
|
||||
else if (c instanceof KeybindComponent) {
|
||||
builder = Component.keybind()
|
||||
.keybind(((KeybindComponent) c).keybind());
|
||||
}
|
||||
else if (c instanceof BlockNBTComponent) {
|
||||
builder = Component.blockNBT()
|
||||
.interpret(((BlockNBTComponent) c).interpret())
|
||||
.nbtPath(((BlockNBTComponent) c).nbtPath())
|
||||
.pos(((BlockNBTComponent) c).pos());
|
||||
}
|
||||
else if (c instanceof EntityNBTComponent) {
|
||||
builder = Component.entityNBT()
|
||||
.interpret(((EntityNBTComponent) c).interpret())
|
||||
.nbtPath(((EntityNBTComponent) c).nbtPath())
|
||||
.selector(((EntityNBTComponent) c).selector());
|
||||
}
|
||||
else if (c instanceof StorageNBTComponent) {
|
||||
builder = Component.storageNBT()
|
||||
.interpret(((StorageNBTComponent) c).interpret())
|
||||
.nbtPath(((StorageNBTComponent) c).nbtPath())
|
||||
.storage(((StorageNBTComponent) c).storage());
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Unknows component type " + c.getClass());
|
||||
}
|
||||
ComponentBuilder<?, ?> builder = switch (c) {
|
||||
case TextComponent textComponent -> Component.text()
|
||||
.content(textComponent.content());
|
||||
case TranslatableComponent translatableComponent -> Component.translatable()
|
||||
.key(translatableComponent.key()).arguments(translatableComponent.arguments());
|
||||
case SelectorComponent selectorComponent -> Component.selector()
|
||||
.pattern(selectorComponent.pattern());
|
||||
case ScoreComponent scoreComponent -> Component.score()
|
||||
.name(scoreComponent.name())
|
||||
.objective(scoreComponent.objective());
|
||||
case KeybindComponent keybindComponent -> Component.keybind()
|
||||
.keybind(keybindComponent.keybind());
|
||||
case BlockNBTComponent blockNBTComponent -> Component.blockNBT()
|
||||
.interpret(blockNBTComponent.interpret())
|
||||
.nbtPath(blockNBTComponent.nbtPath())
|
||||
.pos(blockNBTComponent.pos());
|
||||
case EntityNBTComponent entityNBTComponent -> Component.entityNBT()
|
||||
.interpret(entityNBTComponent.interpret())
|
||||
.nbtPath(entityNBTComponent.nbtPath())
|
||||
.selector(entityNBTComponent.selector());
|
||||
case StorageNBTComponent storageNBTComponent -> Component.storageNBT()
|
||||
.interpret(storageNBTComponent.interpret())
|
||||
.nbtPath(storageNBTComponent.nbtPath())
|
||||
.storage(storageNBTComponent.storage());
|
||||
case null, default -> throw new IllegalArgumentException("Unknown component type " + (c == null ? "null" : c.getClass()));
|
||||
};
|
||||
return builder.style(c.style()).append(c.children());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ChatStatic} instance.
|
||||
*/
|
||||
protected ChatStatic() {}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -1,10 +1,13 @@
|
||||
package fr.pandacube.lib.chat;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.ComponentLike;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A tree structure of {@link Chat} component intended to be rendered in chat using {@link #render(boolean)}.
|
||||
* A tree structure of chat {@link Component} intended to be rendered in chat using {@link #render(boolean)}.
|
||||
*/
|
||||
public class ChatTreeNode {
|
||||
|
||||
@@ -19,7 +22,7 @@ public class ChatTreeNode {
|
||||
/**
|
||||
* The component for the current node.
|
||||
*/
|
||||
public final Chat component;
|
||||
public final ComponentLike component;
|
||||
|
||||
/**
|
||||
* Children nodes.
|
||||
@@ -27,10 +30,10 @@ public class ChatTreeNode {
|
||||
public final List<ChatTreeNode> children = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Construct an new {@link ChatTreeNode}.
|
||||
* Construct a new {@link ChatTreeNode}.
|
||||
* @param cmp the component for the current node.
|
||||
*/
|
||||
public ChatTreeNode(Chat cmp) {
|
||||
public ChatTreeNode(ComponentLike cmp) {
|
||||
component = cmp;
|
||||
}
|
||||
|
||||
@@ -48,9 +51,9 @@ public class ChatTreeNode {
|
||||
* Generate a tree view based on this tree structure.
|
||||
* <p>
|
||||
* Each element in the returned list represent 1 line of this tree view.
|
||||
* Thus, the caller may send each line separately or at once depending of the quantity of data.
|
||||
* Thus, the caller may send each line separately or at once, depending on the quantity of data.
|
||||
* @param console true to render for console, false otherwise.
|
||||
* @return an array of component, each element being a single line.
|
||||
* @return a list of component, each element being a single line.
|
||||
*/
|
||||
public List<Chat> render(boolean console) {
|
||||
List<Chat> ret = new ArrayList<>();
|
||||
|
@@ -1,5 +1,17 @@
|
||||
package fr.pandacube.lib.chat;
|
||||
|
||||
import fr.pandacube.lib.chat.Chat.FormatableChat;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.ComponentLike;
|
||||
import net.kyori.adventure.text.TextComponent;
|
||||
import net.kyori.adventure.text.TranslatableComponent;
|
||||
import net.kyori.adventure.text.TranslationArgument;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import net.kyori.adventure.text.format.TextDecoration.State;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -10,19 +22,10 @@ import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.TextComponent;
|
||||
import net.kyori.adventure.text.TranslatableComponent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import net.kyori.adventure.text.format.TextDecoration.State;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
|
||||
import fr.pandacube.lib.chat.Chat.FormatableChat;
|
||||
import static fr.pandacube.lib.chat.ChatStatic.chat;
|
||||
|
||||
/**
|
||||
* Provides various methods and properties to manipulate text displayed in chat an other parts of the game.
|
||||
* Provides various methods and properties to manipulate text displayed in chat and other parts of the game.
|
||||
*/
|
||||
public class ChatUtil {
|
||||
|
||||
@@ -48,7 +51,7 @@ public class ChatUtil {
|
||||
|
||||
/**
|
||||
* Mapping indicating the text pixel with for specific characters in the default Minecraft font.
|
||||
* If a character doesn’t have a mapping in this map, then its width is {@link #DEFAULT_CHAR_SIZE}.
|
||||
* If a character doesn't have a mapping in this map, then its width is {@link #DEFAULT_CHAR_SIZE}.
|
||||
*/
|
||||
public static final Map<Character, Integer> CHAR_SIZES;
|
||||
static {
|
||||
@@ -112,7 +115,7 @@ public class ChatUtil {
|
||||
* @param nbPages the number of pages.
|
||||
* @param nbPagesToDisplay the number of pages to display around the first page, the last page and the
|
||||
* {@code currentPage}.
|
||||
* @return a {@link Chat} containging the created page navigator.
|
||||
* @return a {@link Chat} containing the created page navigator.
|
||||
*/
|
||||
public static Chat createPagination(String prefix, String cmdFormat, int currentPage, int nbPages, int nbPagesToDisplay) {
|
||||
Set<Integer> pagesToDisplay = new TreeSet<>();
|
||||
@@ -127,7 +130,7 @@ public class ChatUtil {
|
||||
pagesToDisplay.add(i);
|
||||
}
|
||||
|
||||
Chat d = ChatStatic.chat().thenLegacyText(prefix);
|
||||
Chat d = chat().thenLegacyText(prefix);
|
||||
boolean first = true;
|
||||
int previous = 0;
|
||||
|
||||
@@ -149,11 +152,11 @@ public class ChatUtil {
|
||||
else
|
||||
first = false;
|
||||
|
||||
FormatableChat pDisp = Chat.clickableCommand(Chat.text(page), String.format(cmdFormat, page), Chat.text("Aller à la page " + page));
|
||||
FormatableChat pDisplay = Chat.clickableCommand(Chat.text(page), String.format(cmdFormat, page), Chat.text("Aller à la page " + page));
|
||||
if (page == currentPage) {
|
||||
pDisp.highlightedCommandColor();
|
||||
pDisplay.highlightedCommandColor();
|
||||
}
|
||||
d.then(pDisp);
|
||||
d.then(pDisplay);
|
||||
|
||||
previous = page;
|
||||
}
|
||||
@@ -167,6 +170,58 @@ public class ChatUtil {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Do like {@link String#join(CharSequence, Iterable)}, but for components, and the last separator is different from
|
||||
* the others. It is useful when enumerating things in a sentence, for instance :
|
||||
* <code>"a thing<u>, </u>a thing<u> and </u>a thing"</code>
|
||||
* (the coma being the usual separator, and {@code " and "} being the final separator).
|
||||
* @param regularSeparator the separator used everywhere except between the two last components to join.
|
||||
* @param finalSeparator the separator used between the two last components to join.
|
||||
* @param elements the components to join.
|
||||
* @return a new {@link Chat} instance with all the provided {@code component} joined using the separators.
|
||||
*/
|
||||
public static FormatableChat joinGrammatically(ComponentLike regularSeparator, ComponentLike finalSeparator, List<? extends ComponentLike> elements) {
|
||||
int size = elements == null ? 0 : elements.size();
|
||||
int last = size - 1;
|
||||
return switch (size) {
|
||||
case 0, 1, 2 -> join(finalSeparator, elements);
|
||||
default -> (FormatableChat) join(regularSeparator, elements.subList(0, last))
|
||||
.then(finalSeparator)
|
||||
.then(elements.get(last));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Do like {@link String#join(CharSequence, Iterable)}, but for components.
|
||||
* @param separator the separator used everywhere except between the two last components to join.
|
||||
* @param elements the components to join.
|
||||
* @return a new {@link Chat} instance with all the provided {@code component} joined using the separators.
|
||||
*/
|
||||
public static FormatableChat join(ComponentLike separator, Iterable<? extends ComponentLike> elements) {
|
||||
FormatableChat c = chat();
|
||||
if (elements == null)
|
||||
return c;
|
||||
boolean first = true;
|
||||
for (ComponentLike el : elements) {
|
||||
if (!first) {
|
||||
c.then(separator);
|
||||
}
|
||||
c.then(el);
|
||||
first = false;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -210,8 +265,8 @@ public class ChatUtil {
|
||||
count += strWidth(((TextComponent)component).content(), console, actuallyBold);
|
||||
}
|
||||
else if (component instanceof TranslatableComponent) {
|
||||
for (Component c : ((TranslatableComponent)component).args())
|
||||
count += componentWidth(c, console, actuallyBold);
|
||||
for (TranslationArgument c : ((TranslatableComponent)component).arguments())
|
||||
count += componentWidth(c.asComponent(), console, actuallyBold);
|
||||
}
|
||||
|
||||
for (Component c : component.children())
|
||||
@@ -258,7 +313,7 @@ public class ChatUtil {
|
||||
|
||||
|
||||
/**
|
||||
* Wraps the provided text in multiple lines, taking into account the legacy formating.
|
||||
* Wraps the provided text in multiple lines, taking into account the legacy formatting.
|
||||
* <p>
|
||||
* This method only takes into account IG text width. Use a regular text-wrapper for console instead.
|
||||
* @param legacyText the text to wrap.
|
||||
@@ -272,7 +327,7 @@ public class ChatUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the provided text in multiple lines, taking into account the legacy formating.
|
||||
* Wraps the provided text in multiple lines, taking into account the legacy formatting.
|
||||
* <p>
|
||||
* This method only takes into account IG text width. Use a regular text-wrapper for console instead.
|
||||
* @param legacyText the text to wrap.
|
||||
@@ -295,7 +350,7 @@ public class ChatUtil {
|
||||
|
||||
do {
|
||||
char c = legacyText.charAt(index);
|
||||
if (c == ChatColor.COLOR_CHAR && index < legacyText.length() - 1) {
|
||||
if (c == LegacyComponentSerializer.SECTION_CHAR && index < legacyText.length() - 1) {
|
||||
currentWord.append(c);
|
||||
c = legacyText.charAt(++index);
|
||||
currentWord.append(c);
|
||||
@@ -369,7 +424,7 @@ public class ChatUtil {
|
||||
|
||||
/**
|
||||
* Try to render a matrix of {@link Chat} components into a table in the chat or console.
|
||||
* @param data the component, in the form of {@link List} of {@link List} of {@link Chat}. The englobing list holds
|
||||
* @param data the component, in the form of {@link List} of {@link List} of {@link Chat}. The parent list holds
|
||||
* the table lines (line 0 being the top line). Each sublist holds the cells content (element 0 is the
|
||||
* leftText one). The row lengths can be different.
|
||||
* @param space a spacer to put between columns.
|
||||
@@ -377,12 +432,12 @@ public class ChatUtil {
|
||||
* alignment, much harder).
|
||||
* @return a List containing each rendered line of the table.
|
||||
*/
|
||||
public static List<Component> renderTable(List<List<Chat>> data, String space, boolean console) {
|
||||
public static List<Component> renderTable(List<List<ComponentLike>> data, String space, boolean console) {
|
||||
List<List<Component>> compRows = new ArrayList<>(data.size());
|
||||
for (List<Chat> row : data) {
|
||||
for (List<ComponentLike> row : data) {
|
||||
List<Component> compRow = new ArrayList<>(row.size());
|
||||
for (Chat c : row) {
|
||||
compRow.add(c.getAdv());
|
||||
for (ComponentLike c : row) {
|
||||
compRow.add(c.asComponent());
|
||||
}
|
||||
compRows.add(compRow);
|
||||
}
|
||||
@@ -392,7 +447,7 @@ public class ChatUtil {
|
||||
|
||||
/**
|
||||
* Try to render a matrix of {@link Component} components into a table in the chat or console.
|
||||
* @param data the component, in the form of {@link List} of {@link List} of {@link Component}. The englobing list holds
|
||||
* @param data the component, in the form of {@link List} of {@link List} of {@link Component}. The parent list holds
|
||||
* the table lines (line 0 being the top line). Each sublist holds the cells content (element 0 is the
|
||||
* leftText one). The row lengths can be different.
|
||||
* @param space a spacer to put between columns.
|
||||
@@ -416,7 +471,7 @@ public class ChatUtil {
|
||||
// create the lines with appropriate spacing
|
||||
List<Component> spacedRows = new ArrayList<>(data.size());
|
||||
for (List<Component> row : data) {
|
||||
Chat spacedRow = Chat.chat();
|
||||
Chat spacedRow = chat();
|
||||
for (int i = 0; i < row.size() - 1; i++) {
|
||||
int w = componentWidth(row.get(i), console);
|
||||
int padding = nbPixelPerColumn.get(i) - w;
|
||||
@@ -425,8 +480,8 @@ public class ChatUtil {
|
||||
spacedRow.thenText(space);
|
||||
}
|
||||
if (!row.isEmpty())
|
||||
spacedRow.then(row.get(row.size() - 1));
|
||||
spacedRows.add(spacedRow.getAdv());
|
||||
spacedRow.then(row.getLast());
|
||||
spacedRows.add(spacedRow.get());
|
||||
}
|
||||
|
||||
return spacedRows;
|
||||
@@ -448,14 +503,14 @@ public class ChatUtil {
|
||||
*/
|
||||
public static Component customWidthSpace(int width, boolean console) {
|
||||
if (console)
|
||||
return Chat.text(" ".repeat(width)).getAdv();
|
||||
return Chat.text(" ".repeat(width)).get();
|
||||
return switch (width) {
|
||||
case 0, 1 -> Component.empty();
|
||||
case 2 -> Chat.text(".").black().getAdv();
|
||||
case 3 -> Chat.text("`").black().getAdv();
|
||||
case 6 -> Chat.text(". ").black().getAdv();
|
||||
case 7 -> Chat.text("` ").black().getAdv();
|
||||
case 11 -> Chat.text("` ").black().getAdv();
|
||||
case 2 -> Chat.text(".").black().get();
|
||||
case 3 -> Chat.text("`").black().get();
|
||||
case 6 -> Chat.text(". ").black().get();
|
||||
case 7 -> Chat.text("` ").black().get();
|
||||
case 11 -> Chat.text("` ").black().get();
|
||||
default -> {
|
||||
int nbSpace = width / 4;
|
||||
int nbBold = width % 4;
|
||||
@@ -464,13 +519,13 @@ public class ChatUtil {
|
||||
if (nbBold > 0) {
|
||||
yield Chat.text(" ".repeat(nbNotBold)).bold(false)
|
||||
.then(Chat.text(" ".repeat(nbBold)).bold(true))
|
||||
.getAdv();
|
||||
.get();
|
||||
}
|
||||
else
|
||||
yield Chat.text(" ".repeat(nbNotBold)).bold(false).getAdv();
|
||||
yield Chat.text(" ".repeat(nbNotBold)).bold(false).get();
|
||||
}
|
||||
else if (nbBold > 0) {
|
||||
yield Chat.text(" ".repeat(nbBold)).bold(true).getAdv();
|
||||
yield Chat.text(" ".repeat(nbBold)).bold(true).get();
|
||||
}
|
||||
throw new IllegalStateException("Should not be here (width=" + width + "; nbSpace=" + nbSpace + "; nbBold=" + nbBold + "; nbNotBold=" + nbNotBold + ")");
|
||||
}
|
||||
@@ -505,9 +560,9 @@ public class ChatUtil {
|
||||
private static final char PROGRESS_BAR_FULL_CHAR = '|';
|
||||
|
||||
/**
|
||||
* Generate a (eventually multi-part) progress bar using text.
|
||||
* Generate a (eventually multipart) progress bar using text.
|
||||
* @param values the values to render in the progress bar.
|
||||
* @param colors the colors attributed to each values.
|
||||
* @param colors the colors attributed to each value.
|
||||
* @param total the total value of the progress bar.
|
||||
* @param width the width in which the progress bar should fit (in pixel for IG, in character count for console)
|
||||
* @param console true if the progress bar is intended to be displayed on the console, false if it’s in game chat.
|
||||
@@ -602,5 +657,6 @@ public class ChatUtil {
|
||||
return str;
|
||||
}
|
||||
|
||||
private ChatUtil() {}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,230 @@
|
||||
package fr.pandacube.lib.chat;
|
||||
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import net.kyori.adventure.text.format.TextFormat;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyFormat;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Convenient enum to uses legacy format while keeping compatibility with modern chat format and API (Adventure, ...)
|
||||
*/
|
||||
public enum LegacyChatFormat {
|
||||
|
||||
/**
|
||||
* Black (0) color format code.
|
||||
*/
|
||||
BLACK('0'),
|
||||
/**
|
||||
* Dark blue (1) color format code.
|
||||
*/
|
||||
DARK_BLUE('1'),
|
||||
/**
|
||||
* Dark green (2) color format code.
|
||||
*/
|
||||
DARK_GREEN('2'),
|
||||
/**
|
||||
* Dark aqua (3) color format code.
|
||||
*/
|
||||
DARK_AQUA('3'),
|
||||
/**
|
||||
* Dark red (4) color format code.
|
||||
*/
|
||||
DARK_RED('4'),
|
||||
/**
|
||||
* Dark purple (5) color format code.
|
||||
*/
|
||||
DARK_PURPLE('5'),
|
||||
/**
|
||||
* Gold (6) color format code.
|
||||
*/
|
||||
GOLD('6'),
|
||||
/**
|
||||
* Gray (7) color format code.
|
||||
*/
|
||||
GRAY('7'),
|
||||
/**
|
||||
* Dark gray (8) color format code.
|
||||
*/
|
||||
DARK_GRAY('8'),
|
||||
/**
|
||||
* Blue (9) color format code.
|
||||
*/
|
||||
BLUE('9'),
|
||||
/**
|
||||
* Green (A) color format code.
|
||||
*/
|
||||
GREEN('a'),
|
||||
/**
|
||||
* Aqua (B) color format code.
|
||||
*/
|
||||
AQUA('b'),
|
||||
/**
|
||||
* Red (C) color format code.
|
||||
*/
|
||||
RED('c'),
|
||||
/**
|
||||
* Light purple (D) color format code.
|
||||
*/
|
||||
LIGHT_PURPLE('d'),
|
||||
/**
|
||||
* Yellow (E) color format code.
|
||||
*/
|
||||
YELLOW('e'),
|
||||
/**
|
||||
* White (F) color format code.
|
||||
*/
|
||||
WHITE('f'),
|
||||
/**
|
||||
* Obfuscated (K) decoration format code.
|
||||
*/
|
||||
OBFUSCATED('k'),
|
||||
/**
|
||||
* Bold (L) decoration format code.
|
||||
*/
|
||||
BOLD('l'),
|
||||
/**
|
||||
* Strikethrough (M) decoration format code.
|
||||
*/
|
||||
STRIKETHROUGH('m'),
|
||||
/**
|
||||
* Underlined (N) decoration format code.
|
||||
*/
|
||||
UNDERLINED('n'),
|
||||
/**
|
||||
* Italic (O) decoration format code.
|
||||
*/
|
||||
ITALIC('o'),
|
||||
/**
|
||||
* Reset (R) format code.
|
||||
*/
|
||||
RESET('r');
|
||||
|
||||
|
||||
/**
|
||||
* The character used by Minecraft for legacy chat format.
|
||||
*/
|
||||
public static final char COLOR_CHAR = LegacyComponentSerializer.SECTION_CHAR;
|
||||
|
||||
/** {@link #COLOR_CHAR} but as a String! */
|
||||
public static final String COLOR_STR_PREFIX = Character.toString(COLOR_CHAR);
|
||||
|
||||
private static final Map<Character, LegacyChatFormat> BY_CHAR;
|
||||
private static final Map<TextFormat, LegacyChatFormat> BY_FORMAT;
|
||||
private static final Map<LegacyFormat, LegacyChatFormat> BY_LEGACY;
|
||||
|
||||
|
||||
/**
|
||||
* Gets the {@link LegacyChatFormat} from the provided chat color code.
|
||||
* @param code the character code from [0-9A-Fa-fK-Ok-oRr].
|
||||
* @return the {@link LegacyChatFormat} related to the provided code.
|
||||
*/
|
||||
public static LegacyChatFormat of(char code) {
|
||||
return BY_CHAR.get(Character.toLowerCase(code));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link LegacyChatFormat} from the provided {@link TextFormat} instance.
|
||||
* @param format the {@link TextFormat} instance.
|
||||
* @return the {@link LegacyChatFormat} related to the provided format.
|
||||
*/
|
||||
public static LegacyChatFormat of(TextFormat format) {
|
||||
LegacyChatFormat colorOrDecoration = BY_FORMAT.get(format);
|
||||
if (colorOrDecoration != null)
|
||||
return colorOrDecoration;
|
||||
if (format.getClass().getSimpleName().equals("Reset")) // an internal class of legacy serializer library
|
||||
return RESET;
|
||||
throw new IllegalArgumentException("Unsupported format of type " + format.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link LegacyChatFormat} from the provided {@link LegacyFormat} instance.
|
||||
* @param advLegacy the {@link LegacyFormat} instance.
|
||||
* @return the {@link LegacyChatFormat} related to the provided format.
|
||||
*/
|
||||
public static LegacyChatFormat of(LegacyFormat advLegacy) {
|
||||
return BY_LEGACY.get(advLegacy);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The format code of this chat format.
|
||||
*/
|
||||
public final char code;
|
||||
|
||||
/**
|
||||
* The Adventure legacy format instance related to this chat format.
|
||||
*/
|
||||
public final LegacyFormat advLegacyFormat;
|
||||
|
||||
LegacyChatFormat(char code) {
|
||||
this.code = code;
|
||||
advLegacyFormat = LegacyComponentSerializer.parseChar(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the related {@link TextColor}, or null if it's not a color.
|
||||
* @return the related {@link TextColor}, or null if it's not a color.
|
||||
*/
|
||||
public TextColor getTextColor() {
|
||||
return advLegacyFormat.color();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if this format is a color.
|
||||
* @return true if this format is a color, false otherwise.
|
||||
*/
|
||||
public boolean isColor() {
|
||||
return getTextColor() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the related {@link TextDecoration}, or null if it's not a decoration.
|
||||
* @return the related {@link TextDecoration}, or null if it's not a decoration.
|
||||
*/
|
||||
public TextDecoration getTextDecoration() {
|
||||
return advLegacyFormat.decoration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if this format is a decoration (bold, italic, ...).
|
||||
* @return true if this format is a decoration, false otherwise.
|
||||
*/
|
||||
public boolean isDecoration() {
|
||||
return getTextDecoration() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if this format is the reset.
|
||||
* @return true if this format is the reset, false otherwise.
|
||||
*/
|
||||
public boolean isReset() {
|
||||
return this == RESET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return COLOR_STR_PREFIX + code;
|
||||
}
|
||||
|
||||
|
||||
|
||||
static {
|
||||
BY_CHAR = Arrays.stream(values()).sequential()
|
||||
.collect(Collectors.toMap(e -> e.code, e -> e, (e1, e2) -> e1, LinkedHashMap::new));
|
||||
BY_FORMAT = Arrays.stream(values()).sequential()
|
||||
.filter(e -> e.isColor() || e.isDecoration())
|
||||
.collect(Collectors.toMap(e -> {
|
||||
if (e.isColor())
|
||||
return e.getTextColor();
|
||||
return e.getTextDecoration();
|
||||
}, e -> e, (e1, e2) -> e1, LinkedHashMap::new));
|
||||
BY_LEGACY = Arrays.stream(values()).sequential()
|
||||
.collect(Collectors.toMap(e -> e.advLegacyFormat, e -> e, (e1, e2) -> e1, LinkedHashMap::new));
|
||||
}
|
||||
}
|
@@ -15,42 +15,36 @@
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>minecraft-libraries</id>
|
||||
<name>Minecraft Libraries</name>
|
||||
<url>https://libraries.minecraft.net</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>bungeecord-repo</id>
|
||||
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>fr.pandacube.lib</groupId>
|
||||
<artifactId>pandalib-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>fr.pandacube.lib</groupId>
|
||||
<artifactId>pandalib-reflect</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>fr.pandacube.lib</groupId>
|
||||
<artifactId>pandalib-commands</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>net.md-5</groupId>
|
||||
<artifactId>bungeecord-log</artifactId>
|
||||
<version>${bungeecord.version}</version>
|
||||
<groupId>fr.pandacube.lib</groupId>
|
||||
<artifactId>pandalib-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>fr.pandacube.lib</groupId>
|
||||
<artifactId>pandalib-reflect</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>fr.pandacube.lib</groupId>
|
||||
<artifactId>pandalib-commands</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>fr.pandacube.lib</groupId>
|
||||
<artifactId>pandalib-config</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.md-5</groupId>
|
||||
<artifactId>bungeecord-config</artifactId>
|
||||
<artifactId>bungeecord-log</artifactId>
|
||||
<version>${bungeecord.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
@@ -8,10 +8,10 @@ import fr.pandacube.lib.cli.log.CLILogger;
|
||||
import jline.console.ConsoleReader;
|
||||
import org.fusesource.jansi.AnsiConsole;
|
||||
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
|
||||
/**
|
||||
* Class to hangle general standard IO operation for a CLI application. It uses Jline’s {@link ConsoleReader} for the
|
||||
* Class to handle general standard IO operation for a CLI application. It uses Jline’s {@link ConsoleReader} for the
|
||||
* console rendering, a JUL {@link Logger} for logging, and Brigadier to handle commands.
|
||||
*/
|
||||
public class CLI extends Thread {
|
||||
@@ -30,10 +30,10 @@ public class CLI extends Thread {
|
||||
|
||||
AnsiConsole.systemInstall();
|
||||
reader = new ConsoleReader();
|
||||
reader.setPrompt("\r>");
|
||||
reader.setPrompt(">");
|
||||
reader.addCompleter(CLIBrigadierDispatcher.instance);
|
||||
|
||||
// configuration du formatteur pour le logger
|
||||
// configure logger's formatter
|
||||
System.setProperty("net.md_5.bungee.log-date-format", "yyyy-MM-dd HH:mm:ss");
|
||||
logger = CLILogger.getLogger(this);
|
||||
}
|
||||
|
@@ -0,0 +1,137 @@
|
||||
package fr.pandacube.lib.cli;
|
||||
|
||||
import fr.pandacube.lib.cli.commands.CommandAdmin;
|
||||
import fr.pandacube.lib.cli.commands.CommandStop;
|
||||
import fr.pandacube.lib.cli.log.CLILogger;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Main class of a CLI application.
|
||||
*/
|
||||
public abstract class CLIApplication {
|
||||
|
||||
private static CLIApplication instance;
|
||||
|
||||
/**
|
||||
* Returns the current application instance.
|
||||
* @return the current application instance.
|
||||
*/
|
||||
public static CLIApplication getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The instance of {@link CLI} for this application.
|
||||
*/
|
||||
public final CLI cli;
|
||||
|
||||
/**
|
||||
* Creates a new application instance.
|
||||
*/
|
||||
protected CLIApplication() {
|
||||
instance = this;
|
||||
CLI tmpCLI = null;
|
||||
try {
|
||||
tmpCLI = new CLI();
|
||||
Log.setLogger(tmpCLI.getLogger());
|
||||
} catch (Throwable t) {
|
||||
System.err.println("Unable to start application " + getName() + " version " + getClass().getPackage().getImplementationVersion());
|
||||
t.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
cli = tmpCLI;
|
||||
|
||||
try {
|
||||
Log.info("Starting " + getName() + " version " + getClass().getPackage().getImplementationVersion());
|
||||
|
||||
start();
|
||||
|
||||
new CommandAdmin();
|
||||
new CommandStop();
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(shutdownThread);
|
||||
|
||||
cli.start(); // actually starts the CLI thread
|
||||
|
||||
Log.info("Application started.");
|
||||
} catch (Throwable t) {
|
||||
Log.severe("Unable to start application " + getName() + " version " + getClass().getPackage().getImplementationVersion(), t);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the application's {@link Logger}.
|
||||
* @return the application's {@link Logger}.
|
||||
*/
|
||||
public Logger getLogger() {
|
||||
return cli.getLogger();
|
||||
}
|
||||
|
||||
|
||||
private final Thread shutdownThread = new Thread(this::stop);
|
||||
|
||||
private final AtomicBoolean stopping = new AtomicBoolean(false);
|
||||
|
||||
/**
|
||||
* Stops this application.
|
||||
*/
|
||||
public final void stop() {
|
||||
synchronized (stopping) {
|
||||
if (stopping.get())
|
||||
return;
|
||||
stopping.set(true);
|
||||
}
|
||||
Log.info("Stopping " + getName() + " version " + getClass().getPackage().getImplementationVersion());
|
||||
try {
|
||||
end();
|
||||
} catch (Throwable t) {
|
||||
Log.severe("Error stopping application " + getName() + " version " + getClass().getPackage().getImplementationVersion(), t);
|
||||
} finally {
|
||||
Log.info("Bye bye.");
|
||||
|
||||
CLILogger.actuallyResetLogManager();
|
||||
if (!Thread.currentThread().equals(shutdownThread))
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if this application is currently stopping, that is the {@link #stop()} method has been called.
|
||||
* @return true if the application is stopping, false otherwise.
|
||||
*/
|
||||
public boolean isStopping() {
|
||||
return stopping.get();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the name of this application.
|
||||
* @return the name of this application.
|
||||
*/
|
||||
public abstract String getName();
|
||||
|
||||
/**
|
||||
* Method to override to initialize stuff in this application.
|
||||
* This method is called on instanciation of this Application.
|
||||
* @throws Exception If an exception is thrown, the application will not start.
|
||||
*/
|
||||
protected abstract void start() throws Exception;
|
||||
|
||||
/**
|
||||
* Method to override to reload specific stuff in this application.
|
||||
* This method is called by using the command {@code admin reload}.
|
||||
*/
|
||||
public abstract void reload();
|
||||
|
||||
/**
|
||||
* Method to override to execute stuff when this application stops.
|
||||
* This method is called once before this application terminates, possibly from a shutdown hook Thread.
|
||||
*/
|
||||
protected abstract void end();
|
||||
|
||||
|
||||
}
|
@@ -12,13 +12,13 @@ import java.util.function.Predicate;
|
||||
/**
|
||||
* Abstract class that holds the logic of a specific command to be registered in {@link CLIBrigadierDispatcher}.
|
||||
*/
|
||||
public abstract class CLIBrigadierCommand extends BrigadierCommand<Object> {
|
||||
public abstract class CLIBrigadierCommand extends BrigadierCommand<CLICommandSender> {
|
||||
|
||||
/**
|
||||
* Instanciate this command instance.
|
||||
* Instantiate this command instance.
|
||||
*/
|
||||
public CLIBrigadierCommand() {
|
||||
LiteralCommandNode<Object> commandNode = buildCommand().build();
|
||||
LiteralCommandNode<CLICommandSender> commandNode = buildCommand().build();
|
||||
postBuildCommand(commandNode);
|
||||
String[] aliases = getAliases();
|
||||
if (aliases == null)
|
||||
@@ -37,7 +37,7 @@ public abstract class CLIBrigadierCommand extends BrigadierCommand<Object> {
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract LiteralArgumentBuilder<Object> buildCommand();
|
||||
protected abstract LiteralArgumentBuilder<CLICommandSender> buildCommand();
|
||||
|
||||
protected String[] getAliases() {
|
||||
return new String[0];
|
||||
@@ -47,16 +47,16 @@ public abstract class CLIBrigadierCommand extends BrigadierCommand<Object> {
|
||||
|
||||
|
||||
|
||||
public boolean isPlayer(Object sender) {
|
||||
return false;
|
||||
public boolean isPlayer(CLICommandSender sender) {
|
||||
return sender.isPlayer();
|
||||
}
|
||||
|
||||
public boolean isConsole(Object sender) {
|
||||
return true;
|
||||
public boolean isConsole(CLICommandSender sender) {
|
||||
return sender.isConsole();
|
||||
}
|
||||
|
||||
public Predicate<Object> hasPermission(String permission) {
|
||||
return sender -> true;
|
||||
public Predicate<CLICommandSender> hasPermission(String permission) {
|
||||
return sender -> sender.hasPermission(permission);
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ public abstract class CLIBrigadierCommand extends BrigadierCommand<Object> {
|
||||
* @param suggestions the suggestions to wrap.
|
||||
* @return a {@link SuggestionProvider} generating the suggestions from the provided {@link SuggestionsSupplier}.
|
||||
*/
|
||||
protected SuggestionProvider<Object> wrapSuggestions(SuggestionsSupplier<Object> suggestions) {
|
||||
protected SuggestionProvider<CLICommandSender> wrapSuggestions(SuggestionsSupplier<CLICommandSender> suggestions) {
|
||||
return wrapSuggestions(suggestions, Function.identity());
|
||||
}
|
||||
|
||||
|
@@ -1,20 +1,17 @@
|
||||
package fr.pandacube.lib.cli.commands;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.mojang.brigadier.suggestion.Suggestion;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
|
||||
import fr.pandacube.lib.chat.Chat;
|
||||
import fr.pandacube.lib.commands.BrigadierDispatcher;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import jline.console.completer.Completer;
|
||||
import net.kyori.adventure.text.ComponentLike;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Implementation of {@link BrigadierDispatcher} that integrates the commands into the JLine CLI interface.
|
||||
*/
|
||||
public class CLIBrigadierDispatcher extends BrigadierDispatcher<Object> implements Completer {
|
||||
public class CLIBrigadierDispatcher extends BrigadierDispatcher<CLICommandSender> implements Completer {
|
||||
|
||||
/**
|
||||
* The instance of {@link CLIBrigadierDispatcher}.
|
||||
@@ -22,16 +19,22 @@ public class CLIBrigadierDispatcher extends BrigadierDispatcher<Object> implemen
|
||||
public static final CLIBrigadierDispatcher instance = new CLIBrigadierDispatcher();
|
||||
|
||||
|
||||
private static final Object sender = new Object();
|
||||
/**
|
||||
* The sender for the console itself.
|
||||
*/
|
||||
public static final CLICommandSender CLI_CONSOLE_COMMAND_SENDER = new CLIConsoleCommandSender();
|
||||
|
||||
|
||||
private CLIBrigadierDispatcher() {}
|
||||
|
||||
|
||||
/**
|
||||
* Executes the provided command.
|
||||
* @param commandWithoutSlash the command, without the eventual slash at the begining.
|
||||
* Executes the provided command as the console.
|
||||
* @param commandWithoutSlash the command, without the eventual slash at the beginning.
|
||||
* @return the value returned by the executed command.
|
||||
*/
|
||||
public int execute(String commandWithoutSlash) {
|
||||
return execute(sender, commandWithoutSlash);
|
||||
return execute(CLI_CONSOLE_COMMAND_SENDER, commandWithoutSlash);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,17 +53,17 @@ public class CLIBrigadierDispatcher extends BrigadierDispatcher<Object> implemen
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the suggestions for the currently being typed command.
|
||||
* Gets the suggestions for the currently being typed command, as the console.
|
||||
* @param buffer the command that is being typed.
|
||||
* @return the suggestions for the currently being typed command.
|
||||
*/
|
||||
public Suggestions getSuggestions(String buffer) {
|
||||
return getSuggestions(sender, buffer);
|
||||
return getSuggestions(CLI_CONSOLE_COMMAND_SENDER, buffer);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void sendSenderMessage(Object sender, ComponentLike message) {
|
||||
Log.info(Chat.chatComponent(message).getLegacyText());
|
||||
protected void sendSenderMessage(CLICommandSender sender, ComponentLike message) {
|
||||
sender.sendMessage(message);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,46 @@
|
||||
package fr.pandacube.lib.cli.commands;
|
||||
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.audience.MessageType;
|
||||
import net.kyori.adventure.identity.Identity;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A command sender.
|
||||
*/
|
||||
public interface CLICommandSender extends Audience {
|
||||
/**
|
||||
* Gets the name of the sender.
|
||||
* @return The name of the sender.
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* Tells if the sender is a player.
|
||||
* @return true if the sender is a player, false otherwise.
|
||||
*/
|
||||
boolean isPlayer();
|
||||
|
||||
/**
|
||||
* Tells if the sender is on the console.
|
||||
* @return true if the sender is on the console, false otherwise.
|
||||
*/
|
||||
boolean isConsole();
|
||||
|
||||
/**
|
||||
* Tells if the sender has the specified permission.
|
||||
* @param permission the permission to test on the sender.
|
||||
* @return true if the sender has the specified permission.
|
||||
*/
|
||||
boolean hasPermission(String permission);
|
||||
|
||||
/**
|
||||
* Sends the provided message to the sender.
|
||||
* @param message the message to send.
|
||||
*/
|
||||
void sendMessage(String message);
|
||||
|
||||
@Override // force implementation of super-interface default method
|
||||
void sendMessage(@NotNull Identity source, @NotNull Component message, @NotNull MessageType type);
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
package fr.pandacube.lib.cli.commands;
|
||||
|
||||
import fr.pandacube.lib.chat.Chat;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
import net.kyori.adventure.audience.MessageType;
|
||||
import net.kyori.adventure.identity.Identity;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* The console command sender.
|
||||
*/
|
||||
public class CLIConsoleCommandSender implements CLICommandSender {
|
||||
|
||||
/**
|
||||
* Creates a new console command sender.
|
||||
*/
|
||||
protected CLIConsoleCommandSender() {}
|
||||
|
||||
public String getName() {
|
||||
return "Console";
|
||||
}
|
||||
|
||||
public boolean isPlayer() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isConsole() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean hasPermission(String permission) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void sendMessage(String message) {
|
||||
Log.info(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(@NotNull Identity source, @NotNull Component message, @NotNull MessageType type) {
|
||||
sendMessage(Chat.chatComponent(message).getLegacyText());
|
||||
}
|
||||
}
|
@@ -0,0 +1,281 @@
|
||||
package fr.pandacube.lib.cli.commands;
|
||||
|
||||
import com.mojang.brigadier.arguments.ArgumentType;
|
||||
import com.mojang.brigadier.arguments.BoolArgumentType;
|
||||
import com.mojang.brigadier.arguments.DoubleArgumentType;
|
||||
import com.mojang.brigadier.arguments.FloatArgumentType;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.arguments.LongArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.tree.ArgumentCommandNode;
|
||||
import com.mojang.brigadier.tree.CommandNode;
|
||||
import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||
import com.mojang.brigadier.tree.RootCommandNode;
|
||||
import fr.pandacube.lib.chat.Chat;
|
||||
import fr.pandacube.lib.chat.Chat.FormatableChat;
|
||||
import fr.pandacube.lib.chat.ChatTreeNode;
|
||||
import fr.pandacube.lib.cli.CLIApplication;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
import net.kyori.adventure.text.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static fr.pandacube.lib.chat.ChatStatic.chat;
|
||||
import static fr.pandacube.lib.chat.ChatStatic.failureText;
|
||||
import static fr.pandacube.lib.chat.ChatStatic.successText;
|
||||
import static fr.pandacube.lib.chat.ChatStatic.text;
|
||||
|
||||
/**
|
||||
* The {@code admin} command for a {@link CLIApplication}.
|
||||
*/
|
||||
public class CommandAdmin extends CLIBrigadierCommand {
|
||||
|
||||
/**
|
||||
* Initializes the admin command.
|
||||
*/
|
||||
public CommandAdmin() {}
|
||||
|
||||
@Override
|
||||
protected LiteralArgumentBuilder<CLICommandSender> buildCommand() {
|
||||
return literal("admin")
|
||||
.executes(this::version)
|
||||
.then(literal("version")
|
||||
.executes(this::version)
|
||||
)
|
||||
.then(literal("reload")
|
||||
.executes(this::reload)
|
||||
)
|
||||
.then(literal("debug")
|
||||
.executes(this::debug)
|
||||
)
|
||||
.then(literal("commandstruct")
|
||||
.executes(this::commandStruct)
|
||||
.then(argument("path", StringArgumentType.greedyString())
|
||||
.executes(this::commandStruct)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private int version(CommandContext<CLICommandSender> context) {
|
||||
Log.info(chat()
|
||||
.console(context.getSource().isConsole())
|
||||
.infoColor()
|
||||
.thenCenterText(text(CLIApplication.getInstance().getName()))
|
||||
.thenNewLine()
|
||||
.thenText("- Implem. version: ")
|
||||
.thenData(CLIApplication.getInstance().getClass().getPackage().getImplementationVersion())
|
||||
.thenNewLine()
|
||||
.thenText("- Spec. version: ")
|
||||
.thenData(CLIApplication.getInstance().getClass().getPackage().getSpecificationVersion())
|
||||
.getLegacyText());
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private int reload(CommandContext<CLICommandSender> context) {
|
||||
CLIApplication.getInstance().reload();
|
||||
return 1;
|
||||
}
|
||||
|
||||
private int debug(CommandContext<CLICommandSender> context) {
|
||||
Log.setDebug(!Log.isDebugEnabled());
|
||||
Log.info(successText("Mode débug "
|
||||
+ (Log.isDebugEnabled() ? "" : "dés") + "activé").getLegacyText());
|
||||
return 1;
|
||||
}
|
||||
|
||||
private int commandStruct(CommandContext<CLICommandSender> context) {
|
||||
CLICommandSender sender = context.getSource();
|
||||
String[] tokens = tryGetArgument(context, "path", String.class, s -> s.split(" "), new String[0]);
|
||||
|
||||
CommandNode<CLICommandSender> node = CLIBrigadierDispatcher.instance.getDispatcher().findNode(Arrays.asList(tokens));
|
||||
|
||||
if (node == null) {
|
||||
Log.severe(failureText("La commande spécifiée n’a pas été trouvée.").getLegacyText());
|
||||
return 0;
|
||||
}
|
||||
|
||||
Set<CommandNode<CLICommandSender>> scannedNodes = new HashSet<>();
|
||||
DisplayCommandNode displayNode = new DisplayCommandNode();
|
||||
|
||||
// find parent nodes of scanned node to avoid displaying them after redirection and stuff
|
||||
for (int i = 1; i < tokens.length; i++) {
|
||||
CommandNode<CLICommandSender> ignoredNode = CLIBrigadierDispatcher.instance.getDispatcher().findNode(Arrays.asList(Arrays.copyOf(tokens, i)));
|
||||
if (ignoredNode != null) {
|
||||
displayNode.addInline(ignoredNode);
|
||||
scannedNodes.add(ignoredNode);
|
||||
}
|
||||
}
|
||||
|
||||
buildDisplayCommandTree(displayNode, scannedNodes, node);
|
||||
|
||||
ChatTreeNode displayTreeNode = buildDisplayTree(displayNode, sender);
|
||||
for (Chat comp : displayTreeNode.render(true))
|
||||
Log.info(comp.getLegacyText());
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private void buildDisplayCommandTree(DisplayCommandNode displayNode, Set<CommandNode<CLICommandSender>> scannedNodes, CommandNode<CLICommandSender> node) {
|
||||
displayNode.addInline(node);
|
||||
|
||||
scannedNodes.add(node);
|
||||
|
||||
if (node.getRedirect() != null) {
|
||||
if (scannedNodes.contains(node.getRedirect()) || node.getRedirect() instanceof RootCommandNode) {
|
||||
displayNode.addInline(node.getRedirect());
|
||||
}
|
||||
else {
|
||||
buildDisplayCommandTree(displayNode, scannedNodes, node.getRedirect());
|
||||
}
|
||||
}
|
||||
else if (node.getChildren().size() == 1) {
|
||||
buildDisplayCommandTree(displayNode, scannedNodes, node.getChildren().iterator().next());
|
||||
}
|
||||
else if (node.getChildren().size() >= 2) {
|
||||
for (CommandNode<CLICommandSender> child : node.getChildren()) {
|
||||
DisplayCommandNode dNode = new DisplayCommandNode();
|
||||
buildDisplayCommandTree(dNode, scannedNodes, child);
|
||||
displayNode.addChild(dNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private ChatTreeNode buildDisplayTree(DisplayCommandNode displayNode, CLICommandSender sender) {
|
||||
Chat d = chat().then(displayCurrentNode(displayNode.nodes.get(0), false, sender));
|
||||
|
||||
CommandNode<CLICommandSender> prevNode = displayNode.nodes.get(0);
|
||||
for (int i = 1; i < displayNode.nodes.size(); i++) {
|
||||
CommandNode<CLICommandSender> currNode = displayNode.nodes.get(i);
|
||||
if (currNode.equals(prevNode.getRedirect())) {
|
||||
d.then(text(" → ")
|
||||
.hover("Redirects to path: " + CLIBrigadierDispatcher.instance.getDispatcher().getPath(currNode))
|
||||
);
|
||||
d.then(displayCurrentNode(currNode, true, sender));
|
||||
}
|
||||
else {
|
||||
d.thenText(" ");
|
||||
d.then(displayCurrentNode(currNode, false, sender));
|
||||
}
|
||||
prevNode = currNode;
|
||||
}
|
||||
|
||||
|
||||
ChatTreeNode displayTree = new ChatTreeNode(d);
|
||||
|
||||
for (DisplayCommandNode child : displayNode.children) {
|
||||
displayTree.addChild(buildDisplayTree(child, sender));
|
||||
}
|
||||
|
||||
return displayTree;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private Component displayCurrentNode(CommandNode<CLICommandSender> node, boolean redirectTarget, CLICommandSender sender) {
|
||||
if (node == null)
|
||||
throw new IllegalArgumentException("node must not be null");
|
||||
FormatableChat d;
|
||||
if (node instanceof RootCommandNode) {
|
||||
d = text("(root)").italic()
|
||||
.hover("Root command node");
|
||||
}
|
||||
else if (node instanceof ArgumentCommandNode) {
|
||||
ArgumentType<?> type = ((ArgumentCommandNode<?, ?>) node).getType();
|
||||
String typeStr = type.getClass().getSimpleName();
|
||||
if (type instanceof IntegerArgumentType
|
||||
|| type instanceof LongArgumentType
|
||||
|| type instanceof FloatArgumentType
|
||||
|| type instanceof DoubleArgumentType) {
|
||||
typeStr = type.toString();
|
||||
}
|
||||
else if (type instanceof BoolArgumentType) {
|
||||
typeStr = "bool()";
|
||||
}
|
||||
else if (type instanceof StringArgumentType) {
|
||||
typeStr = "string(" + ((StringArgumentType) type).getType().name().toLowerCase() + ")";
|
||||
}
|
||||
String t = "<" + node.getName() + ">";
|
||||
String h = "Argument command node"
|
||||
+ "\nType: " + typeStr;
|
||||
|
||||
if (node.getCommand() != null) {
|
||||
t += "®";
|
||||
h += "\nThis node has a command";
|
||||
}
|
||||
|
||||
d = text(t);
|
||||
|
||||
if (!node.canUse(sender)) {
|
||||
d.gray();
|
||||
h += "\nPermission not granted for you";
|
||||
}
|
||||
|
||||
d.hover(h);
|
||||
}
|
||||
else if (node instanceof LiteralCommandNode) {
|
||||
String t = node.getName();
|
||||
String h = "Literal command node";
|
||||
|
||||
if (node.getCommand() != null) {
|
||||
t += "®";
|
||||
h += "\nThis node has a command";
|
||||
}
|
||||
|
||||
d = text(t);
|
||||
|
||||
if (!node.canUse(sender)) {
|
||||
d.gray();
|
||||
h += "\nPermission not granted for you";
|
||||
}
|
||||
|
||||
d.hover(h);
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Unknown command node type: " + node.getClass());
|
||||
}
|
||||
|
||||
if (redirectTarget)
|
||||
d.gray();
|
||||
return d.get();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private static class DisplayCommandNode {
|
||||
final List<CommandNode<CLICommandSender>> nodes = new ArrayList<>();
|
||||
final List<DisplayCommandNode> children = new ArrayList<>();
|
||||
|
||||
void addInline(CommandNode<CLICommandSender> node) {
|
||||
nodes.add(node);
|
||||
}
|
||||
|
||||
void addChild(DisplayCommandNode child) {
|
||||
children.add(child);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package fr.pandacube.lib.cli.commands;
|
||||
|
||||
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import fr.pandacube.lib.cli.CLIApplication;
|
||||
|
||||
/**
|
||||
* the {@code stop} (or {@code end}) command for a {@link CLIApplication}.
|
||||
*/
|
||||
public class CommandStop extends CLIBrigadierCommand {
|
||||
|
||||
/**
|
||||
* Initializes the admin command.
|
||||
*/
|
||||
public CommandStop() {}
|
||||
|
||||
@Override
|
||||
protected LiteralArgumentBuilder<CLICommandSender> buildCommand() {
|
||||
return literal("stop")
|
||||
.executes(context -> {
|
||||
CLIApplication.getInstance().stop();
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] getAliases() {
|
||||
return new String[] { "end" };
|
||||
}
|
||||
}
|
@@ -1,27 +1,56 @@
|
||||
package fr.pandacube.lib.cli.log;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import fr.pandacube.lib.cli.CLI;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import fr.pandacube.lib.cli.CLIApplication;
|
||||
import fr.pandacube.lib.util.ThrowableUtil;
|
||||
import fr.pandacube.lib.util.log.DailyLogRotateFileHandler;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
import net.md_5.bungee.log.ColouredWriter;
|
||||
import net.md_5.bungee.log.ConciseFormatter;
|
||||
import net.md_5.bungee.log.LoggingOutputStream;
|
||||
|
||||
import java.io.PipedInputStream;
|
||||
import java.io.PipedOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.util.Scanner;
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.LogManager;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Initializer for the logging system of a CLI application.
|
||||
*/
|
||||
public class CLILogger {
|
||||
|
||||
static {
|
||||
System.setProperty("java.util.logging.manager", ShutdownHookDelayerLogManager.class.getName());
|
||||
}
|
||||
|
||||
private static Logger logger = null;
|
||||
|
||||
|
||||
private static class ShutdownHookDelayerLogManager extends LogManager {
|
||||
static ShutdownHookDelayerLogManager instance;
|
||||
public ShutdownHookDelayerLogManager() { instance = this; }
|
||||
@Override public void reset() { /* don't reset yet. */ }
|
||||
private void actuallyReset() { super.reset(); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the LogManager to actually reset.
|
||||
* <p>
|
||||
* This method is called by the shutdown hook of {@link CLIApplication}, because the {@link CLILogger} uses a custom
|
||||
* {@link LogManager} that bypass the reset process during the shutdown of the process.
|
||||
*/
|
||||
public static void actuallyResetLogManager() {
|
||||
ShutdownHookDelayerLogManager.instance.actuallyReset();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize and return the logger for this application.
|
||||
* @param cli the CLI instance to use
|
||||
* @return the logger of this application.
|
||||
* @return the logger for this application.
|
||||
*/
|
||||
public static synchronized Logger getLogger(CLI cli) {
|
||||
if (logger == null) {
|
||||
@@ -38,12 +67,37 @@ public class CLILogger {
|
||||
fileHandler.setFormatter(new ConciseFormatter(false));
|
||||
logger.addHandler(fileHandler);
|
||||
|
||||
System.setErr(new PrintStream(new LoggingOutputStream(logger, Level.SEVERE), true));
|
||||
System.setOut(new PrintStream(new LoggingOutputStream(logger, Level.INFO), true));
|
||||
System.setErr(newRedirector(logger, Level.SEVERE));
|
||||
System.setOut(newRedirector(logger, Level.INFO));
|
||||
|
||||
Log.setLogger(logger);
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, e) -> Log.severe("Uncaught Exception in thread " + t.getName(), e));
|
||||
}
|
||||
return logger;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private static PrintStream newRedirector(Logger logger, Level level) {
|
||||
PipedOutputStream pos = new PipedOutputStream();
|
||||
PrintStream ps = new PrintStream(pos);
|
||||
PipedInputStream pis = new PipedInputStream();
|
||||
ThrowableUtil.wrapEx(() -> pos.connect(pis));
|
||||
Scanner s = new Scanner(pis);
|
||||
|
||||
Thread t = new Thread(() -> {
|
||||
while(s.hasNextLine()) {
|
||||
logger.logp(level, "", "", s.nextLine());
|
||||
}
|
||||
s.close();
|
||||
}, "Logging Redirector Thread (" + level + ")");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
return ps;
|
||||
}
|
||||
|
||||
private CLILogger() {}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,45 @@
|
||||
package fr.pandacube.lib.commands;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Throw an instance of this exception to indicate to the plugin command handler that the user has missused the command.
|
||||
* The message, if provided, must indicate the reason of the mussusage of the command. It will be displayed on the
|
||||
* screen with eventual indications of how to use the command (help command for example).
|
||||
* If a {@link Throwable} cause is provided, it will be relayed to the plugin {@link Logger}.
|
||||
*
|
||||
*/
|
||||
public class BadCommandUsage extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Constructs a new runtime exception with no message or cause.
|
||||
*/
|
||||
public BadCommandUsage() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new runtime exception with the specified cause.
|
||||
* @param cause the cause.
|
||||
*/
|
||||
public BadCommandUsage(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new runtime exception with the specified message.
|
||||
* @param message the message.
|
||||
*/
|
||||
public BadCommandUsage(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new runtime exception with the specified message and cause.
|
||||
* @param message the message.
|
||||
* @param cause the cause.
|
||||
*/
|
||||
public BadCommandUsage(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@@ -10,9 +10,10 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
||||
import com.mojang.brigadier.suggestion.SuggestionProvider;
|
||||
import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@@ -24,12 +25,17 @@ import java.util.function.Predicate;
|
||||
*/
|
||||
public abstract class BrigadierCommand<S> {
|
||||
|
||||
/**
|
||||
* Creates a Brigadier command.
|
||||
*/
|
||||
public BrigadierCommand() {}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a builder for this command.
|
||||
* Concrete class should include any element in the builder that is needed to build the command (sub-commands and
|
||||
* arguments, requirements, redirection, ...).
|
||||
* If any of the sub-commands and arguments needs to know the {@link LiteralCommandNode} builded from the returned
|
||||
* If any of the sub-commands and arguments needs to know the {@link LiteralCommandNode} built from the returned
|
||||
* {@link LiteralArgumentBuilder}, this can be done by overriding {@link #postBuildCommand(LiteralCommandNode)}.
|
||||
* @return a builder for this command.
|
||||
*/
|
||||
@@ -37,16 +43,16 @@ public abstract class BrigadierCommand<S> {
|
||||
|
||||
/**
|
||||
* Method to override if the reference to the command node has to be known when building the subcommands.
|
||||
* @param commandNode the command node builded from {@link #buildCommand()}.
|
||||
* @param commandNode the command node built from {@link #buildCommand()}.
|
||||
*/
|
||||
protected void postBuildCommand(LiteralCommandNode<S> commandNode) {
|
||||
// default implementation does nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to override if this command have any aliases.
|
||||
* Method to override if this command has any aliases.
|
||||
* @return an array of string corresponding to the aliases. This must not include the orignal command name (that
|
||||
* is the name of the literal command node builded from {@link #buildCommand()}).
|
||||
* is the name of the literal command node built from {@link #buildCommand()}).
|
||||
*/
|
||||
protected String[] getAliases() {
|
||||
return new String[0];
|
||||
@@ -235,9 +241,12 @@ public abstract class BrigadierCommand<S> {
|
||||
args = Arrays.copyOf(args, args.length + 1);
|
||||
args[args.length - 1] = message.substring(tokenStartPos);
|
||||
|
||||
for (String s : suggestions.getSuggestions(sender, args.length - 1, args[args.length - 1], args)) {
|
||||
if (s != null)
|
||||
builder.suggest(s);
|
||||
List<String> wrappedResult = suggestions.getSuggestions(sender, args.length - 1, args[args.length - 1], args);
|
||||
if (wrappedResult != null) {
|
||||
for (String s : wrappedResult) {
|
||||
if (s != null)
|
||||
builder.suggest(s);
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Log.severe("Error while tab-completing '" + message + "' for " + sender, e);
|
||||
|
@@ -6,14 +6,14 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||
import fr.pandacube.lib.chat.Chat;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
import net.kyori.adventure.text.ComponentLike;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Abstract class that holds a Brigadier {@link CommandDispatcher} instance.
|
||||
* Subclasses contains logic to integrate this commands dispatcher into their environment (like Bungee or CLI app).
|
||||
* Subclasses contain logic to integrate this commands dispatcher into their environment (like Bungee or CLI app).
|
||||
* @param <S> the command source (or command sender) type.
|
||||
*/
|
||||
public abstract class BrigadierDispatcher<S> {
|
||||
@@ -21,6 +21,11 @@ public abstract class BrigadierDispatcher<S> {
|
||||
|
||||
private final CommandDispatcher<S> dispatcher = new CommandDispatcher<>();
|
||||
|
||||
/**
|
||||
* Creates a new Dispatcher instance.
|
||||
*/
|
||||
public BrigadierDispatcher() {}
|
||||
|
||||
|
||||
/**
|
||||
* Registers the provided command node into this dispatcher.
|
||||
@@ -43,7 +48,7 @@ public abstract class BrigadierDispatcher<S> {
|
||||
/**
|
||||
* Executes the provided command as the provided sender.
|
||||
* @param sender the command sender.
|
||||
* @param commandWithoutSlash the command, without the eventual slash at the begining.
|
||||
* @param commandWithoutSlash the command, without the eventual slash at the beginning.
|
||||
* @return the value returned by the executed command.
|
||||
*/
|
||||
public int execute(S sender, String commandWithoutSlash) {
|
||||
|
@@ -18,7 +18,7 @@ import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Utility methods to replace some functionalities of Brigadier, especialy suggestion sorting that we don’t like.
|
||||
* Utility methods to replace some functionalities of Brigadier, especially suggestion sorting that we don’t like.
|
||||
*/
|
||||
public class BrigadierSuggestionsUtil {
|
||||
|
||||
@@ -140,4 +140,8 @@ public class BrigadierSuggestionsUtil {
|
||||
}
|
||||
|
||||
|
||||
|
||||
private BrigadierSuggestionsUtil() {}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@ import java.util.stream.LongStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Functionnal interface providing suggestions for an argument of a command.
|
||||
* Functional interface providing suggestions for an argument of a command.
|
||||
* @param <S> the type of the command sender.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
@@ -66,7 +66,7 @@ public interface SuggestionsSupplier<S> {
|
||||
* Filter the provided {@link Stream} of string according to the provided token, using the filter returned by {@link #filter(String)},
|
||||
* then returns the strings collected into a {@link List}.
|
||||
* <p>
|
||||
* This methods consume the provided stream, so will not be usable anymore.
|
||||
* This method consume the provided stream, so will not be usable anymore.
|
||||
* @param stream the stream to filter and collet.
|
||||
* @param token the token to consider for filtering.
|
||||
* @return the stream, filtered and collected into a {@link List}.
|
||||
@@ -336,7 +336,7 @@ public interface SuggestionsSupplier<S> {
|
||||
/**
|
||||
* List of all possible duration unit symbols for suggestions.
|
||||
*/
|
||||
public static final List<String> DURATION_SUFFIXES = List.of("y", "mo", "w", "d", "h", "m", "s");
|
||||
List<String> DURATION_SUFFIXES = List.of("y", "mo", "w", "d", "h", "m", "s");
|
||||
|
||||
|
||||
private static void scanAndRemovePastSuffixes(List<String> suffixes, String foundSuffix) {
|
||||
@@ -505,7 +505,7 @@ public interface SuggestionsSupplier<S> {
|
||||
/**
|
||||
* Creates a new {@link SuggestionsSupplier} containing all the suggestions of this instance,
|
||||
* but if this list is still empty, returns the suggestions from the provided one.
|
||||
* @param other another {@link SuggestionsSupplier} to fallback to.
|
||||
* @param other another {@link SuggestionsSupplier} to fall back to.
|
||||
* @return a new {@link SuggestionsSupplier}.
|
||||
*/
|
||||
default SuggestionsSupplier<S> orIfEmpty(SuggestionsSupplier<S> other) {
|
||||
|
33
pandalib-config/pom.xml
Normal file
33
pandalib-config/pom.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>pandalib-parent</artifactId>
|
||||
<groupId>fr.pandacube.lib</groupId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>pandalib-config</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>bungeecord-repo</id>
|
||||
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>net.md-5</groupId>
|
||||
<artifactId>bungeecord-config</artifactId>
|
||||
<version>${bungeecord.version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@@ -1,23 +1,19 @@
|
||||
package fr.pandacube.lib.core.config;
|
||||
package fr.pandacube.lib.config;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import fr.pandacube.lib.chat.ChatColorUtil;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
|
||||
/**
|
||||
* Class that loads a specific config file or directory.
|
||||
*/
|
||||
public abstract class AbstractConfig {
|
||||
|
||||
/**
|
||||
* The {@link File} corresponging to this config file or directory.
|
||||
* The {@link File} corresponding to this config file or directory.
|
||||
*/
|
||||
protected final File configFile;
|
||||
|
||||
@@ -57,7 +53,7 @@ public abstract class AbstractConfig {
|
||||
while ((line = reader.readLine()) != null) {
|
||||
String trimmedLine = line.trim();
|
||||
|
||||
if (ignoreEmpty && trimmedLine.equals(""))
|
||||
if (ignoreEmpty && trimmedLine.isEmpty())
|
||||
continue;
|
||||
|
||||
if (ignoreHashtagComment && trimmedLine.startsWith("#"))
|
||||
@@ -94,7 +90,8 @@ public abstract class AbstractConfig {
|
||||
* @return the list of files in the config directory, or null if this config is not a directory.
|
||||
*/
|
||||
protected List<File> getFileList() {
|
||||
return configFile.isDirectory() ? Arrays.asList(configFile.listFiles()) : null;
|
||||
File[] arr = configFile.listFiles();
|
||||
return arr != null ? List.of(arr) : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +102,7 @@ public abstract class AbstractConfig {
|
||||
* Splits the provided string into a list of permission nodes.
|
||||
* The permission nodes must be separated by {@code ";"}.
|
||||
* @param perms one or more permissions nodes, separated by {@code ";"}.
|
||||
* @return {@code null} if the parameter is null or is equal to {@code "*"}, or the string splitted using {@code ";"}.
|
||||
* @return {@code null} if the parameter is null or is equal to {@code "*"}, or the string split using {@code ";"}.
|
||||
*/
|
||||
public static List<String> splitPermissionsString(String perms) {
|
||||
if (perms == null || perms.equals("*"))
|
||||
@@ -114,25 +111,6 @@ public abstract class AbstractConfig {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Utility method to that translate the {@code '&'} formated string to the legacy format.
|
||||
* @param string the string to convert.
|
||||
* @return a legacy formated string (using {@code '§'}).
|
||||
*/
|
||||
public static String getTranslatedColorCode(String string) {
|
||||
return ChatColorUtil.translateAlternateColorCodes('&', string);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Logs the message as a warning into console, prefixed with the context of this config.
|
||||
* @param message the message to log.
|
||||
*/
|
||||
protected void warning(String message) {
|
||||
Log.warning("Error in configuration '"+configFile.getName()+"': " + message);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The type of config.
|
||||
*/
|
@@ -1,17 +1,17 @@
|
||||
package fr.pandacube.lib.core.config;
|
||||
package fr.pandacube.lib.config;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* An abstract manager for a set of configuration files and folders.
|
||||
* Its uses is to manage the loading/reloading of the configuration of a plugin.
|
||||
* It's uses to manage the loading/reloading of the configuration of a plugin.
|
||||
*/
|
||||
public abstract class AbstractConfigManager {
|
||||
|
||||
/**
|
||||
* The global configuration directory.
|
||||
* May be the one provided by the environmenet API (like Plugin.getPluginFolder() in Bukkit).
|
||||
* It may be the one provided by the environment API (like Plugin.getPluginFolder() in Bukkit).
|
||||
*/
|
||||
protected final File configDir;
|
||||
|
@@ -37,6 +37,12 @@
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>${guava.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Cron expression interpreter -->
|
||||
<dependency>
|
||||
<groupId>ch.eitchnet</groupId>
|
||||
@@ -50,7 +56,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<version>3.5.2</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
@@ -85,6 +91,28 @@
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>com.googlecode.maven-download-plugin</groupId>
|
||||
<artifactId>download-maven-plugin</artifactId>
|
||||
<version>1.7.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>mcversion-download</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>wget</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<url>https://api.pandacube.fr/rest/mcversion</url>
|
||||
<outputDirectory>${project.basedir}/src/main/resources/fr/pandacube/lib/core/mc_version</outputDirectory>
|
||||
<outputFileName>mcversion.json</outputFileName>
|
||||
<skipCache>true</skipCache>
|
||||
<overwrite>true</overwrite>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
package fr.pandacube.lib.core.backup;
|
||||
|
||||
import fr.pandacube.lib.chat.Chat;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import fr.pandacube.lib.chat.LegacyChatFormat;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -16,10 +16,19 @@ import java.util.stream.Collectors;
|
||||
|
||||
import static fr.pandacube.lib.chat.ChatStatic.text;
|
||||
|
||||
/**
|
||||
* Cleanup a backup directory (i.e. removes old backup archives).
|
||||
* It is possible to combine different instances to affect which archive to keep or delete.
|
||||
*/
|
||||
public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTime>> {
|
||||
|
||||
private static final boolean testOnly = false; // if true, no files are deleted
|
||||
|
||||
/**
|
||||
* Creates a {@link BackupCleaner} that keeps the n last archives in the backup directory.
|
||||
* @param n the number of last archives to keep.
|
||||
* @return a {@link BackupCleaner} that keeps the n last archives in the backup directory.
|
||||
*/
|
||||
public static BackupCleaner KEEPING_N_LAST(int n) {
|
||||
return new BackupCleaner() {
|
||||
@Override
|
||||
@@ -32,15 +41,23 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a {@link BackupCleaner} that keeps one archive every n month.
|
||||
* <p>
|
||||
* This cleaner divides each year into sections of n month. For each month, its compute a section id using the
|
||||
* formula <code><i>YEAR</i> * (12 / <i>n</i>) + <i>MONTH</i> / <i>n</i></code>. It then keeps the first archive
|
||||
* found in each section.
|
||||
*
|
||||
* @param n the interval in month between each kept archives. Must be a divider of 12 (1, 2, 3, 4, 6 or 12).
|
||||
* @return a {@link BackupCleaner} that keeps one archive every n month.
|
||||
*/
|
||||
public static BackupCleaner KEEPING_1_EVERY_N_MONTH(int n) {
|
||||
return new BackupCleaner() {
|
||||
@Override
|
||||
public TreeSet<LocalDateTime> apply(TreeSet<LocalDateTime> localDateTimes) {
|
||||
return localDateTimes.stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
ldt -> {
|
||||
return ldt.getYear() * 4 + ldt.getMonthValue() / n;
|
||||
},
|
||||
ldt -> ldt.getYear() * (12 / n) + ldt.getMonthValue() / n,
|
||||
TreeMap::new,
|
||||
Collectors.minBy(LocalDateTime::compareTo))
|
||||
)
|
||||
@@ -53,9 +70,19 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a backup cleaner.
|
||||
*/
|
||||
public BackupCleaner() {}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new {@link BackupCleaner} that keeps the archives kept by this {@link BackupCleaner} or by the provided
|
||||
* one.
|
||||
* In other word, it makes a union operation with the set of archives kept by both original {@link BackupCleaner}.
|
||||
* @param other the other {@link BackupCleaner} to merge with.
|
||||
* @return a new {@link BackupCleaner}. The original ones are not affected.
|
||||
*/
|
||||
public BackupCleaner merge(BackupCleaner other) {
|
||||
BackupCleaner self = this;
|
||||
return new BackupCleaner() {
|
||||
@@ -70,19 +97,24 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Performs the cleanup operation on the provided directory.
|
||||
* @param archiveDir the backup directory to clean up.
|
||||
* @param compressDisplayName the display name of the backup process that manages the backup directory. Used for logs.
|
||||
*/
|
||||
public void cleanupArchives(File archiveDir, String compressDisplayName) {
|
||||
String[] files = archiveDir.list();
|
||||
if (files == null)
|
||||
return;
|
||||
|
||||
Log.info("[Backup] Cleaning up backup directory " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + "...");
|
||||
Log.info("[Backup] Cleaning up backup directory " + LegacyChatFormat.GRAY + compressDisplayName + LegacyChatFormat.RESET + "...");
|
||||
|
||||
TreeMap<LocalDateTime, File> datedFiles = new TreeMap<>();
|
||||
|
||||
for (String filename : files) {
|
||||
File file = new File(archiveDir, filename);
|
||||
if (!filename.matches("\\d{8}-\\d{6}\\.zip")) {
|
||||
Log.warning("[Backup] " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + " Invalid file in backup directory: " + filename);
|
||||
Log.warning("[Backup] " + LegacyChatFormat.GRAY + compressDisplayName + LegacyChatFormat.RESET + " Invalid file in backup directory: " + filename);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -91,7 +123,7 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi
|
||||
try {
|
||||
ldt = LocalDateTime.parse(dateTimeStr, BackupProcess.dateFileNameFormatter);
|
||||
} catch (DateTimeParseException e) {
|
||||
Log.warning("[Backup] " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + " Unable to parse file name to a date-time: " + filename, e);
|
||||
Log.warning("[Backup] " + LegacyChatFormat.GRAY + compressDisplayName + LegacyChatFormat.RESET + " Unable to parse file name to a date-time: " + filename, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -124,7 +156,7 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi
|
||||
if (testOnly || oneDeleted)
|
||||
Log.warning(c.getLegacyText());
|
||||
|
||||
Log.info("[Backup] Backup directory " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + " cleaned.");
|
||||
Log.info("[Backup] Backup directory " + LegacyChatFormat.GRAY + compressDisplayName + LegacyChatFormat.RESET + " cleaned.");
|
||||
}
|
||||
|
||||
|
||||
|
@@ -1,11 +1,8 @@
|
||||
package fr.pandacube.lib.core.backup;
|
||||
|
||||
import fc.cron.CronExpression;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
@@ -13,22 +10,37 @@ import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.LongStream;
|
||||
|
||||
/**
|
||||
* Handles the backup processes.
|
||||
*/
|
||||
public class BackupManager extends TimerTask {
|
||||
|
||||
private final File backupDirectory;
|
||||
|
||||
/**
|
||||
* The {@link Persist} instance of this {@link BackupManager}.
|
||||
*/
|
||||
protected final Persist persist;
|
||||
|
||||
/**
|
||||
* The list of backup processes that are scheduled.
|
||||
*/
|
||||
protected final List<BackupProcess> backupQueue = new ArrayList<>();
|
||||
|
||||
/* package */ final AtomicReference<BackupProcess> runningBackup = new AtomicReference<>();
|
||||
|
||||
private final Timer schedulerTimer = new Timer();
|
||||
|
||||
/**
|
||||
* Instantiate a new backup manager.
|
||||
* @param backupDirectory the root backup directory.
|
||||
*/
|
||||
public BackupManager(File backupDirectory) {
|
||||
this.backupDirectory = backupDirectory;
|
||||
if (!backupDirectory.exists()) {
|
||||
backupDirectory.mkdirs();
|
||||
}
|
||||
persist = new Persist(this);
|
||||
|
||||
|
||||
@@ -37,17 +49,32 @@ public class BackupManager extends TimerTask {
|
||||
schedulerTimer.scheduleAtFixedRate(this, new Date(nextMinute), 60_000);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a new backup process to the queue.
|
||||
* @param process the backup process to add.
|
||||
*/
|
||||
protected void addProcess(BackupProcess process) {
|
||||
process.displayNextSchedule();
|
||||
backupQueue.add(process);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the backup root directory.
|
||||
* @return the backup root directory.
|
||||
*/
|
||||
public File getBackupDirectory() {
|
||||
return backupDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if a backup is currently running.
|
||||
* @return true if a backup is running, false otherwise.
|
||||
*/
|
||||
public synchronized boolean isBackupRunning() {
|
||||
return runningBackup.get() != null;
|
||||
}
|
||||
|
||||
|
||||
public synchronized void run() {
|
||||
BackupProcess tmp;
|
||||
if ((tmp = runningBackup.get()) != null) {
|
||||
@@ -65,12 +92,16 @@ public class BackupManager extends TimerTask {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Disables this backup manager, canceling scheduled backups.
|
||||
* It will wait for a currently running backup to finish before returning.
|
||||
*/
|
||||
@SuppressWarnings("BusyWait")
|
||||
public synchronized void onDisable() {
|
||||
|
||||
schedulerTimer.cancel();
|
||||
|
||||
if (runningBackup.get() != null) {
|
||||
if (isBackupRunning()) {
|
||||
Log.warning("[Backup] Waiting after the end of a backup...");
|
||||
BackupProcess tmp;
|
||||
while ((tmp = runningBackup.get()) != null) {
|
||||
@@ -88,8 +119,6 @@ public class BackupManager extends TimerTask {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
persist.save();
|
||||
}
|
||||
|
||||
|
||||
|
@@ -1,48 +1,75 @@
|
||||
package fr.pandacube.lib.core.backup;
|
||||
|
||||
import fc.cron.CronExpression;
|
||||
import fr.pandacube.lib.chat.LegacyChatFormat;
|
||||
import fr.pandacube.lib.core.cron.CronScheduler;
|
||||
import fr.pandacube.lib.util.FileUtils;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.DateFormat;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeFormatterBuilder;
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.function.BiPredicate;
|
||||
|
||||
/**
|
||||
* A backup process.
|
||||
*/
|
||||
public abstract class BackupProcess implements Comparable<BackupProcess>, Runnable {
|
||||
private final BackupManager backupManager;
|
||||
|
||||
/**
|
||||
* The process identifier.
|
||||
*/
|
||||
public final String identifier;
|
||||
|
||||
|
||||
/**
|
||||
* The zip compressor.
|
||||
*/
|
||||
protected ZipCompressor compressor = null;
|
||||
|
||||
|
||||
private boolean enabled = true;
|
||||
private String scheduling = "0 2 * * *"; // cron format, here is everyday at 2am
|
||||
private String scheduling = "0 2 * * *"; // cron format, here is every day at 2am
|
||||
private BackupCleaner backupCleaner = null;
|
||||
private List<String> ignoreList = new ArrayList<>();
|
||||
|
||||
|
||||
/**
|
||||
* Instantiates a new backup process.
|
||||
* @param bm the associated backup manager.
|
||||
* @param n the process identifier.
|
||||
*/
|
||||
protected BackupProcess(BackupManager bm, final String n) {
|
||||
backupManager = bm;
|
||||
identifier = n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the associated backup manager.
|
||||
* @return the associated backup manager.
|
||||
*/
|
||||
public BackupManager getBackupManager() {
|
||||
return backupManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the process identifier.
|
||||
* @return the process identifier.
|
||||
*/
|
||||
public String getIdentifier() {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the display name of this process.
|
||||
* Default implementation returns {@link #getIdentifier()}.
|
||||
* @return the display name of this process.
|
||||
*/
|
||||
protected String getDisplayName() {
|
||||
return getIdentifier();
|
||||
}
|
||||
@@ -55,9 +82,11 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Provides a predicate that tells if a provided file must be included in the archive or not.
|
||||
* The default implementation returns a filter based on the content of {@link #getIgnoreList()}.
|
||||
* @return a predicate.
|
||||
*/
|
||||
public BiPredicate<File, String> getFilenameFilter() {
|
||||
return (file, path) -> {
|
||||
for (String exclude : ignoreList) {
|
||||
@@ -75,47 +104,91 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the source directory to back up.
|
||||
* @return the source directory to back up.
|
||||
*/
|
||||
public abstract File getSourceDir();
|
||||
|
||||
/**
|
||||
* Gets the directory in which to put the archives.
|
||||
* @return the directory in which to put the archives.
|
||||
*/
|
||||
protected abstract File getTargetDir();
|
||||
|
||||
/**
|
||||
* Called when the backup starts.
|
||||
*/
|
||||
protected abstract void onBackupStart();
|
||||
|
||||
/**
|
||||
* Called when the backup ends.
|
||||
* @param success true if the backup ended successfully.
|
||||
*/
|
||||
protected abstract void onBackupEnd(boolean success);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Tells if this backup process is enabled.
|
||||
* A disabled backup process will not run.
|
||||
* @return true if this backup process is enabled, false otherwise.
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the enabled status of this backup process.
|
||||
* @param enabled the enabled status of this backup process.
|
||||
*/
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the string representation of the scheduling, using cron format.
|
||||
* @return the string representation of the scheduling.
|
||||
*/
|
||||
public String getScheduling() {
|
||||
return scheduling;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the string representation of the scheduling.
|
||||
* @param scheduling the string representation of the scheduling, in the CRON format (without seconds).
|
||||
*/
|
||||
public void setScheduling(String scheduling) {
|
||||
this.scheduling = scheduling;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the associated backup cleaner, that is executed at the end of this backup process.
|
||||
* @return the associated backup cleaner.
|
||||
*/
|
||||
public BackupCleaner getBackupCleaner() {
|
||||
return backupCleaner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the backup cleaner of this backup process.
|
||||
* @param backupCleaner the backup cleaner of this backup process.
|
||||
*/
|
||||
public void setBackupCleaner(BackupCleaner backupCleaner) {
|
||||
this.backupCleaner = backupCleaner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current list of files that are ignored during the backup process.
|
||||
* @return the current list of files that are ignored during the backup process.
|
||||
*/
|
||||
public List<String> getIgnoreList() {
|
||||
return ignoreList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new list of files that will be ignored during the backup process.
|
||||
* @param ignoreList the new list of files that are ignored during the backup process.
|
||||
*/
|
||||
public void setIgnoreList(List<String> ignoreList) {
|
||||
this.ignoreList = ignoreList;
|
||||
}
|
||||
@@ -136,7 +209,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
|
||||
File sourceDir = getSourceDir();
|
||||
|
||||
if (!sourceDir.exists()) {
|
||||
Log.warning("[Backup] Unable to compress " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + ": source directory " + sourceDir + " doesn’t exist");
|
||||
Log.warning("[Backup] Unable to compress " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + ": source directory " + sourceDir + " doesn't exist");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -146,7 +219,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
|
||||
onBackupStart();
|
||||
|
||||
new Thread(() -> {
|
||||
Log.info("[Backup] Starting for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " ...");
|
||||
Log.info("[Backup] Starting for " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " ...");
|
||||
|
||||
compressor = new ZipCompressor(sourceDir, target, 9, filter);
|
||||
|
||||
@@ -156,7 +229,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
|
||||
|
||||
success = true;
|
||||
|
||||
Log.info("[Backup] Finished for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET);
|
||||
Log.info("[Backup] Finished for " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET);
|
||||
|
||||
try {
|
||||
BackupCleaner cleaner = getBackupCleaner();
|
||||
@@ -190,17 +263,18 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Logs the scheduling status of this backup process.
|
||||
*/
|
||||
public void displayNextSchedule() {
|
||||
Log.info("[Backup] " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " next backup on "
|
||||
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext())));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public abstract void displayNextSchedule();
|
||||
|
||||
|
||||
/**
|
||||
* A formatter used to format and parse the name of backup archives, based on a date and time.
|
||||
*/
|
||||
public static final DateTimeFormatter dateFileNameFormatter = new DateTimeFormatterBuilder()
|
||||
.appendValue(ChronoField.YEAR, 4)
|
||||
.appendValue(ChronoField.MONTH_OF_YEAR, 2)
|
||||
@@ -216,49 +290,67 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
|
||||
return dateFileNameFormatter.format(ZonedDateTime.now());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Logs the progress of this currently running backup process.
|
||||
* Logs nothing if this backup is not in progress.
|
||||
*/
|
||||
public void logProgress() {
|
||||
if (compressor == null)
|
||||
return;
|
||||
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + ": " + compressor.getState().getLegacyText());
|
||||
Log.info("[Backup] " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + ": " + compressor.getState().getLegacyText());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Tells if this backup process could start now.
|
||||
* @return true if this backup process could start now, false otherwise.
|
||||
*/
|
||||
public boolean couldRunNow() {
|
||||
if (!isEnabled())
|
||||
return false;
|
||||
if (!isDirty())
|
||||
return false;
|
||||
if (getNext() > System.currentTimeMillis())
|
||||
return false;
|
||||
return true;
|
||||
return getNext() <= System.currentTimeMillis();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Gets the time of the next scheduled run.
|
||||
* @return the time, in millis-timestamp, of the next scheduled run, or {@link Long#MAX_VALUE} if it’s not scheduled.
|
||||
*/
|
||||
public long getNext() {
|
||||
if (!hasNextScheduled())
|
||||
return Long.MAX_VALUE;
|
||||
return getNextCompress(backupManager.persist.isDirtySince(identifier));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if this backup is scheduled or not.
|
||||
* @return true if this backup is scheduled, false otherwise.
|
||||
*/
|
||||
public boolean hasNextScheduled() {
|
||||
return isEnabled() && isDirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the content to be backed up is dirty or not. The source data is not dirty if it has not changed since
|
||||
* the last backup.
|
||||
* @return the dirty status of the data to be backed-up by this backup process.
|
||||
*/
|
||||
public boolean isDirty() {
|
||||
return backupManager.persist.isDirty(identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the source data as dirty since now.
|
||||
*/
|
||||
public void setDirtySinceNow() {
|
||||
backupManager.persist.setDirtySinceNow(identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the source data as not dirty.
|
||||
*/
|
||||
public void setNotDirty() {
|
||||
backupManager.persist.setNotDirty(identifier);
|
||||
}
|
||||
@@ -268,9 +360,9 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
|
||||
|
||||
|
||||
/**
|
||||
* get the timestamp (in ms) of when the next compress will run, depending on since when the files to compress are dirty.
|
||||
* @param dirtySince the timestamp in ms since the files are dirty
|
||||
* @return the timestamp in ms when the next compress of the files should be run, or 0 if it is not yet scheduled
|
||||
* Gets the millis-timestamp of when the next compress will run, depending on since when the files to compress are dirty.
|
||||
* @param dirtySince the timestamp in ms since the files are dirty.
|
||||
* @return the timestamp in ms when the next compress of the files should be run, or 0 if it is not yet scheduled.
|
||||
*/
|
||||
public long getNextCompress(long dirtySince) {
|
||||
if (dirtySince == -1)
|
||||
|
@@ -3,7 +3,7 @@ package fr.pandacube.lib.core.backup;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import fr.pandacube.lib.core.json.Json;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
@@ -12,6 +12,11 @@ import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Handles the data stored used for backup manager, like dirty status of data to be backed up.
|
||||
* The data is stored using JSON format, in a file in the root backup directory.
|
||||
* The file is updated on disk on every call to a {@code set*(...)} method.
|
||||
*/
|
||||
public class Persist {
|
||||
|
||||
private Map<String, Long> dirtySince = new HashMap<>();
|
||||
@@ -19,17 +24,18 @@ public class Persist {
|
||||
private final File file;
|
||||
|
||||
// private final Set<String> dirtyWorldsSave = new HashSet<>();
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new instance, immediately loading the data from the file if it exists, or creating an empty one if not.
|
||||
* @param bm the associated backup manager.
|
||||
*/
|
||||
public Persist(BackupManager bm) {
|
||||
file = new File(bm.getBackupDirectory(), "source-dirty-since.json");
|
||||
load();
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
load();
|
||||
}
|
||||
|
||||
protected void load() {
|
||||
private void load() {
|
||||
boolean loaded = false;
|
||||
try (FileReader reader = new FileReader(file)) {
|
||||
dirtySince = Json.gson.fromJson(reader, new TypeToken<Map<String, Long>>(){}.getType());
|
||||
@@ -48,8 +54,8 @@ public class Persist {
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
public void save() {
|
||||
|
||||
private void save() {
|
||||
try (FileWriter writer = new FileWriter(file, false)) {
|
||||
Json.gsonPrettyPrinting.toJson(dirtySince, writer);
|
||||
}
|
||||
@@ -57,27 +63,41 @@ public class Persist {
|
||||
Log.severe("could not save " + file, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public void setDirtySinceNow(String id) {
|
||||
|
||||
/**
|
||||
* Sets the backup process with the provided id as dirty.
|
||||
* @param id the id of the backup process.
|
||||
*/
|
||||
public synchronized void setDirtySinceNow(String id) {
|
||||
dirtySince.put(id, System.currentTimeMillis());
|
||||
save();
|
||||
}
|
||||
|
||||
public void setNotDirty(String id) {
|
||||
/**
|
||||
* Sets the backup process with the provided id as not dirty.
|
||||
* @param id the id of the backup process.
|
||||
*/
|
||||
public synchronized void setNotDirty(String id) {
|
||||
dirtySince.put(id, -1L);
|
||||
save();
|
||||
}
|
||||
|
||||
|
||||
public boolean isDirty(String id) {
|
||||
|
||||
/**
|
||||
* Tells if the backup process with the provided id is dirty.
|
||||
* @param id the id of the backup process.
|
||||
* @return true if the process is marked as dirty, false otherwise.
|
||||
*/
|
||||
public synchronized boolean isDirty(String id) {
|
||||
return isDirtySince(id) != -1;
|
||||
}
|
||||
|
||||
public long isDirtySince(String id) {
|
||||
|
||||
/**
|
||||
* Tells since when the backup process with the provided id is dirty.
|
||||
* @param id the id of the backup process.
|
||||
* @return the millis-timestamp of when the backup process has been marked dirty.
|
||||
*/
|
||||
public synchronized long isDirtySince(String id) {
|
||||
if (!dirtySince.containsKey(id))
|
||||
setDirtySinceNow(id);
|
||||
return dirtySince.get(id);
|
||||
|
@@ -1,22 +1,32 @@
|
||||
package fr.pandacube.lib.core.backup;
|
||||
|
||||
import com.google.common.io.Files;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import fr.pandacube.lib.chat.LegacyChatFormat;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.DateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.function.BiPredicate;
|
||||
|
||||
/**
|
||||
* A special backup process that handle the backup of rotated log files.
|
||||
*/
|
||||
public class RotatedLogsBackupProcess extends BackupProcess {
|
||||
final String logFileRegexPattern;
|
||||
final File sourceLogDirectory;
|
||||
final boolean inNewThread;
|
||||
|
||||
/**
|
||||
* Create a new instance of this backup process.
|
||||
* @param bm the backup manager.
|
||||
* @param inNewThread tells if this process should be run in a separate thread (true) or in the same thread handling
|
||||
* the backup manager (false).
|
||||
* @param sourceLogDir the directory where the rotated log files are stored, usually {@code ./logs/}.
|
||||
* @param logFileRegexPattern the pattern to match the rotated log files (usually dated log files, excluding the
|
||||
* current log file).
|
||||
*/
|
||||
public RotatedLogsBackupProcess(BackupManager bm, boolean inNewThread, File sourceLogDir, String logFileRegexPattern) {
|
||||
super(bm, "logs");
|
||||
this.logFileRegexPattern = logFileRegexPattern;
|
||||
@@ -43,7 +53,7 @@ public class RotatedLogsBackupProcess extends BackupProcess {
|
||||
if (!getSourceDir().isDirectory())
|
||||
return;
|
||||
|
||||
Log.info("[Backup] Starting for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " ...");
|
||||
Log.info("[Backup] Starting for " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " ...");
|
||||
|
||||
try {
|
||||
// wait a little after the log message above, in case the log file rotation has to be performed.
|
||||
@@ -72,9 +82,9 @@ public class RotatedLogsBackupProcess extends BackupProcess {
|
||||
|
||||
success = true;
|
||||
|
||||
Log.info("[Backup] Finished for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET);
|
||||
Log.info("[Backup] Finished for " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET);
|
||||
} catch (Exception e) {
|
||||
Log.severe("[Backup] Failed for : " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET, e);
|
||||
Log.severe("[Backup] Failed for : " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET, e);
|
||||
} finally {
|
||||
onBackupEnd(success);
|
||||
|
||||
@@ -84,7 +94,7 @@ public class RotatedLogsBackupProcess extends BackupProcess {
|
||||
|
||||
|
||||
|
||||
public List<File> getFilesToMove() {
|
||||
private List<File> getFilesToMove() {
|
||||
List<File> ret = new ArrayList<>();
|
||||
for (File f : getSourceDir().listFiles()) {
|
||||
if (f.getName().matches(logFileRegexPattern))
|
||||
@@ -120,10 +130,4 @@ public class RotatedLogsBackupProcess extends BackupProcess {
|
||||
protected void onBackupEnd(boolean success) {
|
||||
setDirtySinceNow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void displayNextSchedule() {
|
||||
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " next backup on "
|
||||
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext())));
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -117,7 +118,11 @@ public class ZipCompressor {
|
||||
}
|
||||
|
||||
for (Entry entry : entriesToCompress) {
|
||||
entry.zip();
|
||||
try {
|
||||
entry.zip();
|
||||
} catch (NoSuchFileException ignored) {
|
||||
// file has been deleted since
|
||||
}
|
||||
}
|
||||
|
||||
synchronized (stateLock) {
|
||||
@@ -158,8 +163,8 @@ public class ZipCompressor {
|
||||
}
|
||||
|
||||
private class Entry {
|
||||
File file;
|
||||
String entry;
|
||||
final File file;
|
||||
final String entry;
|
||||
Entry(File f, String e) {
|
||||
file = f;
|
||||
entry = e;
|
||||
|
@@ -4,7 +4,7 @@ import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import fc.cron.CronExpression;
|
||||
import fr.pandacube.lib.core.json.Json;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
@@ -20,7 +20,6 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
// TODO Add support for persisted last execution timestamps
|
||||
/**
|
||||
* Application wide task scheduler using Cron expression.
|
||||
*/
|
||||
@@ -55,10 +54,10 @@ public class CronScheduler {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if (!tasks.isEmpty()) {
|
||||
CronTask next = tasks.get(0);
|
||||
CronTask next = tasks.getFirst();
|
||||
if (next.nextRun <= now) {
|
||||
next.runAsync();
|
||||
setLastRun(next.taskId, next.nextRun);
|
||||
setLastRun(next.taskId, now);
|
||||
onTaskUpdate(false);
|
||||
continue;
|
||||
}
|
||||
@@ -78,8 +77,10 @@ public class CronScheduler {
|
||||
|
||||
/**
|
||||
* Schedule a task.
|
||||
* If a task with the provided taskId already exists, it will be replaced.
|
||||
* @param taskId the id of the task.
|
||||
* @param cronExpression the scheduling of the task. May use seconds (6 values) or not (5 values)
|
||||
* @param cronExpression the scheduling of the task. May use seconds (6 values) or not (5 values).
|
||||
* See {@link CronExpression} for the format.
|
||||
* @param task the task to run.
|
||||
*/
|
||||
public static void schedule(String taskId, String cronExpression, Runnable task) {
|
||||
@@ -101,7 +102,7 @@ public class CronScheduler {
|
||||
|
||||
/**
|
||||
* Cancel a scheduled task.
|
||||
* Will not stop a current execution of the task. If the task does not exists, it will not do anything.
|
||||
* Will not stop a current execution of the task. If the task does not exist, it will not do anything.
|
||||
* @param taskId the id of the task to cancel.
|
||||
*/
|
||||
public static void unSchedule(String taskId) {
|
||||
@@ -185,8 +186,6 @@ public class CronScheduler {
|
||||
catch (final JsonParseException e) {
|
||||
Log.severe("cannot load " + lastRunFile, e);
|
||||
}
|
||||
finally {
|
||||
}
|
||||
|
||||
if (!loaded) {
|
||||
saveLastRuns();
|
||||
@@ -225,5 +224,6 @@ public class CronScheduler {
|
||||
.toEpochMilli();
|
||||
}
|
||||
|
||||
private CronScheduler() {}
|
||||
|
||||
}
|
||||
|
@@ -1,47 +1,96 @@
|
||||
package fr.pandacube.lib.core.json;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.RecordComponent;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.ToNumberStrategy;
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.TypeAdapterFactory;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonToken;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import com.google.gson.stream.MalformedJsonException;
|
||||
import fr.pandacube.lib.core.mc_version.MinecraftVersionList.MinecraftVersionListAdapter;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Provides pre-instanciated {@link Gson} instances, all with support for Java records.
|
||||
* Provides pre-instanced {@link Gson} objects, all with support for Java records and additional
|
||||
* {@link TypeAdapterFactory} provided with {@link #registerTypeAdapterFactory(TypeAdapterFactory)}.
|
||||
*/
|
||||
public class Json {
|
||||
|
||||
/**
|
||||
* {@link Gson} instance with {@link GsonBuilder#setLenient()} and support for Java records.
|
||||
* Makes Gson deserialize numbers to Number subclasses the same way SnakeYAML does
|
||||
*/
|
||||
private static final ToNumberStrategy YAML_EQUIVALENT_NUMBER_STRATEGY = in -> {
|
||||
String value = in.nextString();
|
||||
|
||||
// YAML uses Regex to resolve values as INT or FLOAT (see org.yaml.snakeyaml.resolver.Resolver), trying FLOAT first.
|
||||
// We see in the regex that FLOAT MUST have a "." in the string, but INT must not, so we try that.
|
||||
boolean isFloat = value.contains(".");
|
||||
|
||||
if (isFloat) {
|
||||
// if float, will only parse to Double
|
||||
// (see org.yaml.snakeyaml.constructor.SafeConstructor.ConstructYamlFloat)
|
||||
try {
|
||||
Double d = Double.valueOf(value);
|
||||
if ((d.isInfinite() || d.isNaN()) && !in.isLenient()) {
|
||||
throw new MalformedJsonException("JSON forbids NaN and infinities: " + d + "; at path " + in.getPreviousPath());
|
||||
}
|
||||
return d;
|
||||
} catch (NumberFormatException e) {
|
||||
throw new JsonParseException("Cannot parse " + value + "; at path " + in.getPreviousPath(), e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// if integer, will try to parse int, then long, then BigDecimal
|
||||
// (see org.yaml.snakeyaml.constructor.SafeConstructor.ConstructYamlInt
|
||||
// then org.yaml.snakeyaml.constructor.SafeConstructor.createNumber)
|
||||
try {
|
||||
return Integer.valueOf(value);
|
||||
} catch (NumberFormatException e) {
|
||||
try {
|
||||
return Long.valueOf(value);
|
||||
} catch (NumberFormatException e2) {
|
||||
try {
|
||||
return new BigInteger(value);
|
||||
} catch (NumberFormatException e3) {
|
||||
throw new JsonParseException("Cannot parse " + value + "; at path " + in.getPreviousPath(), e3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* {@link Gson} instance with {@link GsonBuilder#setLenient()} and support for Java records and additional
|
||||
* {@link TypeAdapterFactory} provided with {@link #registerTypeAdapterFactory(TypeAdapterFactory)}.
|
||||
*/
|
||||
public static final Gson gson = build(Function.identity());
|
||||
|
||||
/**
|
||||
* {@link Gson} instance with {@link GsonBuilder#setLenient()}, {@link GsonBuilder#setPrettyPrinting()}
|
||||
* and support for Java records.
|
||||
* {@link Gson} instance with {@link GsonBuilder#setLenient()}, {@link GsonBuilder#setPrettyPrinting()} and support
|
||||
* for Java records and additional {@link TypeAdapterFactory} provided with
|
||||
* {@link #registerTypeAdapterFactory(TypeAdapterFactory)}.
|
||||
*/
|
||||
public static final Gson gsonPrettyPrinting = build(GsonBuilder::setPrettyPrinting);
|
||||
|
||||
/**
|
||||
* {@link Gson} instance with {@link GsonBuilder#setLenient()}, {@link GsonBuilder#serializeNulls()}
|
||||
* and support for Java records.
|
||||
* {@link Gson} instance with {@link GsonBuilder#setLenient()}, {@link GsonBuilder#serializeNulls()} and support for
|
||||
* Java records and additional {@link TypeAdapterFactory} provided with
|
||||
* {@link #registerTypeAdapterFactory(TypeAdapterFactory)}.
|
||||
*/
|
||||
public static final Gson gsonSerializeNulls = build(GsonBuilder::serializeNulls);
|
||||
|
||||
/**
|
||||
* {@link Gson} instance with {@link GsonBuilder#setLenient()}, {@link GsonBuilder#serializeNulls()},
|
||||
* {@link GsonBuilder#setPrettyPrinting()} and support for Java records.
|
||||
* {@link GsonBuilder#setPrettyPrinting()} and support for Java records and additional {@link TypeAdapterFactory}
|
||||
* provided with {@link #registerTypeAdapterFactory(TypeAdapterFactory)}.
|
||||
*/
|
||||
public static final Gson gsonSerializeNullsPrettyPrinting = build(b -> b.serializeNulls().setPrettyPrinting());
|
||||
|
||||
@@ -52,84 +101,68 @@ public class Json {
|
||||
|
||||
|
||||
private static Gson build(Function<GsonBuilder, GsonBuilder> builderModifier) {
|
||||
return builderModifier
|
||||
.apply(new GsonBuilder().registerTypeAdapterFactory(new RecordAdapterFactory()).setLenient()).create();
|
||||
GsonBuilder base = new GsonBuilder()
|
||||
.registerTypeAdapterFactory(new CustomAdapterFactory())
|
||||
.disableHtmlEscaping()
|
||||
.setObjectToNumberStrategy(YAML_EQUIVALENT_NUMBER_STRATEGY)
|
||||
.setLenient();
|
||||
return builderModifier.apply(base).create();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds the provided {@link TypeAdapterFactory} to all the static Gson instances of this class.
|
||||
* @param factory the factory to add to the
|
||||
*/
|
||||
public static void registerTypeAdapterFactory(TypeAdapterFactory factory) {
|
||||
synchronized (customTypeAdapterFactories) {
|
||||
customTypeAdapterFactories.add(factory);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static final List<TypeAdapterFactory> customTypeAdapterFactories = new ArrayList<>();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// from https://github.com/google/gson/issues/1794#issuecomment-812964421
|
||||
private static class RecordAdapterFactory implements TypeAdapterFactory {
|
||||
private static class CustomAdapterFactory implements TypeAdapterFactory {
|
||||
@Override
|
||||
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<T> clazz = (Class<T>) type.getRawType();
|
||||
if (!clazz.isRecord() || clazz == Record.class) {
|
||||
return null;
|
||||
synchronized (customTypeAdapterFactories) {
|
||||
for (TypeAdapterFactory actualFactory : customTypeAdapterFactories) {
|
||||
TypeAdapter<T> adapter = actualFactory.create(gson, type);
|
||||
if (adapter != null)
|
||||
return adapter;
|
||||
}
|
||||
}
|
||||
return new RecordTypeAdapter<>(gson, this, type);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class RecordTypeAdapter<T> extends TypeAdapter<T> {
|
||||
private final Gson gson;
|
||||
private final TypeAdapterFactory factory;
|
||||
private final TypeToken<T> type;
|
||||
|
||||
public RecordTypeAdapter(Gson gson, TypeAdapterFactory factory, TypeToken<T> type) {
|
||||
this.gson = gson;
|
||||
this.factory = factory;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(JsonWriter out, T value) throws IOException {
|
||||
gson.getDelegateAdapter(factory, type).write(out, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T read(JsonReader reader) throws IOException {
|
||||
if (reader.peek() == JsonToken.NULL) {
|
||||
reader.nextNull();
|
||||
return null;
|
||||
} else {
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<T> clazz = (Class<T>) type.getRawType();
|
||||
|
||||
RecordComponent[] recordComponents = clazz.getRecordComponents();
|
||||
Map<String, TypeToken<?>> typeMap = new HashMap<>();
|
||||
for (RecordComponent recordComponent : recordComponents) {
|
||||
typeMap.put(recordComponent.getName(), TypeToken.get(recordComponent.getGenericType()));
|
||||
}
|
||||
var argsMap = new HashMap<String, Object>();
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
String name = reader.nextName();
|
||||
argsMap.put(name, gson.getAdapter(typeMap.get(name)).read(reader));
|
||||
}
|
||||
reader.endObject();
|
||||
|
||||
var argTypes = new Class<?>[recordComponents.length];
|
||||
var args = new Object[recordComponents.length];
|
||||
for (int i = 0; i < recordComponents.length; i++) {
|
||||
argTypes[i] = recordComponents[i].getType();
|
||||
args[i] = argsMap.get(recordComponents[i].getName());
|
||||
}
|
||||
Constructor<T> constructor;
|
||||
try {
|
||||
constructor = clazz.getDeclaredConstructor(argTypes);
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance(args);
|
||||
} catch (NoSuchMethodException | InstantiationException | SecurityException | IllegalAccessException
|
||||
| IllegalArgumentException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
static {
|
||||
registerTypeAdapterFactory(StackTraceElementAdapter.FACTORY);
|
||||
registerTypeAdapterFactory(ThrowableAdapter.FACTORY);
|
||||
registerTypeAdapterFactory(MinecraftVersionListAdapter.FACTORY);
|
||||
}
|
||||
|
||||
|
||||
/*public static void main(String[] args) {
|
||||
TypeToken<Map<String, Object>> MAP_STR_OBJ_TYPE = new TypeToken<>() { };
|
||||
Map<String, Object> map = gson.fromJson("{" +
|
||||
"\"int\":34," +
|
||||
"\"long\":3272567356876864," +
|
||||
"\"bigint\":-737868677777837833757846576245765," +
|
||||
"\"float\":34.0" +
|
||||
"}", MAP_STR_OBJ_TYPE.getType());
|
||||
for (String key : map.keySet()) {
|
||||
Object v = map.get(key);
|
||||
System.out.println(key + ": " + v + " (type " + v.getClass() + ")");
|
||||
}
|
||||
}*/
|
||||
|
||||
private Json() {}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,64 @@
|
||||
package fr.pandacube.lib.core.json;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
import com.google.gson.TypeAdapterFactory;
|
||||
import com.google.gson.internal.bind.TreeTypeAdapter;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
/* package */ class StackTraceElementAdapter implements JsonSerializer<StackTraceElement>, JsonDeserializer<StackTraceElement> {
|
||||
|
||||
public static final TypeAdapterFactory FACTORY = TreeTypeAdapter.newTypeHierarchyFactory(StackTraceElement.class, new StackTraceElementAdapter());
|
||||
|
||||
|
||||
@Override
|
||||
public StackTraceElement deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
JsonObject obj = json.getAsJsonObject();
|
||||
|
||||
String classLoader = obj.has("classloader") && obj.get("classloader").isJsonPrimitive()
|
||||
? obj.get("classloader").getAsString() : null;
|
||||
String module = obj.has("module") && obj.get("module").isJsonPrimitive()
|
||||
? obj.get("module").getAsString() : null;
|
||||
String moduleVersion = obj.has("moduleversion") && obj.get("moduleversion").isJsonPrimitive()
|
||||
? obj.get("moduleversion").getAsString() : null;
|
||||
String clazz = obj.has("class") && obj.get("class").isJsonPrimitive()
|
||||
? obj.get("class").getAsString() : null;
|
||||
if (clazz == null) {
|
||||
throw new JsonParseException("Missing 'class' entry");
|
||||
}
|
||||
String method = obj.has("method") && obj.get("method").isJsonPrimitive()
|
||||
? obj.get("method").getAsString() : null;
|
||||
if (method == null) {
|
||||
throw new JsonParseException("Missing 'method' entry");
|
||||
}
|
||||
String file = obj.has("file") && obj.get("file").isJsonPrimitive()
|
||||
? obj.get("file").getAsString() : null;
|
||||
int line = obj.has("line") && obj.get("line").isJsonPrimitive()
|
||||
? obj.get("line").getAsInt() : -1;
|
||||
|
||||
return new StackTraceElement(classLoader, module, moduleVersion, clazz, method, file, line);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(StackTraceElement src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonObject obj = new JsonObject();
|
||||
obj.addProperty("class", src.getClassName());
|
||||
obj.addProperty("method", src.getMethodName());
|
||||
obj.addProperty("line", src.getLineNumber());
|
||||
if (src.getClassLoaderName() != null)
|
||||
obj.addProperty("classloader", src.getClassLoaderName());
|
||||
if (src.getModuleName() != null)
|
||||
obj.addProperty("module", src.getModuleName());
|
||||
if (src.getModuleVersion() != null)
|
||||
obj.addProperty("moduleversion", src.getModuleVersion());
|
||||
if (src.getFileName() != null)
|
||||
obj.addProperty("file", src.getFileName());
|
||||
return obj;
|
||||
}
|
||||
}
|
@@ -0,0 +1,218 @@
|
||||
package fr.pandacube.lib.core.json;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
import com.google.gson.TypeAdapterFactory;
|
||||
import com.google.gson.internal.bind.TreeTypeAdapter;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
import fr.pandacube.lib.util.ThrowableUtil;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Gson Adapter that handles serialization and deserialization of {@link Throwable} instances properly.
|
||||
*/
|
||||
public class ThrowableAdapter implements JsonSerializer<Throwable>, JsonDeserializer<Throwable> {
|
||||
|
||||
/* package */ static final TypeAdapterFactory FACTORY = TreeTypeAdapter.newTypeHierarchyFactory(Throwable.class, new ThrowableAdapter());
|
||||
|
||||
private ThrowableAdapter() {}
|
||||
|
||||
|
||||
@Override
|
||||
public Throwable deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
|
||||
JsonObject obj = json.getAsJsonObject();
|
||||
String message = obj.has("message") && !obj.get("message").isJsonNull()
|
||||
? obj.get("message").getAsString() : null;
|
||||
Throwable cause = obj.has("cause") && !obj.get("cause").isJsonNull()
|
||||
? context.deserialize(obj.get("cause"), Throwable.class) : null;
|
||||
|
||||
// handle types
|
||||
Throwable t = null;
|
||||
if (obj.has("types") && obj.get("types").isJsonArray()) {
|
||||
t = instantiate(obj.getAsJsonArray("types"), message, cause);
|
||||
}
|
||||
if (t == null) {
|
||||
t = new Throwable(message, cause);
|
||||
}
|
||||
|
||||
// handle suppressed
|
||||
JsonArray suppressed = obj.has("suppressed") && !obj.get("suppressed").isJsonNull()
|
||||
? obj.get("suppressed").getAsJsonArray() : null;
|
||||
if (suppressed != null) {
|
||||
for (JsonElement jsonEl : suppressed) {
|
||||
t.addSuppressed(context.deserialize(jsonEl, Throwable.class));
|
||||
}
|
||||
}
|
||||
|
||||
// handle stacktrace
|
||||
JsonArray stacktrace = obj.has("stacktrace") && !obj.get("stacktrace").isJsonNull()
|
||||
? obj.get("stacktrace").getAsJsonArray() : null;
|
||||
if (stacktrace != null) {
|
||||
List<StackTraceElement> els = new ArrayList<>();
|
||||
for (JsonElement jsonEl : stacktrace) {
|
||||
els.add(context.deserialize(jsonEl, StackTraceElement.class));
|
||||
}
|
||||
t.setStackTrace(els.toArray(new StackTraceElement[0]));
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(Throwable src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonObject json = new JsonObject();
|
||||
|
||||
// toString for easy json reading (not used for deserialization)
|
||||
json.addProperty("tostring", src.toString());
|
||||
|
||||
// handle types
|
||||
JsonArray types = new JsonArray();
|
||||
Class<?> cl = src.getClass();
|
||||
while (cl != Throwable.class) {
|
||||
if (cl.getCanonicalName() != null)
|
||||
types.add(cl.getCanonicalName());
|
||||
cl = cl.getSuperclass();
|
||||
}
|
||||
json.add("types", types);
|
||||
|
||||
// general data
|
||||
if (src.getMessage() != null)
|
||||
json.addProperty("message", src.getMessage());
|
||||
if (src.getCause() != null)
|
||||
json.add("cause", context.serialize(src.getCause()));
|
||||
|
||||
// handle suppressed
|
||||
JsonArray suppressed = new JsonArray();
|
||||
for (Throwable supp : src.getSuppressed()) {
|
||||
suppressed.add(context.serialize(supp));
|
||||
}
|
||||
json.add("suppressed", suppressed);
|
||||
|
||||
// handle stacktrace
|
||||
JsonArray stacktrace = new JsonArray();
|
||||
for (StackTraceElement stackTraceElement : src.getStackTrace()) {
|
||||
stacktrace.add(context.serialize(stackTraceElement));
|
||||
}
|
||||
json.add("stacktrace", stacktrace);
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private static final Map<Class<? extends Throwable>, ThrowableSubAdapter<?>> subAdapters = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
/**
|
||||
* Register a new adapter for a specific {@link Throwable} subclass.
|
||||
* @param clazz the type handled by the specified sub-adapter.
|
||||
* @param subAdapter the sub-adapter.
|
||||
* @param <T> the type.
|
||||
*/
|
||||
public static <T extends Throwable> void registerSubAdapter(Class<T> clazz, ThrowableSubAdapter<T> subAdapter) {
|
||||
subAdapters.put(clazz, subAdapter);
|
||||
}
|
||||
|
||||
private static <T extends Throwable> ThrowableSubAdapter<T> defaultSubAdapter(Class<T> clazz) {
|
||||
BiFunction<String, Throwable, T> constructor = null;
|
||||
|
||||
// try (String, Throwable) constructor
|
||||
try {
|
||||
Constructor<T> constr = clazz.getConstructor(String.class, Throwable.class);
|
||||
if (constr.canAccess(null)) {
|
||||
constructor = (m, t) -> ThrowableUtil.wrapReflectEx(() -> constr.newInstance(m, t));
|
||||
}
|
||||
} catch (ReflectiveOperationException ignore) { }
|
||||
|
||||
// try (String) constructor
|
||||
try {
|
||||
Constructor<T> constr = clazz.getConstructor(String.class);
|
||||
if (constr.canAccess(null)) {
|
||||
constructor = ThrowableSubAdapter.messageOnly((m) -> ThrowableUtil.wrapReflectEx(() -> constr.newInstance(m)));
|
||||
}
|
||||
} catch (ReflectiveOperationException ignore) { }
|
||||
|
||||
if (constructor == null) {
|
||||
Log.warning("Provided Throwable class '" + clazz + "' does not have any of those constructors or are not accessible: (String, Throwable), (String).");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ThrowableSubAdapter<>(constructor);
|
||||
}
|
||||
|
||||
|
||||
private Throwable instantiate(JsonArray types, String message, Throwable cause) {
|
||||
Throwable t = null;
|
||||
for (JsonElement clNameEl : types) {
|
||||
String clName = clNameEl.getAsString();
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<? extends Throwable> cl = (Class<? extends Throwable>) Class.forName(clName);
|
||||
ThrowableSubAdapter<? extends Throwable> subAdapter = subAdapters.get(cl);
|
||||
if (subAdapter == null)
|
||||
subAdapter = defaultSubAdapter(cl);
|
||||
|
||||
if (subAdapter != null) {
|
||||
t = subAdapter.constructor.apply(message, cause);
|
||||
break;
|
||||
}
|
||||
} catch (ReflectiveOperationException ignore) { }
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adapter for specific subclasses of {@link Throwable}.
|
||||
* @param <T> the type handled by this adapter.
|
||||
*/
|
||||
public static class ThrowableSubAdapter<T extends Throwable> {
|
||||
private final BiFunction<String, Throwable, T> constructor;
|
||||
|
||||
/**
|
||||
* Creates a new adapter for a {@link Throwable}.
|
||||
* @param constructor function that will construct a new throwable of the handled type, with prefilled message and cause if possible.
|
||||
*/
|
||||
protected ThrowableSubAdapter(BiFunction<String, Throwable, T> constructor) {
|
||||
this.constructor = constructor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to use on {@link Throwable} class that only have a message (no cause) constructor.
|
||||
* @param constructorWithMessage function that will construct a new throwable, with prefilled message.
|
||||
* @return a function that will construct a throwable using the provided function, then will try to init the cause of the throwable.
|
||||
* @param <T> the type of the constructed {@link Throwable}.
|
||||
*/
|
||||
public static <T extends Throwable> BiFunction<String, Throwable, T> messageOnly(Function<String, T> constructorWithMessage) {
|
||||
return (m, t) -> {
|
||||
T inst = constructorWithMessage.apply(m);
|
||||
try {
|
||||
inst.initCause(t);
|
||||
} catch (Exception ignore) { }
|
||||
return inst;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -15,8 +15,8 @@ public class TypeConverter {
|
||||
/**
|
||||
* Converts the provided object to an {@link Integer}.
|
||||
* @param o the object to convert.
|
||||
* @return a the object converted to an {@link Integer}.
|
||||
* @throws ConvertionException is a conversion error occurs.
|
||||
* @return the object converted to an {@link Integer}.
|
||||
* @throws ConversionException is a conversion error occurs.
|
||||
*/
|
||||
public static Integer toInteger(Object o) {
|
||||
if (o == null) {
|
||||
@@ -27,7 +27,7 @@ public class TypeConverter {
|
||||
try {
|
||||
return ((JsonElement)o).getAsInt();
|
||||
} catch(UnsupportedOperationException e) {
|
||||
throw new ConvertionException(e);
|
||||
throw new ConversionException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,34 +38,34 @@ public class TypeConverter {
|
||||
try {
|
||||
return Integer.parseInt((String)o);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ConvertionException(e);
|
||||
throw new ConversionException(e);
|
||||
}
|
||||
}
|
||||
if (o instanceof Boolean) {
|
||||
return ((Boolean)o) ? 1 : 0;
|
||||
}
|
||||
|
||||
throw new ConvertionException("No integer convertion available for an instance of "+o.getClass());
|
||||
throw new ConversionException("No integer conversion available for an instance of "+o.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided object to a primitive int.
|
||||
* @param o the object to convert.
|
||||
* @return a the object converted to a primitive int.
|
||||
* @throws ConvertionException is a conversion error occurs.
|
||||
* @return the object converted to a primitive int.
|
||||
* @throws ConversionException is a conversion error occurs.
|
||||
*/
|
||||
public static int toPrimInt(Object o) {
|
||||
Integer val = toInteger(o);
|
||||
if (val == null)
|
||||
throw new ConvertionException("null values can't be converted to primitive int");
|
||||
throw new ConversionException("null values can't be converted to primitive int");
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided object to a {@link Double}.
|
||||
* @param o the object to convert.
|
||||
* @return a the object converted to a {@link Double}.
|
||||
* @throws ConvertionException is a conversion error occurs.
|
||||
* @return the object converted to a {@link Double}.
|
||||
* @throws ConversionException is a conversion error occurs.
|
||||
*/
|
||||
public static Double toDouble(Object o) {
|
||||
if (o == null) {
|
||||
@@ -76,7 +76,7 @@ public class TypeConverter {
|
||||
try {
|
||||
return ((JsonElement)o).getAsDouble();
|
||||
} catch(UnsupportedOperationException e) {
|
||||
throw new ConvertionException(e);
|
||||
throw new ConversionException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,35 +87,35 @@ public class TypeConverter {
|
||||
try {
|
||||
return Double.parseDouble((String)o);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ConvertionException(e);
|
||||
throw new ConversionException(e);
|
||||
}
|
||||
}
|
||||
if (o instanceof Boolean) {
|
||||
return ((Boolean)o) ? 1d : 0d;
|
||||
}
|
||||
|
||||
throw new ConvertionException("No double convertion available for an instance of "+o.getClass());
|
||||
throw new ConversionException("No double conversion available for an instance of "+o.getClass());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided object to a primitive double.
|
||||
* @param o the object to convert.
|
||||
* @return a the object converted to a primitive double.
|
||||
* @throws ConvertionException is a conversion error occurs.
|
||||
* @return the object converted to a primitive double.
|
||||
* @throws ConversionException is a conversion error occurs.
|
||||
*/
|
||||
public static double toPrimDouble(Object o) {
|
||||
Double val = toDouble(o);
|
||||
if (val == null)
|
||||
throw new ConvertionException("null values can't converted to primitive int");
|
||||
throw new ConversionException("null values can't converted to primitive int");
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided object to a {@link String}.
|
||||
* @param o the object to convert.
|
||||
* @return a the object converted to a {@link String}.
|
||||
* @throws ConvertionException is a conversion error occurs.
|
||||
* @return the object converted to a {@link String}.
|
||||
* @throws ConversionException is a conversion error occurs.
|
||||
*/
|
||||
public static String toString(Object o) {
|
||||
if (o == null) {
|
||||
@@ -126,7 +126,7 @@ public class TypeConverter {
|
||||
try {
|
||||
return ((JsonElement)o).getAsString();
|
||||
} catch(UnsupportedOperationException e) {
|
||||
throw new ConvertionException(e);
|
||||
throw new ConversionException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ public class TypeConverter {
|
||||
return o.toString();
|
||||
}
|
||||
|
||||
throw new ConvertionException("No string convertion available for an instance of "+o.getClass());
|
||||
throw new ConversionException("No string conversion available for an instance of "+o.getClass());
|
||||
|
||||
}
|
||||
|
||||
@@ -144,8 +144,8 @@ public class TypeConverter {
|
||||
* @param mapIntKeys if the String key representing an int should be duplicated as integer type,
|
||||
* which map to the same value as the original String key. For example, if a key is "12" and map
|
||||
* to the object <i>o</i>, an integer key 12 will be added and map to the same object <i>o</i>.
|
||||
* @return a the object converted to a {@link Map}.
|
||||
* @throws ConvertionException is a conversion error occurs.
|
||||
* @return the object converted to a {@link Map}.
|
||||
* @throws ConversionException is a conversion error occurs.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map<Object, Object> toMap(Object o, boolean mapIntKeys) {
|
||||
@@ -186,15 +186,15 @@ public class TypeConverter {
|
||||
return map;
|
||||
}
|
||||
|
||||
throw new ConvertionException("No Map convertion available for an instance of "+o.getClass());
|
||||
throw new ConversionException("No Map conversion available for an instance of "+o.getClass());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts the provided object to a {@link List}.
|
||||
* @param o the object to convert.
|
||||
* @return a the object converted to a {@link List}.
|
||||
* @throws ConvertionException is a conversion error occurs.
|
||||
* @return the object converted to a {@link List}.
|
||||
* @throws ConversionException is a conversion error occurs.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static List<Object> toList(Object o) {
|
||||
@@ -217,7 +217,7 @@ public class TypeConverter {
|
||||
}
|
||||
|
||||
|
||||
throw new ConvertionException("No Map convertion available for an instance of "+o.getClass());
|
||||
throw new ConversionException("No Map conversion available for an instance of "+o.getClass());
|
||||
|
||||
|
||||
|
||||
@@ -225,17 +225,20 @@ public class TypeConverter {
|
||||
|
||||
|
||||
/**
|
||||
* Thrown when a convertion error occurs.
|
||||
* Thrown when a conversion error occurs.
|
||||
*/
|
||||
public static class ConvertionException extends RuntimeException {
|
||||
public static class ConversionException extends RuntimeException {
|
||||
|
||||
private ConvertionException(String m) {
|
||||
private ConversionException(String m) {
|
||||
super(m);
|
||||
}
|
||||
private ConvertionException(Throwable t) {
|
||||
private ConversionException(Throwable t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private TypeConverter() {}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,85 @@
|
||||
package fr.pandacube.lib.core.mc_version;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
import com.google.gson.TypeAdapterFactory;
|
||||
import com.google.gson.internal.bind.TreeTypeAdapter;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* Record holding the data for {@link ProtocolVersion}, to facilitate serializing and deserializing.
|
||||
* @param protocolOfVersion mapping from a version string to the corresponding protocol version number.
|
||||
* @param versionsOfProtocol mapping from a protocol version number to a list of the supported MC versions.
|
||||
*/
|
||||
public record MinecraftVersionList(
|
||||
Map<String, Integer> protocolOfVersion,
|
||||
Map<Integer, List<String>> versionsOfProtocol
|
||||
) {
|
||||
|
||||
/**
|
||||
* Creates an empty {@link MinecraftVersionList}.
|
||||
*/
|
||||
public MinecraftVersionList() {
|
||||
this(new TreeMap<>(MinecraftVersionUtil::compareVersions), new TreeMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new pair of version string and protocol version number.
|
||||
* @param versionId the version string (e.g. "1.19.4").
|
||||
* @param protocolVersion the protocol version number.
|
||||
*/
|
||||
public void add(String versionId, int protocolVersion) {
|
||||
protocolOfVersion.put(versionId, protocolVersion);
|
||||
List<String> versions = versionsOfProtocol.computeIfAbsent(protocolVersion, p -> new ArrayList<>());
|
||||
versions.add(versionId);
|
||||
versions.sort(MinecraftVersionUtil::compareVersions);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gson Adapter that ensure the data in {@link MinecraftVersionList} is sorted correctly when deserializing.
|
||||
*/
|
||||
public static class MinecraftVersionListAdapter implements JsonSerializer<MinecraftVersionList>, JsonDeserializer<MinecraftVersionList> {
|
||||
/**
|
||||
* Gson adapter factory for {@link MinecraftVersionList}.
|
||||
*/
|
||||
public static final TypeAdapterFactory FACTORY = TreeTypeAdapter.newTypeHierarchyFactory(MinecraftVersionList.class, new MinecraftVersionListAdapter());
|
||||
|
||||
private static final TypeToken<Map<String, Integer>> MAP_STR_INT_TYPE = new TypeToken<>() { };
|
||||
private static final TypeToken<Map<Integer, List<String>>> MAP_INT_LIST_STRING_TYPE = new TypeToken<>() { };
|
||||
|
||||
private MinecraftVersionListAdapter() {}
|
||||
|
||||
@Override
|
||||
public MinecraftVersionList deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (!(json instanceof JsonObject jsonObj))
|
||||
throw new JsonParseException("Expected JsonObject, got " + json.getClass().getSimpleName() + ".");
|
||||
MinecraftVersionList mvList = new MinecraftVersionList();
|
||||
mvList.protocolOfVersion.putAll(context.deserialize(jsonObj.get("protocolOfVersion"), MAP_STR_INT_TYPE.getType()));
|
||||
mvList.versionsOfProtocol.putAll(context.deserialize(jsonObj.get("versionsOfProtocol"), MAP_INT_LIST_STRING_TYPE.getType()));
|
||||
for (List<String> versionLists : mvList.versionsOfProtocol.values()) {
|
||||
versionLists.sort(MinecraftVersionUtil::compareVersions);
|
||||
}
|
||||
return mvList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(MinecraftVersionList src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonObject obj = new JsonObject();
|
||||
obj.add("protocolOfVersion", context.serialize(src.protocolOfVersion));
|
||||
obj.add("versionsOfProtocol", context.serialize(src.versionsOfProtocol));
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,141 @@
|
||||
package fr.pandacube.lib.core.mc_version;
|
||||
|
||||
import fr.pandacube.lib.util.StringUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Utility class to manipulate {@link String}s representing Minecraft versions.
|
||||
*/
|
||||
public class MinecraftVersionUtil {
|
||||
|
||||
|
||||
/**
|
||||
* Compare two Minecraft version strings. It uses the rules of semantic
|
||||
* versioning to compare the versions.
|
||||
* @param v1 the first version to compare.
|
||||
* @param v2 the second version to compare.
|
||||
* @return 0 if they are equal, <0 if v1<v2 and vice-versa.
|
||||
*/
|
||||
public static int compareVersions(String v1, String v2) {
|
||||
int[] v1Int = decomposedVersion(v1);
|
||||
int[] v2Int = decomposedVersion(v2);
|
||||
|
||||
for (int i = 0; i < Math.min(v1Int.length, v2Int.length); i++) {
|
||||
int cmp = Integer.compare(v1Int[i], v2Int[i]);
|
||||
if (cmp != 0)
|
||||
return cmp;
|
||||
}
|
||||
|
||||
return Integer.compare(v1Int.length, v2Int.length);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decompose a version string into a series of integers.
|
||||
* @param v a string representation of a version (eg. 1.19.1).
|
||||
* @return an array of int representing the provided version (eg. [1, 19, 1]).
|
||||
*/
|
||||
public static int[] decomposedVersion(String v) {
|
||||
try {
|
||||
return Arrays.stream(v.split("\\.")).mapToInt(Integer::parseInt).toArray();
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Invalid version format: '" + v + "'.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the two provided Minecraft versions are consecutive.
|
||||
* <p>
|
||||
* Two versions are consecutive if (considering {@code 1.X[.Y]}):
|
||||
* <ul>
|
||||
* <li>They are part of the same main version (X value)</li>
|
||||
* <li>v1 has no Y value, and v2 has Y = 1 (eg. 1.19 and 1.19.1) OR
|
||||
* both v1 and v2 has a Y value and those values are consecutive.
|
||||
* </li>
|
||||
* </ul>
|
||||
* @param v1 the first version.
|
||||
* @param v2 the second version.
|
||||
* @return thue if the second version is consecutive to the first one.
|
||||
*/
|
||||
public static boolean areConsecutive(String v1, String v2) {
|
||||
int[] v1Int = decomposedVersion(v1);
|
||||
int[] v2Int = decomposedVersion(v2);
|
||||
|
||||
if (v1Int.length == v2Int.length) {
|
||||
for (int i = 0; i < v1Int.length - 1; i++) {
|
||||
if (v1Int[i] != v2Int[i])
|
||||
return false;
|
||||
}
|
||||
return v1Int[v1Int.length - 1] + 1 == v2Int[v2Int.length - 1];
|
||||
}
|
||||
else if (v1Int.length == v2Int.length - 1) {
|
||||
for (int i = 0; i < v1Int.length; i++) {
|
||||
if (v1Int[i] != v2Int[i])
|
||||
return false;
|
||||
}
|
||||
return v2Int[v2Int.length - 1] == 1;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Generate a string representation of the provided list of version, with
|
||||
* merged consecutive versions and using
|
||||
* {@link StringUtil#joinGrammatically(CharSequence, CharSequence, List)}.
|
||||
*
|
||||
* @param versions the minecraft versions list to use.
|
||||
* @param finalWordSeparator the word separator between the two last versions in the returned string, like "and",
|
||||
* "or" or any other word of any language. The spaces before and after are already
|
||||
* concatenated.
|
||||
* @return a string representation of the provided list of version.
|
||||
*/
|
||||
public static String toString(List<String> versions, String finalWordSeparator) {
|
||||
if (versions.isEmpty())
|
||||
return "";
|
||||
// put them in order and remove duplicates
|
||||
versions = new ArrayList<>(toOrderedSet(versions));
|
||||
List<String> keptVersions = new ArrayList<>(versions.size());
|
||||
|
||||
for (int i = 0, firstConsecutive = 0; i < versions.size(); i++) {
|
||||
if (i == versions.size() - 1 || !areConsecutive(versions.get(i), versions.get(i + 1))) {
|
||||
if (firstConsecutive == i) {
|
||||
keptVersions.add(versions.get(i));
|
||||
firstConsecutive++;
|
||||
}
|
||||
else {
|
||||
// merge
|
||||
if (i - firstConsecutive > 1)
|
||||
keptVersions.add(versions.get(firstConsecutive) + "-" + versions.get(i));
|
||||
else {
|
||||
keptVersions.add(versions.get(firstConsecutive));
|
||||
keptVersions.add(versions.get(i));
|
||||
}
|
||||
firstConsecutive = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return StringUtil.joinGrammatically(", ", " " + finalWordSeparator + " ", keptVersions);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static Set<String> toOrderedSet(List<String> versions) {
|
||||
Set<String> set = new TreeSet<>(MinecraftVersionUtil::compareVersions);
|
||||
set.addAll(versions);
|
||||
return set;
|
||||
}
|
||||
|
||||
|
||||
private MinecraftVersionUtil() {}
|
||||
|
||||
}
|
@@ -0,0 +1,249 @@
|
||||
package fr.pandacube.lib.core.mc_version;
|
||||
|
||||
import fr.pandacube.lib.core.json.Json;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
import fr.pandacube.lib.util.StringUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* Class handling a relationship table of known Minecraft version and their
|
||||
* corresponding protocol version.
|
||||
* <p>
|
||||
* The data if fetch updated data from an external API on startup. If it fails,
|
||||
* it uses the data stored in the current package at build time.
|
||||
* <p>
|
||||
* The public static methods are used to fetch an instance of {@link ProtocolVersion}
|
||||
* based on the provided protocol version (e.g. 763) or Minecraft version (e.g. "1.20.1").
|
||||
* An instance of this class provides information related to a protocol version
|
||||
* (the protocol version number and all the corresponding Minecraft versions).
|
||||
*/
|
||||
public class ProtocolVersion implements Comparable<ProtocolVersion> {
|
||||
|
||||
private static final String ONLINE_DATA_URL = "https://api.pandacube.fr/rest/mcversion";
|
||||
|
||||
private static final AtomicReference<MinecraftVersionList> versionList = new AtomicReference<>();
|
||||
|
||||
private static void initIfNecessary() {
|
||||
synchronized (versionList) {
|
||||
if (versionList.get() == null) {
|
||||
init();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the currently used data cache by a new source.
|
||||
* <p>
|
||||
* <b>Note: </b>this method is not meant to be used by the final user of
|
||||
* this class. Use it only if you have a better data source.
|
||||
* @param data the data to use instead of the provided (external API or packaged file)
|
||||
*/
|
||||
public static void setRawData(MinecraftVersionList data) {
|
||||
versionList.set(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the raw data used internally by this class.
|
||||
* <p>
|
||||
* <b>Note: </b>this method is not meant to be used by the final user of
|
||||
* this class. Use it only if you know what you do.
|
||||
* @return the current instance of {@link MinecraftVersionUtil} uses
|
||||
* internally by this class.
|
||||
*/
|
||||
public static MinecraftVersionList getRawData() {
|
||||
initIfNecessary();
|
||||
return versionList.get();
|
||||
}
|
||||
|
||||
private static void init() {
|
||||
// try online source first
|
||||
try {
|
||||
HttpResponse<String> response = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(5))
|
||||
.build()
|
||||
.send(HttpRequest.newBuilder(URI.create(ONLINE_DATA_URL)).build(),
|
||||
BodyHandlers.ofString()
|
||||
);
|
||||
if (response.statusCode() == 200) {
|
||||
MinecraftVersionList data = Json.gson.fromJson(response.body(), MinecraftVersionList.class);
|
||||
versionList.set(data);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.warning(e);
|
||||
}
|
||||
|
||||
if (versionList.get() != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.warning("Unable to get minecraft version data from API. Using local data instead.");
|
||||
// try local source
|
||||
try (InputStream is = ProtocolVersion.class.getResourceAsStream("mcversion.json")) {
|
||||
if (is != null) {
|
||||
try (InputStreamReader isr = new InputStreamReader(is)) {
|
||||
MinecraftVersionList data = Json.gson.fromJson(isr, MinecraftVersionList.class);
|
||||
versionList.set(data);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.warning(e);
|
||||
}
|
||||
|
||||
if (versionList.get() != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.severe("Unable to get Minecraft versions data from classpath. Using empty data instead.");
|
||||
|
||||
versionList.set(new MinecraftVersionList());
|
||||
}
|
||||
|
||||
|
||||
private static int getPVNOfVersion(String version) {
|
||||
initIfNecessary();
|
||||
Integer v = versionList.get().protocolOfVersion().get(version);
|
||||
return v == null ? -1 : v;
|
||||
}
|
||||
|
||||
private static List<String> getVersionsOfPVN(int pvn) {
|
||||
initIfNecessary();
|
||||
return versionList.get().versionsOfProtocol().get(pvn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link ProtocolVersion} associated with the provided Minecraft version.
|
||||
* @param version The Minecraft version, in the format "X.X[.X]" (eg. "1.17" or "1.8.8").
|
||||
* @return an instance of {@link ProtocolVersion}.
|
||||
*/
|
||||
public static ProtocolVersion ofVersion(String version) {
|
||||
int pvn = getPVNOfVersion(version);
|
||||
if (pvn == -1)
|
||||
return null;
|
||||
List<String> versions = getVersionsOfPVN(pvn);
|
||||
if (versions == null) {
|
||||
versions = List.of(version);
|
||||
}
|
||||
return new ProtocolVersion(pvn, List.copyOf(versions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link ProtocolVersion} associated with the provided protocol version number.
|
||||
* @param pvn The protocol version number.
|
||||
* @return an instance of {@link ProtocolVersion}.
|
||||
*/
|
||||
public static ProtocolVersion ofProtocol(int pvn) {
|
||||
List<String> versions = getVersionsOfPVN(pvn);
|
||||
if (versions == null) {
|
||||
return null;
|
||||
}
|
||||
return new ProtocolVersion(pvn, List.copyOf(versions));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns all the {@link ProtocolVersion} currently known by this class.
|
||||
* @return all the {@link ProtocolVersion} currently known by this class.
|
||||
*/
|
||||
public static List<ProtocolVersion> allKnownProtocolVersions() {
|
||||
return versionList.get().versionsOfProtocol().entrySet().stream()
|
||||
.filter(e -> e.getValue() != null && !e.getValue().isEmpty())
|
||||
.map(e -> new ProtocolVersion(e.getKey(), List.copyOf(e.getValue())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate a string representation of the provided list of version, using
|
||||
* {@link StringUtil#joinGrammatically(CharSequence, CharSequence, List)}.
|
||||
*
|
||||
* @param versions the minecraft versions to list
|
||||
* @param finalWordSeparator the word separator between the two last versions in the returned string, like "and",
|
||||
* "or" or any other word of any language. The spaces before and after are already
|
||||
* concatenated.
|
||||
* @return a string representation of the provided list of version.
|
||||
*/
|
||||
public static String displayOptimizedListOfVersions(List<ProtocolVersion> versions, String finalWordSeparator) {
|
||||
return MinecraftVersionUtil.toString(versions.stream().flatMap(pv -> pv.versions.stream()).toList(), finalWordSeparator);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The protocol version number.
|
||||
*/
|
||||
public final int protocolVersionNumber;
|
||||
/**
|
||||
* All Minecraft version supported by this protocol version number.
|
||||
*/
|
||||
public final List<String> versions;
|
||||
|
||||
private ProtocolVersion(int protocolVersionNumber, List<String> versions) {
|
||||
this.protocolVersionNumber = protocolVersionNumber;
|
||||
this.versions = versions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ProtocolVersion{protocol=" + protocolVersionNumber + ", toString(\"and\")=" + toString("and") + "}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of all the Minecraft version of this enum value, using
|
||||
* {@link StringUtil#joinGrammatically(CharSequence, CharSequence, List)}.
|
||||
*
|
||||
* @param finalWordSeparator the word separator between the two last versions in the returned string, like "and",
|
||||
* "or" or any other word of any language. The spaces before and after are already
|
||||
* concatenated.
|
||||
* @return a string representation of this {@link ProtocolVersion}.
|
||||
*/
|
||||
public String toString(String finalWordSeparator) {
|
||||
return displayOptimizedListOfVersions(List.of(this), finalWordSeparator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first (earliest) Minecraft version that supports this protocol version.
|
||||
* @return the first (earliest) Minecraft version that supports this protocol version.
|
||||
*/
|
||||
public String getFirstVersion() {
|
||||
return versions.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last (latest) Minecraft version that supports this protocol version.
|
||||
* @return the last (latest) Minecraft version that supports this protocol version.
|
||||
*/
|
||||
public String getLastVersion() {
|
||||
return versions.get(versions.size() - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof ProtocolVersion pv && protocolVersionNumber == pv.protocolVersionNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return protocolVersionNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull ProtocolVersion o) {
|
||||
return Integer.compare(protocolVersionNumber, o.protocolVersionNumber);
|
||||
}
|
||||
}
|
@@ -2,7 +2,7 @@ package fr.pandacube.lib.core.search;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
@@ -15,7 +15,7 @@ import java.util.concurrent.ExecutionException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Utility class to manage searching among a set of {@link SearchResult} instances, using case insensitive keywords.
|
||||
* Utility class to manage searching among a set of {@link SearchResult} instances, using case-insensitive keywords.
|
||||
* The search engine is responsible for storing a database of entries ({@link SearchResult}) that can be searched using
|
||||
* keywords. This class provides methods to returns a list of results for provided keywords, a list of keyword
|
||||
* suggestions based on pre-typed keywords.
|
||||
|
@@ -0,0 +1,238 @@
|
||||
{
|
||||
"protocolOfVersion": {
|
||||
"1.7.2": 4,
|
||||
"1.7.3": 4,
|
||||
"1.7.4": 4,
|
||||
"1.7.5": 4,
|
||||
"1.7.6": 5,
|
||||
"1.7.7": 5,
|
||||
"1.7.8": 5,
|
||||
"1.7.9": 5,
|
||||
"1.7.10": 5,
|
||||
"1.8": 47,
|
||||
"1.8.1": 47,
|
||||
"1.8.2": 47,
|
||||
"1.8.3": 47,
|
||||
"1.8.4": 47,
|
||||
"1.8.5": 47,
|
||||
"1.8.6": 47,
|
||||
"1.8.7": 47,
|
||||
"1.8.8": 47,
|
||||
"1.8.9": 47,
|
||||
"1.9": 107,
|
||||
"1.9.1": 108,
|
||||
"1.9.2": 109,
|
||||
"1.9.3": 110,
|
||||
"1.9.4": 110,
|
||||
"1.10": 210,
|
||||
"1.10.1": 210,
|
||||
"1.10.2": 210,
|
||||
"1.11": 315,
|
||||
"1.11.1": 316,
|
||||
"1.11.2": 316,
|
||||
"1.12": 335,
|
||||
"1.12.1": 338,
|
||||
"1.12.2": 340,
|
||||
"1.13": 393,
|
||||
"1.13.1": 401,
|
||||
"1.13.2": 404,
|
||||
"1.14": 477,
|
||||
"1.14.1": 480,
|
||||
"1.14.2": 485,
|
||||
"1.14.3": 490,
|
||||
"1.14.4": 498,
|
||||
"1.15": 573,
|
||||
"1.15.1": 575,
|
||||
"1.15.2": 578,
|
||||
"1.16": 735,
|
||||
"1.16.1": 736,
|
||||
"1.16.2": 751,
|
||||
"1.16.3": 753,
|
||||
"1.16.4": 754,
|
||||
"1.16.5": 754,
|
||||
"1.17": 755,
|
||||
"1.17.1": 756,
|
||||
"1.18": 757,
|
||||
"1.18.1": 757,
|
||||
"1.18.2": 758,
|
||||
"1.19": 759,
|
||||
"1.19.1": 760,
|
||||
"1.19.2": 760,
|
||||
"1.19.3": 761,
|
||||
"1.19.4": 762,
|
||||
"1.20": 763,
|
||||
"1.20.1": 763,
|
||||
"1.20.2": 764,
|
||||
"1.20.3": 765,
|
||||
"1.20.4": 765,
|
||||
"1.20.5": 766,
|
||||
"1.20.6": 766,
|
||||
"1.21": 767,
|
||||
"1.21.1": 767,
|
||||
"1.21.2": 768,
|
||||
"1.21.3": 768,
|
||||
"1.21.4": 769
|
||||
},
|
||||
"versionsOfProtocol": {
|
||||
"4": [
|
||||
"1.7.2",
|
||||
"1.7.3",
|
||||
"1.7.4",
|
||||
"1.7.5"
|
||||
],
|
||||
"5": [
|
||||
"1.7.6",
|
||||
"1.7.7",
|
||||
"1.7.8",
|
||||
"1.7.9",
|
||||
"1.7.10"
|
||||
],
|
||||
"47": [
|
||||
"1.8",
|
||||
"1.8.1",
|
||||
"1.8.2",
|
||||
"1.8.3",
|
||||
"1.8.4",
|
||||
"1.8.5",
|
||||
"1.8.6",
|
||||
"1.8.7",
|
||||
"1.8.8",
|
||||
"1.8.9"
|
||||
],
|
||||
"107": [
|
||||
"1.9"
|
||||
],
|
||||
"108": [
|
||||
"1.9.1"
|
||||
],
|
||||
"109": [
|
||||
"1.9.2"
|
||||
],
|
||||
"110": [
|
||||
"1.9.3",
|
||||
"1.9.4"
|
||||
],
|
||||
"210": [
|
||||
"1.10",
|
||||
"1.10.1",
|
||||
"1.10.2"
|
||||
],
|
||||
"315": [
|
||||
"1.11"
|
||||
],
|
||||
"316": [
|
||||
"1.11.1",
|
||||
"1.11.2"
|
||||
],
|
||||
"335": [
|
||||
"1.12"
|
||||
],
|
||||
"338": [
|
||||
"1.12.1"
|
||||
],
|
||||
"340": [
|
||||
"1.12.2"
|
||||
],
|
||||
"393": [
|
||||
"1.13"
|
||||
],
|
||||
"401": [
|
||||
"1.13.1"
|
||||
],
|
||||
"404": [
|
||||
"1.13.2"
|
||||
],
|
||||
"477": [
|
||||
"1.14"
|
||||
],
|
||||
"480": [
|
||||
"1.14.1"
|
||||
],
|
||||
"485": [
|
||||
"1.14.2"
|
||||
],
|
||||
"490": [
|
||||
"1.14.3"
|
||||
],
|
||||
"498": [
|
||||
"1.14.4"
|
||||
],
|
||||
"573": [
|
||||
"1.15"
|
||||
],
|
||||
"575": [
|
||||
"1.15.1"
|
||||
],
|
||||
"578": [
|
||||
"1.15.2"
|
||||
],
|
||||
"735": [
|
||||
"1.16"
|
||||
],
|
||||
"736": [
|
||||
"1.16.1"
|
||||
],
|
||||
"751": [
|
||||
"1.16.2"
|
||||
],
|
||||
"753": [
|
||||
"1.16.3"
|
||||
],
|
||||
"754": [
|
||||
"1.16.4",
|
||||
"1.16.5"
|
||||
],
|
||||
"755": [
|
||||
"1.17"
|
||||
],
|
||||
"756": [
|
||||
"1.17.1"
|
||||
],
|
||||
"757": [
|
||||
"1.18",
|
||||
"1.18.1"
|
||||
],
|
||||
"758": [
|
||||
"1.18.2"
|
||||
],
|
||||
"759": [
|
||||
"1.19"
|
||||
],
|
||||
"760": [
|
||||
"1.19.1",
|
||||
"1.19.2"
|
||||
],
|
||||
"761": [
|
||||
"1.19.3"
|
||||
],
|
||||
"762": [
|
||||
"1.19.4"
|
||||
],
|
||||
"763": [
|
||||
"1.20",
|
||||
"1.20.1"
|
||||
],
|
||||
"764": [
|
||||
"1.20.2"
|
||||
],
|
||||
"765": [
|
||||
"1.20.3",
|
||||
"1.20.4"
|
||||
],
|
||||
"766": [
|
||||
"1.20.5",
|
||||
"1.20.6"
|
||||
],
|
||||
"767": [
|
||||
"1.21",
|
||||
"1.21.1"
|
||||
],
|
||||
"768": [
|
||||
"1.21.2",
|
||||
"1.21.3"
|
||||
],
|
||||
"769": [
|
||||
"1.21.4"
|
||||
]
|
||||
}
|
||||
}
|
@@ -27,7 +27,7 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-dbcp2</artifactId>
|
||||
<version>2.9.0</version>
|
||||
<version>2.12.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<version>3.5.2</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
@@ -56,29 +56,32 @@
|
||||
<artifact>org.apache.commons:commons-dbcp2</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/MANIFEST.MF</exclude>
|
||||
<exclude>META-INF/versions/9/**</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
<filter>
|
||||
<artifact>org.apache.commons:commons-pool2</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/MANIFEST.MF</exclude>
|
||||
<exclude>META-INF/versions/9/**</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
<filter>
|
||||
<artifact>commons-logging:commons-logging</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/MANIFEST.MF</exclude>
|
||||
<exclude>META-INF/versions/9/**</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
<relocations>
|
||||
<relocation>
|
||||
<pattern>org.apache.commons</pattern>
|
||||
<shadedPattern>fr.pandacube.lib.db.shaded.commons</shadedPattern>
|
||||
<pattern>org.apache.commons.dbcp2</pattern>
|
||||
<shadedPattern>fr.pandacube.lib.db.shaded.commons.dbcp2</shadedPattern>
|
||||
</relocation>
|
||||
<relocation>
|
||||
<pattern>org.apache.commons</pattern>
|
||||
<shadedPattern>fr.pandacube.lib.db.shaded.commons</shadedPattern>
|
||||
<pattern>org.apache.commons.pool2</pattern>
|
||||
<shadedPattern>fr.pandacube.lib.db.shaded.commons.pool2</shadedPattern>
|
||||
</relocation>
|
||||
</relocations>
|
||||
<transformers>
|
||||
|
@@ -15,7 +15,7 @@ import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import fr.pandacube.lib.reflect.Reflect;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
|
||||
/**
|
||||
* Static class to handle most of the database operations.
|
||||
@@ -52,10 +52,10 @@ public final class DB {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialialize the table represented by the provided class.
|
||||
* Initialize the table represented by the provided class.
|
||||
* @param elemClass the class representing a table.
|
||||
* @param <E> the type representing the table.
|
||||
* @throws DBInitTableException if the table failed to initialized.
|
||||
* @throws DBInitTableException if the table failed to initialize.
|
||||
*/
|
||||
public static synchronized <E extends SQLElement<E>> void initTable(Class<E> elemClass) throws DBInitTableException {
|
||||
if (connection == null) {
|
||||
@@ -111,7 +111,7 @@ public final class DB {
|
||||
* @param elemClass the class representing a table.
|
||||
* @return a table name.
|
||||
* @param <E> the type representing the table.
|
||||
* @throws DBInitTableException if the provided table had to be initialized and it failed to do so.
|
||||
* @throws DBInitTableException if the provided table had to be initialized and failed to do so.
|
||||
*/
|
||||
public static <E extends SQLElement<E>> String getTableName(Class<E> elemClass) throws DBInitTableException {
|
||||
initTable(elemClass);
|
||||
@@ -130,7 +130,7 @@ public final class DB {
|
||||
* @param elemClass the class representing a table.
|
||||
* @return the {@code id} field of the provided table.
|
||||
* @param <E> the type representing the table.
|
||||
* @throws DBInitTableException if the provided table had to be initialized and it failed to do so.
|
||||
* @throws DBInitTableException if the provided table had to be initialized and failed to do so.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <E extends SQLElement<E>> SQLField<E, Integer> getSQLIdField(Class<E> elemClass) throws DBInitTableException {
|
||||
@@ -226,8 +226,8 @@ public final class DB {
|
||||
* @throws DBException if an error occurs when interacting with the database.
|
||||
*/
|
||||
public static <E extends SQLElement<E>> E getFirst(Class<E> elemClass, SQLWhere<E> where, SQLOrderBy<E> orderBy, Integer offset) throws DBException {
|
||||
SQLElementList<E> elts = getAll(elemClass, where, orderBy, 1, offset);
|
||||
return (elts.size() == 0) ? null : elts.get(0);
|
||||
SQLElementList<E> elements = getAll(elemClass, where, orderBy, 1, offset);
|
||||
return (elements.size() == 0) ? null : elements.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -294,15 +294,15 @@ public final class DB {
|
||||
* @throws DBException if an error occurs when interacting with the database.
|
||||
*/
|
||||
public static <E extends SQLElement<E>> SQLElementList<E> getAll(Class<E> elemClass, SQLWhere<E> where, SQLOrderBy<E> orderBy, Integer limit, Integer offset) throws DBException {
|
||||
SQLElementList<E> elmts = new SQLElementList<>();
|
||||
forEach(elemClass, where, orderBy, limit, offset, elmts::add);
|
||||
return elmts;
|
||||
SQLElementList<E> elements = new SQLElementList<>();
|
||||
forEach(elemClass, where, orderBy, limit, offset, elements::add);
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through all the entries from the provided table.
|
||||
* @param elemClass the class representing a table.
|
||||
* @param action the action to perform on each entries.
|
||||
* @param action the action to perform on each entry.
|
||||
* @param <E> the type representing the table.
|
||||
* @throws DBException if an error occurs when interacting with the database.
|
||||
*/
|
||||
@@ -314,7 +314,7 @@ public final class DB {
|
||||
* Iterate through the entries from the provided table, using the provided {@code WHERE} clause.
|
||||
* @param elemClass the class representing a table.
|
||||
* @param where the {@code WHERE} clause of the query.
|
||||
* @param action the action to perform on each entries.
|
||||
* @param action the action to perform on each entry.
|
||||
* @param <E> the type representing the table.
|
||||
* @throws DBException if an error occurs when interacting with the database.
|
||||
*/
|
||||
@@ -328,7 +328,7 @@ public final class DB {
|
||||
* @param elemClass the class representing a table.
|
||||
* @param where the {@code WHERE} clause of the query.
|
||||
* @param orderBy the {@code ORDER BY} clause of the query.
|
||||
* @param action the action to perform on each entries.
|
||||
* @param action the action to perform on each entry.
|
||||
* @param <E> the type representing the table.
|
||||
* @throws DBException if an error occurs when interacting with the database.
|
||||
*/
|
||||
@@ -343,7 +343,7 @@ public final class DB {
|
||||
* @param where the {@code WHERE} clause of the query.
|
||||
* @param orderBy the {@code ORDER BY} clause of the query.
|
||||
* @param limit the {@code LIMIT} clause of the query.
|
||||
* @param action the action to perform on each entries.
|
||||
* @param action the action to perform on each entry.
|
||||
* @param <E> the type representing the table.
|
||||
* @throws DBException if an error occurs when interacting with the database.
|
||||
*/
|
||||
@@ -359,7 +359,7 @@ public final class DB {
|
||||
* @param orderBy the {@code ORDER BY} clause of the query.
|
||||
* @param limit the {@code LIMIT} clause of the query.
|
||||
* @param offset the {@code OFFSET} clause of the query.
|
||||
* @param action the action to perform on each entries.
|
||||
* @param action the action to perform on each entry.
|
||||
* @param <E> the type representing the table.
|
||||
* @throws DBException if an error occurs when interacting with the database.
|
||||
*/
|
||||
@@ -577,7 +577,7 @@ public final class DB {
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <E extends SQLElement<E>> E getElementInstance(ResultSet set, Class<E> elemClass) throws DBException {
|
||||
try {
|
||||
E instance = Reflect.ofClass(elemClass).constructor(int.class).instanciate(set.getInt("id"));
|
||||
E instance = Reflect.ofClass(elemClass).constructor(int.class).instantiate(set.getInt("id"));
|
||||
|
||||
int fieldCount = set.getMetaData().getColumnCount();
|
||||
|
||||
@@ -623,7 +623,7 @@ public final class DB {
|
||||
|
||||
return instance;
|
||||
} catch (ReflectiveOperationException | IllegalArgumentException | SecurityException | SQLException e) {
|
||||
throw new DBException("Can't instanciate " + elemClass.getName(), e);
|
||||
throw new DBException("Can't instantiate " + elemClass.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -23,7 +23,8 @@ public class DBConnection {
|
||||
public DBConnection(String host, int port, String dbname, String login, String password) {
|
||||
this("jdbc:mysql://" + host + ":" + port + "/" + dbname
|
||||
+ "?useUnicode=true"
|
||||
+ "&useSSL=false"
|
||||
+ "&sslMode=DISABLED"
|
||||
+ "&allowPublicKeyRetrieval=true"
|
||||
+ "&characterEncoding=utf8"
|
||||
+ "&characterSetResults=utf8"
|
||||
+ "&character_set_server=utf8mb4"
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package fr.pandacube.lib.db;
|
||||
|
||||
/**
|
||||
* Exception thrown when something bad happends when using the {@link DB} API.
|
||||
* Exception thrown when something bad happens when using the {@link DB} API.
|
||||
*/
|
||||
public class DBException extends Exception {
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package fr.pandacube.lib.db;
|
||||
|
||||
/**
|
||||
* Exception thrown when something bad happends when initializing a new table using {@link DB#initTable(Class)}.
|
||||
* Exception thrown when something bad happens when initializing a new table using {@link DB#initTable(Class)}.
|
||||
*/
|
||||
public class DBInitTableException extends DBException {
|
||||
|
||||
|
@@ -26,7 +26,7 @@ import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import fr.pandacube.lib.util.EnumUtil;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
|
||||
/**
|
||||
* Represents an entry in a SQL table. Each subclass is for a specific table.
|
||||
@@ -119,7 +119,7 @@ public abstract class SQLElement<E extends SQLElement<E>> {
|
||||
|
||||
/**
|
||||
* Gets the name of the table in the database, without the prefix defined by {@link DB#init(DBConnection, String)}.
|
||||
* @return The unprefixed name of the table in the database.
|
||||
* @return The non-prefixed name of the table in the database.
|
||||
*/
|
||||
protected abstract String tableName();
|
||||
|
||||
@@ -133,7 +133,7 @@ public abstract class SQLElement<E extends SQLElement<E>> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the values of this entry that are known to be nullable or have a default value.
|
||||
* Fills the entries values that are known to be nullable or have a default value.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void initDefaultValues() {
|
||||
@@ -193,7 +193,7 @@ public abstract class SQLElement<E extends SQLElement<E>> {
|
||||
/**
|
||||
* Sets a value in this entry.
|
||||
* <p>
|
||||
* This is not good practice to set the {@code id} field of any entry, because it’s an unique auto-incremented
|
||||
* This is not good practice to set the {@code id} field of any entry, because it’s a unique auto-incremented
|
||||
* value. Use {@link #save()} and {@link #delete()} to set or unset the {@code id} instead, in consistence with the
|
||||
* database.
|
||||
* @param field the field to set.
|
||||
@@ -241,25 +241,32 @@ public abstract class SQLElement<E extends SQLElement<E>> {
|
||||
* Gets the value of the provided field in this entry.
|
||||
* @param field the field to get the value from.
|
||||
* @return the value of the provided field in this entry.
|
||||
* @throws IllegalArgumentException if the provided field is null or not from the table represented by this class.
|
||||
* @throws IllegalStateException if the field is not nullable and there is no value set
|
||||
* @param <T> the Java type of the field.
|
||||
*/
|
||||
public <T> T get(SQLField<E, T> field) {
|
||||
if (field == null) throw new IllegalArgumentException("field can't be null");
|
||||
if (field == null)
|
||||
throw new IllegalArgumentException("field can't be null");
|
||||
if (!fields.containsKey(field.getName()) || !fields.get(field.getName()).equals(field))
|
||||
throw new IllegalArgumentException("The provided field " + field + " is not from this table " + getClass().getName());
|
||||
if (values.containsKey(field)) {
|
||||
@SuppressWarnings("unchecked")
|
||||
T val = (T) values.get(field);
|
||||
return val;
|
||||
}
|
||||
throw new IllegalArgumentException("The field '" + field.getName() + "' in this instance of " + getClass().getName()
|
||||
+ " does not exist or is not set");
|
||||
if (field.nullable)
|
||||
return null;
|
||||
throw new IllegalStateException("The non-nullable field '" + field.getName() + "' in this instance of " + getClass().getName()
|
||||
+ " is not set");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the foreign table entry targeted by the provided foreignkey of this table.
|
||||
* @param field a foreignkey of this table.
|
||||
* @param <T> the type of the foreignkey field.
|
||||
* Gets the foreign table entry targeted by the provided foreign key of this table.
|
||||
* @param field a foreign key of this table.
|
||||
* @param <T> the type of the foreign key field.
|
||||
* @param <P> the targeted foreign table type.
|
||||
* @return the foreign table entry targeted by the provided foreignkey of this table.
|
||||
* @return the foreign table entry targeted by the provided foreign key of this table.
|
||||
* @throws DBException if an error occurs when interacting with the database.
|
||||
*/
|
||||
public <T, P extends SQLElement<P>> P getReferencedEntry(SQLFKField<E, T, P> field) throws DBException {
|
||||
@@ -271,11 +278,11 @@ public abstract class SQLElement<E extends SQLElement<E>> {
|
||||
/**
|
||||
* Gets the original table entry which the provided foreign key is targeting this entry, and following the provided
|
||||
* {@code ORDER BY}, {@code LIMIT} and {@code OFFSET} clauses.
|
||||
* @param field a foreignkey in the original table.
|
||||
* @param field a foreign key in the original table.
|
||||
* @param orderBy the {@code ORDER BY} clause of the query.
|
||||
* @param limit the {@code LIMIT} clause of the query.
|
||||
* @param offset the {@code OFFSET} clause of the query.
|
||||
* @param <T> the type of the foreignkey field.
|
||||
* @param <T> the type of the foreign key field.
|
||||
* @param <F> the table class of the foreign key that reference a field of this entry.
|
||||
* @return the original table entry which the provided foreign key is targeting this entry.
|
||||
* @throws DBException if an error occurs when interacting with the database.
|
||||
@@ -314,7 +321,7 @@ public abstract class SQLElement<E extends SQLElement<E>> {
|
||||
|
||||
/**
|
||||
* Saves this entry into the database, either by updating the already existing entry in it, or by creating a new
|
||||
* entry if it doesn’t exist yet.
|
||||
* entry if it doesn't exist yet.
|
||||
* @return this.
|
||||
* @throws DBException if an error occurs when interacting with the database.
|
||||
*/
|
||||
@@ -354,7 +361,7 @@ public abstract class SQLElement<E extends SQLElement<E>> {
|
||||
first = false;
|
||||
concatValues.append(" ? ");
|
||||
concatFields.append("`").append(entry.getKey().getName()).append("`");
|
||||
addValueToSQLObjectList(psValues, entry.getKey(), entry.getValue());
|
||||
psValues.add(entry.getKey().fromJavaTypeToJDBCType(entry.getValue()));
|
||||
}
|
||||
|
||||
try (Connection c = db.getConnection();
|
||||
@@ -382,20 +389,6 @@ public abstract class SQLElement<E extends SQLElement<E>> {
|
||||
return (E) this;
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
/* package */ static <E extends SQLElement<E>> void addValueToSQLObjectList(List<Object> list, SQLField<E, ?> field, Object jValue) throws DBException {
|
||||
if (jValue != null && field.type instanceof SQLCustomType) {
|
||||
try {
|
||||
jValue = ((SQLCustomType)field.type).javaToDbConv.apply(jValue);
|
||||
} catch (Exception e) {
|
||||
throw new DBException("Error while converting value of field '"+field.getName()+"' with SQLCustomType from "+field.type.getJavaType()
|
||||
+"(java source) to "+((SQLCustomType<?, ?>)field.type).intermediateJavaType+"(jdbc destination). The original value is '"+jValue+"'", e);
|
||||
}
|
||||
}
|
||||
list.add(jValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if this entry is currently stored in DB or not.
|
||||
* @return true if this entry is currently stored in DB, or false otherwise.
|
||||
@@ -474,14 +467,14 @@ public abstract class SQLElement<E extends SQLElement<E>> {
|
||||
* Creates a new SQL field.
|
||||
* @param type the type of the field.
|
||||
* @param nullable true if nullable, false if {@code NOT NULL}.
|
||||
* @param autoIncr if {@code AUTO_INCREMENT}.
|
||||
* @param autoIncrement if {@code AUTO_INCREMENT}.
|
||||
* @param deflt a default value for this field. A null value indicate that this has no default value.
|
||||
* @return the new SQL field.
|
||||
* @param <E> the table type.
|
||||
* @param <T> the Java type of this field.
|
||||
*/
|
||||
protected static <E extends SQLElement<E>, T> SQLField<E, T> field(SQLType<T> type, boolean nullable, boolean autoIncr, T deflt) {
|
||||
return new SQLField<>(type, nullable, autoIncr, deflt);
|
||||
protected static <E extends SQLElement<E>, T> SQLField<E, T> field(SQLType<T> type, boolean nullable, boolean autoIncrement, T deflt) {
|
||||
return new SQLField<>(type, nullable, autoIncrement, deflt);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -500,13 +493,13 @@ public abstract class SQLElement<E extends SQLElement<E>> {
|
||||
* Creates a new SQL field.
|
||||
* @param type the type of the field.
|
||||
* @param nullable true if nullable, false if {@code NOT NULL}.
|
||||
* @param autoIncr if {@code AUTO_INCREMENT}.
|
||||
* @param autoIncrement if {@code AUTO_INCREMENT}.
|
||||
* @return the new SQL field.
|
||||
* @param <E> the table type.
|
||||
* @param <T> the Java type of this field.
|
||||
*/
|
||||
protected static <E extends SQLElement<E>, T> SQLField<E, T> field(SQLType<T> type, boolean nullable, boolean autoIncr) {
|
||||
return new SQLField<>(type, nullable, autoIncr);
|
||||
protected static <E extends SQLElement<E>, T> SQLField<E, T> field(SQLType<T> type, boolean nullable, boolean autoIncrement) {
|
||||
return new SQLField<>(type, nullable, autoIncrement);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -16,6 +16,11 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> {
|
||||
|
||||
/**
|
||||
* Creates an empty list of sql elements.
|
||||
*/
|
||||
public SQLElementList() {}
|
||||
|
||||
/**
|
||||
* Stores all the values modified by {@link #setCommon(SQLField, Object)}.
|
||||
*/
|
||||
@@ -46,7 +51,7 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> {
|
||||
E emptyElement = elemClass.getConstructor().newInstance();
|
||||
emptyElement.set(field, value, false);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("Illegal field or value or can't instanciante an empty instance of "
|
||||
throw new IllegalArgumentException("Illegal field or value or can't instantiate an empty instance of "
|
||||
+ elemClass.getName() + ". (the instance is only created to test validity of field and value)", e);
|
||||
}
|
||||
|
||||
@@ -83,7 +88,7 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void applyNewValuesToElements(List<E> storedEl) {
|
||||
// applique les valeurs dans chaques objets de la liste
|
||||
// applique les valeurs dans chaque objet de la liste
|
||||
for (E el : storedEl) {
|
||||
for (@SuppressWarnings("rawtypes") SQLField entry : modifiedValues.keySet()) {
|
||||
if (!el.isModified(entry)) {
|
||||
@@ -100,7 +105,7 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> {
|
||||
/**
|
||||
* Removes all the entries of this list from the database.
|
||||
* This method has the same effect as calling the {@link SQLElement#delete()} method individually on each element,
|
||||
* but with only one SQL query to delete all of the entries.
|
||||
* but with only one SQL query to delete all the entries.
|
||||
* <p>
|
||||
* If you intend to remove the entries from the database just after fetching them, call directly the
|
||||
* {@link DB#delete(Class, SQLWhere)} method instead.
|
||||
@@ -124,9 +129,9 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> {
|
||||
|
||||
/**
|
||||
* Get all the entries targeted by the foreign key of all the entries in this list.
|
||||
* @param foreignKey a foreignkey of this table.
|
||||
* @param foreignKey a foreign key of this table.
|
||||
* @param orderBy the {@code ORDER BY} clause of the query.
|
||||
* @return a list of foreign table entries targeted by the provided foreignkey of this table.
|
||||
* @return a list of foreign table entries targeted by the provided foreign key of this table.
|
||||
* @param <T> the field’s Java type.
|
||||
* @param <P> the target table type.
|
||||
* @throws DBException if an error occurs when interacting with the database.
|
||||
@@ -144,7 +149,7 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> {
|
||||
|
||||
/**
|
||||
* Get all the entries targeted by the foreign key of all the entries in this list, mapped from the foreign key value.
|
||||
* @param foreignKey a foreignkey of this table.
|
||||
* @param foreignKey a foreign key of this table.
|
||||
* @return a map of the foreign key values, mapped to the foreign table’s entries.
|
||||
* @param <T> the field’s Java type.
|
||||
* @param <P> the target table type.
|
||||
@@ -163,11 +168,11 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> {
|
||||
/**
|
||||
* Gets all the original table’s entries which the provided foreign key is targeting the entries of this list, and
|
||||
* following the provided {@code ORDER BY}, {@code LIMIT} and {@code OFFSET} clauses.
|
||||
* @param foreignKey a foreignkey in the original table.
|
||||
* @param foreignKey a foreign key in the original table.
|
||||
* @param orderBy the {@code ORDER BY} clause of the query.
|
||||
* @param limit the {@code LIMIT} clause of the query.
|
||||
* @param offset the {@code OFFSET} clause of the query.
|
||||
* @param <T> the type of the foreignkey field.
|
||||
* @param <T> the type of the foreign key field.
|
||||
* @param <F> the table class of the foreign key that reference a field of this entry.
|
||||
* @return the original table’s entries which the provided foreign key is targeting the entries of this list.
|
||||
* @throws DBException if an error occurs when interacting with the database.
|
||||
@@ -187,11 +192,11 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> {
|
||||
* Gets all the original table’s entries which the provided foreign key is targeting the entries of this list,
|
||||
* following the provided {@code ORDER BY}, {@code LIMIT} and {@code OFFSET} clauses, and mapped from the foreign
|
||||
* key value.
|
||||
* @param foreignKey a foreignkey in the original table.
|
||||
* @param foreignKey a foreign key in the original table.
|
||||
* @param orderBy the {@code ORDER BY} clause of the query.
|
||||
* @param limit the {@code LIMIT} clause of the query.
|
||||
* @param offset the {@code OFFSET} clause of the query.
|
||||
* @param <T> the type of the foreignkey field.
|
||||
* @param <T> the type of the foreign key field.
|
||||
* @param <F> the table class of the foreign key that reference a field of this entry.
|
||||
* @return a map of the foreign key values, mapped to the orignal table’s entries.
|
||||
* @throws DBException if an error occurs when interacting with the database.
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package fr.pandacube.lib.db;
|
||||
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
|
||||
/**
|
||||
* A foreign key field in a SQL table.
|
||||
@@ -28,7 +28,7 @@ public class SQLFKField<F extends SQLElement<F>, T, P extends SQLElement<P>> ext
|
||||
SQLField<F, Integer> f = DB.getSQLIdField(fkEl);
|
||||
return new SQLFKField<>(f.type, nul, deflt, fkEl, f);
|
||||
} catch (DBInitTableException e) {
|
||||
Log.severe("Can't create Foreign key Field targetting id field of '"+fkEl+"'", e);
|
||||
Log.severe("Can't create Foreign key Field targeting id field of '"+fkEl+"'", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public class SQLFKField<F extends SQLElement<F>, T, P extends SQLElement<P>> ext
|
||||
}
|
||||
|
||||
if (fkF.getSQLElementType() == null)
|
||||
throw new RuntimeException("Can't initialize foreign key. The primary key in the table " + fkEl.getName() + " is not properly initialized and can't be targetted by a forein key");
|
||||
throw new RuntimeException("Can't initialize foreign key. The primary key in the table " + fkEl.getName() + " is not properly initialized and can't be targeted by a foreign key");
|
||||
sqlPrimaryKeyField = fkF;
|
||||
sqlForeignKeyElemClass = fkEl;
|
||||
}
|
||||
|
@@ -24,10 +24,10 @@ public class SQLField<E extends SQLElement<E>, T> {
|
||||
/* package */ final boolean autoIncrement;
|
||||
/* package */ final T defaultValue;
|
||||
|
||||
/* package */ SQLField(SQLType<T> type, boolean nullable, boolean autoIncr, T deflt) {
|
||||
/* package */ SQLField(SQLType<T> type, boolean nullable, boolean autoIncrement, T deflt) {
|
||||
this.type = type;
|
||||
this.nullable = nullable;
|
||||
autoIncrement = autoIncr;
|
||||
this.autoIncrement = autoIncrement;
|
||||
defaultValue = deflt;
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ public class SQLField<E extends SQLElement<E>, T> {
|
||||
this(type, nullable, false, null);
|
||||
}
|
||||
|
||||
/* package */ SQLField(SQLType<T> type, boolean nullable, boolean autoIncr) {
|
||||
this(type, nullable, autoIncr, null);
|
||||
/* package */ SQLField(SQLType<T> type, boolean nullable, boolean autoIncrement) {
|
||||
this(type, nullable, autoIncrement, null);
|
||||
}
|
||||
|
||||
/* package */ SQLField(SQLType<T> type, boolean nullable, T deflt) {
|
||||
@@ -133,7 +133,7 @@ public class SQLField<E extends SQLElement<E>, T> {
|
||||
/**
|
||||
* Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code =} operator.
|
||||
* @param r the value to compare with.
|
||||
* @return a SQL {@code WHERE} expression.
|
||||
* @return a new SQL {@code WHERE} expression.
|
||||
*/
|
||||
public fr.pandacube.lib.db.SQLWhere<E> eq(T r) {
|
||||
return comp(SQLComparator.EQ, r);
|
||||
@@ -142,7 +142,7 @@ public class SQLField<E extends SQLElement<E>, T> {
|
||||
/**
|
||||
* Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code >=} operator.
|
||||
* @param r the value to compare with.
|
||||
* @return a SQL {@code WHERE} expression.
|
||||
* @return a new SQL {@code WHERE} expression.
|
||||
*/
|
||||
public fr.pandacube.lib.db.SQLWhere<E> geq(T r) {
|
||||
return comp(SQLComparator.GEQ, r);
|
||||
@@ -151,7 +151,7 @@ public class SQLField<E extends SQLElement<E>, T> {
|
||||
/**
|
||||
* Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code >} operator.
|
||||
* @param r the value to compare with.
|
||||
* @return a SQL {@code WHERE} expression.
|
||||
* @return a new SQL {@code WHERE} expression.
|
||||
*/
|
||||
public fr.pandacube.lib.db.SQLWhere<E> gt(T r) {
|
||||
return comp(SQLComparator.GT, r);
|
||||
@@ -160,7 +160,7 @@ public class SQLField<E extends SQLElement<E>, T> {
|
||||
/**
|
||||
* Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code <=} operator.
|
||||
* @param r the value to compare with.
|
||||
* @return a SQL {@code WHERE} expression.
|
||||
* @return a new SQL {@code WHERE} expression.
|
||||
*/
|
||||
public fr.pandacube.lib.db.SQLWhere<E> leq(T r) {
|
||||
return comp(SQLComparator.LEQ, r);
|
||||
@@ -169,7 +169,7 @@ public class SQLField<E extends SQLElement<E>, T> {
|
||||
/**
|
||||
* Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code <} operator.
|
||||
* @param r the value to compare with.
|
||||
* @return a SQL {@code WHERE} expression.
|
||||
* @return a new SQL {@code WHERE} expression.
|
||||
*/
|
||||
public fr.pandacube.lib.db.SQLWhere<E> lt(T r) {
|
||||
return comp(SQLComparator.LT, r);
|
||||
@@ -178,7 +178,7 @@ public class SQLField<E extends SQLElement<E>, T> {
|
||||
/**
|
||||
* Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code !=} operator.
|
||||
* @param r the value to compare with.
|
||||
* @return a SQL {@code WHERE} expression.
|
||||
* @return a new SQL {@code WHERE} expression.
|
||||
*/
|
||||
public fr.pandacube.lib.db.SQLWhere<E> neq(T r) {
|
||||
return comp(SQLComparator.NEQ, r);
|
||||
@@ -194,7 +194,7 @@ public class SQLField<E extends SQLElement<E>, T> {
|
||||
* Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code LIKE}
|
||||
* keyword.
|
||||
* @param like the value to compare with.
|
||||
* @return a SQL {@code WHERE} expression.
|
||||
* @return a new SQL {@code WHERE} expression.
|
||||
*/
|
||||
public fr.pandacube.lib.db.SQLWhere<E> like(String like) {
|
||||
return new SQLWhereLike<>(this, like);
|
||||
@@ -206,7 +206,7 @@ public class SQLField<E extends SQLElement<E>, T> {
|
||||
* Create a SQL {@code WHERE} expression testing the presence of this field in the provided collection of value
|
||||
* using the {@code IN} keyword.
|
||||
* @param v the value to compare with.
|
||||
* @return a SQL {@code WHERE} expression.
|
||||
* @return a new SQL {@code WHERE} expression.
|
||||
*/
|
||||
public fr.pandacube.lib.db.SQLWhere<E> in(Collection<T> v) {
|
||||
return new SQLWhereIn<>(this, v);
|
||||
@@ -216,7 +216,7 @@ public class SQLField<E extends SQLElement<E>, T> {
|
||||
|
||||
/**
|
||||
* Create a SQL {@code WHERE} expression testing the nullity of this field using the {@code IS NULL} keyword.
|
||||
* @return a SQL {@code WHERE} expression.
|
||||
* @return a new SQL {@code WHERE} expression.
|
||||
*/
|
||||
public fr.pandacube.lib.db.SQLWhere<E> isNull() {
|
||||
return new SQLWhereNull<>(this, true);
|
||||
@@ -225,10 +225,35 @@ public class SQLField<E extends SQLElement<E>, T> {
|
||||
/**
|
||||
* Create a SQL {@code WHERE} expression testing the non-nullity of this field using the {@code IS NOT NULL}
|
||||
* keyword.
|
||||
* @return a SQL {@code WHERE} expression.
|
||||
* @return a new SQL {@code WHERE} expression.
|
||||
*/
|
||||
public fr.pandacube.lib.db.SQLWhere<E> isNotNull() {
|
||||
return new SQLWhereNull<>(this, false);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
/* package */ Object fromJavaTypeToJDBCType(Object value) throws DBException {
|
||||
Object ret = value;
|
||||
if (value != null && type instanceof SQLCustomType customType) {
|
||||
try {
|
||||
ret = customType.javaToDbConv.apply(value);
|
||||
} catch (Exception e) {
|
||||
throw new DBException("Error while converting value of field '" + name + "' with SQLCustomType from " + type.getJavaType()
|
||||
+ "(java source) to " + customType.intermediateJavaType + "(jdbc destination). The original value is '" + value + "'", e);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* package */ Collection<Object> fromListJavaTypeToJDBCType(Collection<?> values) throws DBException {
|
||||
if (values == null)
|
||||
return null;
|
||||
List<Object> ret = new ArrayList<>(values.size());
|
||||
for (Object value : values) {
|
||||
ret.add(fromJavaTypeToJDBCType(value));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
|
||||
/**
|
||||
* Builder for a SQL {@code UPDATE} query.
|
||||
@@ -74,7 +74,7 @@ public class SQLUpdateBuilder<E extends SQLElement<E>> {
|
||||
if (!first)
|
||||
sql.append(", ");
|
||||
sql.append("`").append(entry.getKey().getName()).append("` = ? ");
|
||||
SQLElement.addValueToSQLObjectList(params, entry.getKey(), entry.getValue());
|
||||
params.add(entry.getKey().fromJavaTypeToJDBCType(entry.getValue()));
|
||||
first = false;
|
||||
}
|
||||
|
||||
|
@@ -3,15 +3,21 @@ package fr.pandacube.lib.db;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
|
||||
/**
|
||||
* A SQL {@code WHERE} expression.
|
||||
* SQL {@code WHERE} expression.
|
||||
* @param <E> the table type.
|
||||
*/
|
||||
public abstract class SQLWhere<E extends SQLElement<E>> {
|
||||
|
||||
/**
|
||||
* Creates a SQL WHERE expression.
|
||||
*/
|
||||
protected SQLWhere() {}
|
||||
|
||||
/* package */ abstract ParameterizedSQLString toSQL() throws DBException;
|
||||
|
||||
@Override
|
||||
@@ -29,7 +35,7 @@ public abstract class SQLWhere<E extends SQLElement<E>> {
|
||||
* Create a SQL {@code WHERE} expression that is true when this expression {@code AND} the provided expression is
|
||||
* true.
|
||||
* @param other the other expression.
|
||||
* @return a SQL {@code WHERE} expression.
|
||||
* @return a new SQL {@code WHERE} expression.
|
||||
*/
|
||||
public SQLWhere<E> and(SQLWhere<E> other) {
|
||||
return SQLWhere.<E>and().and(this).and(other);
|
||||
@@ -39,7 +45,7 @@ public abstract class SQLWhere<E extends SQLElement<E>> {
|
||||
* Create a SQL {@code WHERE} expression that is true when this expression {@code OR} the provided expression is
|
||||
* true.
|
||||
* @param other the other expression.
|
||||
* @return a SQL {@code WHERE} expression.
|
||||
* @return a new SQL {@code WHERE} expression.
|
||||
*/
|
||||
public SQLWhere<E> or(SQLWhere<E> other) {
|
||||
return SQLWhere.<E>or().or(this).or(other);
|
||||
@@ -48,7 +54,7 @@ public abstract class SQLWhere<E extends SQLElement<E>> {
|
||||
|
||||
/**
|
||||
* Create a SQL {@code WHERE} expression builder joining multiple expressions with the {@code AND} operator.
|
||||
* @return a SQL {@code WHERE} expression.
|
||||
* @return a new SQL {@code WHERE} expression.
|
||||
* @param <E> the table type.
|
||||
*/
|
||||
public static <E extends SQLElement<E>> SQLWhereAndBuilder<E> and() {
|
||||
@@ -57,13 +63,63 @@ public abstract class SQLWhere<E extends SQLElement<E>> {
|
||||
|
||||
/**
|
||||
* Create a SQL {@code WHERE} expression builder joining multiple expressions with the {@code OR} operator.
|
||||
* @return a SQL {@code WHERE} expression.
|
||||
* @return a new SQL {@code WHERE} expression.
|
||||
* @param <E> the table type.
|
||||
*/
|
||||
public static <E extends SQLElement<E>> SQLWhereOrBuilder<E> or() {
|
||||
return new SQLWhereOrBuilder<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a custom SQL {@code WHERE} expression.
|
||||
* @param whereExpr the raw SQL {@code WHERE} expression.
|
||||
* @return a new SQL {@code WHERE} expression.
|
||||
* @param <E> the table type.
|
||||
*/
|
||||
public static <E extends SQLElement<E>> SQLWhere<E> expression(String whereExpr) {
|
||||
return expression(whereExpr, List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a custom SQL {@code WHERE} expression.
|
||||
* @param whereExpr the raw SQL {@code WHERE} expression.
|
||||
* @param params the parameters of the provided expression.
|
||||
* @return a new SQL {@code WHERE} expression.
|
||||
* @param <E> the table type.
|
||||
*/
|
||||
public static <E extends SQLElement<E>> SQLWhere<E> expression(String whereExpr, List<Object> params) {
|
||||
return new SQLWhereCustomExpression<>(whereExpr, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a SQL {@code WHERE ... IN ...} expression with a custom left operand.
|
||||
* @param leftExpr the raw SQL left operand.
|
||||
* @param valuesIn the values on the right of the {@code IN} operator.
|
||||
* @return a new SQL {@code WHERE} expression.
|
||||
* @param <E> the table type.
|
||||
*/
|
||||
public static <E extends SQLElement<E>> SQLWhere<E> expressionIn(String leftExpr, Collection<?> valuesIn) {
|
||||
return expressionIn(leftExpr, List.of(), valuesIn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a SQL {@code WHERE ... IN ...} expression with a custom left operand.
|
||||
* @param leftExpr the raw SQL left operand.
|
||||
* @param leftParams the parameters of the left operand.
|
||||
* @param valuesIn the values on the right of the {@code IN} operator.
|
||||
* @return a new SQL {@code WHERE} expression.
|
||||
* @param <E> the table type.
|
||||
*/
|
||||
public static <E extends SQLElement<E>> SQLWhere<E> expressionIn(String leftExpr, List<Object> leftParams, Collection<?> valuesIn) {
|
||||
return new SQLWhereInCustom<>(leftExpr, leftParams, valuesIn);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A SQL {@code WHERE} expression builder joining multiple expressions with the {@code AND} or {@code OR} operator.
|
||||
@@ -207,9 +263,8 @@ public abstract class SQLWhere<E extends SQLElement<E>> {
|
||||
|
||||
@Override
|
||||
/* package */ ParameterizedSQLString toSQL() throws DBException {
|
||||
List<Object> params = new ArrayList<>();
|
||||
SQLElement.addValueToSQLObjectList(params, left, right);
|
||||
return new ParameterizedSQLString("`" + left.getName() + "` " + comp.sql + " ? ", params);
|
||||
return new ParameterizedSQLString("`" + left.getName() + "` " + comp.sql + " ? ",
|
||||
List.of(left.fromJavaTypeToJDBCType(right)));
|
||||
}
|
||||
|
||||
/* package */ enum SQLComparator {
|
||||
@@ -241,33 +296,39 @@ public abstract class SQLWhere<E extends SQLElement<E>> {
|
||||
|
||||
|
||||
|
||||
/* package */ static class SQLWhereIn<E extends SQLElement<E>> extends SQLWhere<E> {
|
||||
/* package */ static class SQLWhereInCustom<E extends SQLElement<E>> extends SQLWhere<E> {
|
||||
|
||||
private final SQLField<E, ?> field;
|
||||
private final Collection<?> values;
|
||||
private final String leftExpression;
|
||||
private final List<Object> leftExpressionParameters;
|
||||
protected Collection<?> collectionIn;
|
||||
|
||||
/* package */ <T> SQLWhereIn(SQLField<E, T> f, Collection<T> v) {
|
||||
if (f == null || v == null)
|
||||
throw new IllegalArgumentException("All arguments for SQLWhereIn constructor can't be null");
|
||||
field = f;
|
||||
values = v;
|
||||
/* package */ <T> SQLWhereInCustom(String leftExpr, List<Object> leftExprParams, Collection<T> collectionIn) {
|
||||
if (leftExpr == null)
|
||||
throw new IllegalArgumentException("leftExpr can't be null");
|
||||
if (leftExprParams == null)
|
||||
leftExprParams = List.of();
|
||||
if (collectionIn == null)
|
||||
collectionIn = List.of();
|
||||
leftExpression = leftExpr;
|
||||
leftExpressionParameters = leftExprParams;
|
||||
this.collectionIn = collectionIn;
|
||||
}
|
||||
|
||||
@Override
|
||||
/* package */ ParameterizedSQLString toSQL() throws DBException {
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
if (values.isEmpty())
|
||||
if (collectionIn.isEmpty())
|
||||
return new ParameterizedSQLString(" 1=0 ", params);
|
||||
|
||||
for (Object v : values)
|
||||
SQLElement.addValueToSQLObjectList(params, field, v);
|
||||
params.addAll(leftExpressionParameters);
|
||||
params.addAll(collectionIn);
|
||||
|
||||
char[] questions = new char[values.size() == 0 ? 0 : (values.size() * 2 - 1)];
|
||||
char[] questions = new char[collectionIn.size() * 2 - 1];
|
||||
for (int i = 0; i < questions.length; i++)
|
||||
questions[i] = i % 2 == 0 ? '?' : ',';
|
||||
|
||||
return new ParameterizedSQLString("`" + field.getName() + "` IN (" + new String(questions) + ") ", params);
|
||||
return new ParameterizedSQLString("(" + leftExpression + ") IN (" + new String(questions) + ") ", params);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -277,6 +338,32 @@ public abstract class SQLWhere<E extends SQLElement<E>> {
|
||||
|
||||
|
||||
|
||||
/* package */ static class SQLWhereIn<E extends SQLElement<E>> extends SQLWhereInCustom<E> {
|
||||
|
||||
private final SQLField<E, ?> field;
|
||||
private boolean collectionFiltered = false;
|
||||
|
||||
/* package */ <T> SQLWhereIn(SQLField<E, T> f, Collection<T> v) {
|
||||
super("`" + Objects.requireNonNull(f).getName() + "`", List.of(), v);
|
||||
field = f;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
ParameterizedSQLString toSQL() throws DBException {
|
||||
if (!collectionFiltered) {
|
||||
collectionIn = field.fromListJavaTypeToJDBCType(collectionIn);
|
||||
collectionFiltered = true;
|
||||
}
|
||||
return super.toSQL();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* package */ static class SQLWhereLike<E extends SQLElement<E>> extends SQLWhere<E> {
|
||||
@@ -334,6 +421,33 @@ public abstract class SQLWhere<E extends SQLElement<E>> {
|
||||
|
||||
|
||||
|
||||
/* package */ static class SQLWhereCustomExpression<E extends SQLElement<E>> extends SQLWhere<E> {
|
||||
|
||||
private final String sqlExpression;
|
||||
private final List<Object> parameters;
|
||||
|
||||
/* package */ SQLWhereCustomExpression(String sqlExpression, List<Object> parameters) {
|
||||
if (sqlExpression == null)
|
||||
throw new IllegalArgumentException("sqlExpression can't be null");
|
||||
if (parameters == null)
|
||||
parameters = List.of();
|
||||
this.sqlExpression = sqlExpression;
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
/* package */ ParameterizedSQLString toSQL() {
|
||||
return new ParameterizedSQLString(sqlExpression, parameters);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
@@ -1,12 +0,0 @@
|
||||
# pandalib-net
|
||||
|
||||
A TCP network library that uses the standard Java socket API, to ease the ommunication between the different processes
|
||||
running the server network Pandacube.
|
||||
|
||||
It’s still in development (actually not touched since years), and it’s supposed to be a replacement for the old
|
||||
`pandalib-netapi`. This module is then marked as Beta using the Google Guava annotation.
|
||||
|
||||
- Packet based communication
|
||||
- Supports Request/Answer packets
|
||||
- Uses binary packet id and data
|
||||
* Input streams are handled in separate Threads
|
@@ -1,65 +0,0 @@
|
||||
package fr.pandacube.lib.net;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.google.common.annotations.Beta;
|
||||
|
||||
@Beta
|
||||
public class Array8Bit {
|
||||
|
||||
public static final int BIT_COUNT = Byte.SIZE;
|
||||
|
||||
private boolean[] values = new boolean[BIT_COUNT];
|
||||
|
||||
public Array8Bit(byte b) {
|
||||
fromByte(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bits (index 0 is the lowest significant bit)
|
||||
*/
|
||||
public Array8Bit(boolean[] bits) {
|
||||
if (bits == null || bits.length != BIT_COUNT)
|
||||
throw new IllegalArgumentException("bits is null or bits.length != "+BIT_COUNT);
|
||||
values = Arrays.copyOf(bits, BIT_COUNT);
|
||||
}
|
||||
|
||||
/**
|
||||
* i = 0 is the lowest significant bit
|
||||
*/
|
||||
public boolean getBit(int i) {
|
||||
return values[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* i = 0 is the lowest significant bit
|
||||
*/
|
||||
public void setBit(int i, boolean b) {
|
||||
values[i] = b;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void fromByte(byte b) {
|
||||
int mask = 1;
|
||||
for (int i = 0; i < BIT_COUNT; i++) {
|
||||
values[i] = (b & mask) != 0;
|
||||
mask <<= 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public byte toByte() {
|
||||
byte b = 0;
|
||||
for (int i=BIT_COUNT-1; i>=0; i--) {
|
||||
b <<= 1;
|
||||
if (values[i]) b |= 1;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@@ -1,275 +0,0 @@
|
||||
package fr.pandacube.lib.net;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.annotations.Beta;
|
||||
|
||||
@Beta
|
||||
public final class ByteBuffer implements Cloneable {
|
||||
|
||||
public static final Charset NETWORK_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
private java.nio.ByteBuffer buff;
|
||||
|
||||
public ByteBuffer() {
|
||||
this(16);
|
||||
}
|
||||
|
||||
public ByteBuffer(int initSize) {
|
||||
buff = java.nio.ByteBuffer.allocate(initSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ByteBuffer that is initially <b>backed</b> by the provided byte array.
|
||||
* The position of this buffer will be 0.
|
||||
* If this ByteBuffer needs a biffer array, the provided array is replaced by a new one,
|
||||
* making the provided array not related to this ByteBuffer anymore.
|
||||
* @param data array of byte that serve as a backend for this ByteBuffer.
|
||||
*/
|
||||
public ByteBuffer(byte[] data) {
|
||||
buff = java.nio.ByteBuffer.wrap(data);
|
||||
}
|
||||
|
||||
private void askForBufferExtension(int needed) {
|
||||
while (buff.remaining() < needed) {
|
||||
java.nio.ByteBuffer newBuff = java.nio.ByteBuffer.wrap(Arrays.copyOf(buff.array(), buff.array().length * 2));
|
||||
newBuff.position(buff.position());
|
||||
buff = newBuff;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This clone method also clone the underlying array.
|
||||
*/
|
||||
@SuppressWarnings("MethodDoesntCallSuperMethod")
|
||||
@Override
|
||||
public ByteBuffer clone() {
|
||||
return new ByteBuffer(Arrays.copyOf(buff.array(), buff.array().length));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.nio.ByteBuffer#get()
|
||||
*/
|
||||
public byte getByte() {
|
||||
return buff.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.nio.ByteBuffer#get(byte[])
|
||||
*/
|
||||
public byte[] getByteArray(byte[] b) {
|
||||
buff.get(b);
|
||||
return b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the next byte array wich is preceded with his size as integer,
|
||||
* or null if the founded size is negative.
|
||||
*/
|
||||
public byte[] getSizedByteArray() {
|
||||
int size = getInt();
|
||||
if (size < 0) return null;
|
||||
return getByteArray(new byte[size]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.nio.ByteBuffer#getChar()
|
||||
*/
|
||||
public char getChar() {
|
||||
return buff.getChar();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.nio.ByteBuffer#getShort()
|
||||
*/
|
||||
public short getShort() {
|
||||
return buff.getShort();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.nio.ByteBuffer#getInt()
|
||||
*/
|
||||
public int getInt() {
|
||||
return buff.getInt();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.nio.ByteBuffer#getLong()
|
||||
*/
|
||||
public long getLong() {
|
||||
return buff.getLong();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.nio.ByteBuffer#getFloat()
|
||||
*/
|
||||
public float getFloat() {
|
||||
return buff.getFloat();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.nio.ByteBuffer#getDouble()
|
||||
*/
|
||||
public double getDouble() {
|
||||
return buff.getDouble();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.nio.ByteBuffer#put(byte)
|
||||
*/
|
||||
public ByteBuffer putByte(byte b) {
|
||||
askForBufferExtension(Byte.BYTES);
|
||||
buff.put(b);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.nio.ByteBuffer#put(byte[])
|
||||
*/
|
||||
public ByteBuffer putByteArray(byte[] b) {
|
||||
askForBufferExtension(b.length * Byte.BYTES);
|
||||
buff.put(b);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ByteBuffer putSizedByteArray(byte[] b) {
|
||||
if (b == null) {
|
||||
return putInt(-1);
|
||||
}
|
||||
putInt(b.length);
|
||||
return putByteArray(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.nio.ByteBuffer#putChar(char)
|
||||
*/
|
||||
public ByteBuffer putChar(char value) {
|
||||
askForBufferExtension(Character.BYTES);
|
||||
buff.putChar(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.nio.ByteBuffer#putShort(short)
|
||||
*/
|
||||
public ByteBuffer putShort(short value) {
|
||||
askForBufferExtension(Short.BYTES);
|
||||
buff.putShort(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.nio.ByteBuffer#putInt(int)
|
||||
*/
|
||||
public ByteBuffer putInt(int value) {
|
||||
askForBufferExtension(Integer.BYTES);
|
||||
buff.putInt(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.nio.ByteBuffer#putLong(long)
|
||||
*/
|
||||
public ByteBuffer putLong(long value) {
|
||||
askForBufferExtension(Long.BYTES);
|
||||
buff.putLong(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.nio.ByteBuffer#putFloat(float)
|
||||
*/
|
||||
public ByteBuffer putFloat(float value) {
|
||||
askForBufferExtension(Float.BYTES);
|
||||
buff.putFloat(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.nio.ByteBuffer#putDouble(double)
|
||||
*/
|
||||
public ByteBuffer putDouble(double value) {
|
||||
askForBufferExtension(Double.BYTES);
|
||||
buff.putDouble(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.nio.ByteBuffer#position()
|
||||
*/
|
||||
public int getPosition() {
|
||||
return buff.position();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.nio.ByteBuffer#position(int)
|
||||
*/
|
||||
public void setPosition(int p) {
|
||||
buff.position(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.nio.ByteBuffer#capacity()
|
||||
*/
|
||||
public int capacity() {
|
||||
return buff.capacity();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param s null String are supported
|
||||
*/
|
||||
public ByteBuffer putString(String s) {
|
||||
if (s == null) {
|
||||
return putInt(-1);
|
||||
}
|
||||
return putSizedByteArray(s.getBytes(NETWORK_CHARSET));
|
||||
}
|
||||
|
||||
/**
|
||||
* returned string can be null
|
||||
*/
|
||||
public String getString() {
|
||||
byte[] binaryString = getSizedByteArray();
|
||||
return (binaryString == null) ? null : new String(binaryString, NETWORK_CHARSET);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param list The list can be null, and any String can be null too.
|
||||
*/
|
||||
public ByteBuffer putListOfString(List<String> list) {
|
||||
if (list == null) {
|
||||
return putInt(-1);
|
||||
}
|
||||
putInt(list.size());
|
||||
for (String str : list)
|
||||
putString(str);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a List of String. The list can be null, and any element can be null too.
|
||||
*/
|
||||
public List<String> getListOfString() {
|
||||
int size = getInt();
|
||||
if (size < 0)
|
||||
return null;
|
||||
List<String> list = new ArrayList<>();
|
||||
for (int i = 0; i < size; i++)
|
||||
list.add(getString());
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.nio.ByteBuffer#array()
|
||||
*/
|
||||
public byte[] array() {
|
||||
return buff.array();
|
||||
}
|
||||
|
||||
}
|
@@ -1,60 +0,0 @@
|
||||
package fr.pandacube.lib.net;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.google.common.annotations.Beta;
|
||||
|
||||
@Beta
|
||||
public class PPacket {
|
||||
public final String name;
|
||||
/* package */ int id;
|
||||
public final byte[] content;
|
||||
|
||||
/**
|
||||
* Construct a new PPacket based on the content of the provided buffer before his position.
|
||||
* @param n the name of the packet.
|
||||
* @param buff the buffer where the data comes from. Only the content before {@link ByteBuffer#getPosition()} is copied.
|
||||
*/
|
||||
public PPacket(String n, ByteBuffer buff) {
|
||||
this(n, Arrays.copyOf(buff.array(), buff.getPosition()));
|
||||
}
|
||||
|
||||
public PPacket(String n, byte[] c) {
|
||||
name = n;
|
||||
content = c;
|
||||
}
|
||||
|
||||
/* package */ PPacket(String n, int i, byte[] c) {
|
||||
this(n, c);
|
||||
id = i;
|
||||
}
|
||||
|
||||
public ByteBuffer getContentAsBuffer() {
|
||||
return new ByteBuffer(content);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public static PPacket buildSingleStringContentPacket(String name, String content) {
|
||||
return new PPacket(name, new ByteBuffer().putString(content));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* package */ static PPacket buildLoginPacket(String password) {
|
||||
return buildSingleStringContentPacket("login", password);
|
||||
}
|
||||
/* package */ static PPacket buildBadFormatPacket(String message) {
|
||||
return buildSingleStringContentPacket("bad_format", message);
|
||||
}
|
||||
/* package */ static PPacket buildLoginBadPacket() {
|
||||
return new PPacket("login_bad", new byte[0]);
|
||||
}
|
||||
}
|
@@ -1,47 +0,0 @@
|
||||
package fr.pandacube.lib.net;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.google.common.annotations.Beta;
|
||||
|
||||
@Beta
|
||||
public class PPacketAnswer extends PPacket {
|
||||
/* package */ final int answer;
|
||||
|
||||
/**
|
||||
* Construct a new PPacketAnswer based on the content of the provided buffer before his position.
|
||||
* @param n the name of the packet.
|
||||
* @param buff the buffer where the data comes from. Only the content before {@link ByteBuffer#getPosition()} is copied.
|
||||
*/
|
||||
public PPacketAnswer(PPacket answered, String n, ByteBuffer buff) {
|
||||
this(answered, n, Arrays.copyOf(buff.array(), buff.getPosition()));
|
||||
}
|
||||
|
||||
public PPacketAnswer(PPacket answered, String n, byte[] c) {
|
||||
super(n, c);
|
||||
answer = answered.id;
|
||||
}
|
||||
|
||||
/* package */ PPacketAnswer(String n, int i, int a, byte[] c) {
|
||||
super(n, i, c);
|
||||
answer = a;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public static PPacketAnswer buildSingleStringContentPacketAnswer(PPacket answered, String name, String content) {
|
||||
ByteBuffer pwBuff = new ByteBuffer().putString(content);
|
||||
return new PPacketAnswer(answered, name, Arrays.copyOf(pwBuff.array(), pwBuff.getPosition()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* package */ static PPacketAnswer buildLoginOkPacket(PPacket loginPacket) {
|
||||
return new PPacketAnswer(loginPacket, "login_ok", new byte[0]);
|
||||
}
|
||||
/* package */ static PPacketAnswer buildExceptionPacket(PPacket answered, String message) {
|
||||
return buildSingleStringContentPacketAnswer(answered, "exception", message);
|
||||
}
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
package fr.pandacube.lib.net;
|
||||
|
||||
import com.google.common.annotations.Beta;
|
||||
|
||||
@Beta
|
||||
@FunctionalInterface
|
||||
public interface PPacketListener<P extends PPacket> {
|
||||
|
||||
/**
|
||||
* Called when we receive a packet (except responses)
|
||||
* @param connection the connection from where the packet comes
|
||||
* @param packet the received packet
|
||||
*/
|
||||
void onPacketReceive(PSocket connection, P packet);
|
||||
|
||||
}
|
@@ -1,157 +0,0 @@
|
||||
package fr.pandacube.lib.net;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import com.google.common.annotations.Beta;
|
||||
|
||||
import fr.pandacube.lib.util.Log;
|
||||
|
||||
@Beta
|
||||
public class PServer extends Thread implements Closeable {
|
||||
private static final AtomicInteger connectionCounterId = new AtomicInteger(0);
|
||||
|
||||
private final int port;
|
||||
private ServerSocket socket;
|
||||
private final String socketName;
|
||||
|
||||
private final List<TCPServerClientConnection> clients = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
private final AtomicBoolean isClosed = new AtomicBoolean(false);
|
||||
|
||||
|
||||
private final List<PPacketListener<PPacket>> globalPacketListeners = Collections.synchronizedList(new ArrayList<>());
|
||||
private final List<PSocketConnectionListener> clientConnectionListeners = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
|
||||
|
||||
private final String password;
|
||||
|
||||
public PServer(int port, String sckName, String password) {
|
||||
super("PServer " + sckName);
|
||||
setDaemon(true);
|
||||
if (port <= 0 || port > 65535) throw new IllegalArgumentException("le numéro de port est invalide");
|
||||
socketName = sckName;
|
||||
this.port = port;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
try {
|
||||
|
||||
socket = new ServerSocket();
|
||||
socket.setReceiveBufferSize(PSocket.NETWORK_TCP_BUFFER_SIZE);
|
||||
socket.setPerformancePreferences(0, 1, 0);
|
||||
socket.bind(new InetSocketAddress(port));
|
||||
|
||||
while (true) {
|
||||
Socket socketClient = socket.accept();
|
||||
socketClient.setSendBufferSize(PSocket.NETWORK_TCP_BUFFER_SIZE);
|
||||
socketClient.setSoTimeout(PSocket.NETWORK_TIMEOUT);
|
||||
|
||||
TCPServerClientConnection co = new TCPServerClientConnection(socketClient,
|
||||
connectionCounterId.getAndIncrement());
|
||||
co.start();
|
||||
}
|
||||
} catch (SocketException ignored) {
|
||||
} catch (Exception e) {
|
||||
Log.warning("Plus aucune connexion ne peux être acceptée", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void addPacketListener(PPacketListener<PPacket> l) {
|
||||
globalPacketListeners.add(l);
|
||||
}
|
||||
|
||||
public boolean removePacketListener(PPacketListener<PPacket> l) {
|
||||
return globalPacketListeners.remove(l);
|
||||
}
|
||||
|
||||
public void addConnectionListener(PSocketConnectionListener l) {
|
||||
clientConnectionListeners.add(l);
|
||||
}
|
||||
|
||||
public void removeConnectionListener(PSocketConnectionListener l) {
|
||||
clientConnectionListeners.remove(l);
|
||||
}
|
||||
|
||||
protected class TCPServerClientConnection extends PSocket {
|
||||
|
||||
boolean loggedIn;
|
||||
|
||||
private TCPServerClientConnection(Socket s, int coId) {
|
||||
super(s, "Conn#" + coId + " via PServer " + socketName, password);
|
||||
addConnectionListener(new PSocketConnectionListener() {
|
||||
@Override
|
||||
public void onDisconnect(PSocket connection) {
|
||||
try {
|
||||
clientConnectionListeners.forEach(l -> l.onDisconnect(connection));
|
||||
} finally {
|
||||
clients.remove((TCPServerClientConnection)connection);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onConnect(PSocket connection) {
|
||||
clients.add((TCPServerClientConnection)connection);
|
||||
clientConnectionListeners.forEach(l -> l.onConnect(connection));
|
||||
}
|
||||
});
|
||||
addPacketListener((conn, packet) ->
|
||||
globalPacketListeners.forEach(l -> {
|
||||
try {
|
||||
l.onPacketReceive(conn, packet);
|
||||
} catch (Exception e) {
|
||||
Log.severe("Exception while calling PPacketListener.onPacketReceive().", e);
|
||||
sendSilently(PPacketAnswer.buildExceptionPacket(packet, e.toString()));
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
if (isClosed.get()) return;
|
||||
isClosed.set(true);
|
||||
|
||||
clients.forEach(PSocket::close);
|
||||
|
||||
socket.close();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
return isClosed.get() || socket.isClosed();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public List<PSocket> getClients() {
|
||||
synchronized (clients) {
|
||||
return new ArrayList<>(clients);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.getClass().getName() + "{thread=" + getName() + ", socket=" + socket + "}";
|
||||
}
|
||||
|
||||
}
|
@@ -1,350 +0,0 @@
|
||||
package fr.pandacube.lib.net;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import com.google.common.annotations.Beta;
|
||||
|
||||
import fr.pandacube.lib.util.Log;
|
||||
|
||||
/**
|
||||
* A wrapper for a {@link Socket}. The connection must point to a software using {@link PServer}
|
||||
* as wrapper for the target {@link ServerSocket}.
|
||||
* <br>
|
||||
* This class provides a simple way to exchange data between client and server :
|
||||
* <ul>
|
||||
* <li>Maintained connection with the server</li>
|
||||
* <li>Login with a password (send in the first packet)</li>
|
||||
* <li>Binary packet id</li>
|
||||
* <li>Binary data</li>
|
||||
* <li>Input stream in a separate Thread</li>
|
||||
* </ul>
|
||||
*
|
||||
*/
|
||||
@Beta
|
||||
public class PSocket extends Thread implements Closeable {
|
||||
|
||||
public static final int NETWORK_TCP_BUFFER_SIZE = 1024 * 1024;
|
||||
|
||||
public static final int NETWORK_TIMEOUT = 0; // no timeout (milli-seconds)
|
||||
|
||||
private boolean server = false;
|
||||
private Socket socket;
|
||||
private final SocketAddress addr;
|
||||
private DataInputStream in;
|
||||
private DataOutputStream out;
|
||||
private final Object outSynchronizer = new Object();
|
||||
private String password;
|
||||
|
||||
private final AtomicBoolean isClosed = new AtomicBoolean(false);
|
||||
|
||||
private final List<PPacketListener<PPacket>> packetListeners = Collections.synchronizedList(new ArrayList<>());
|
||||
private final List<PSocketConnectionListener> connectionListeners = Collections.synchronizedList(new ArrayList<>());
|
||||
private final Map<Integer, PPacketListener<PPacketAnswer>> answersCallbacks = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
private int nextSendId = 0;
|
||||
|
||||
/**
|
||||
* Create a new PSocket that will connect to the specified SocketAddress.
|
||||
* @param a The target server to connect to
|
||||
* @param connName the name of the connection, used to name the Thread used to receive the packet.
|
||||
* @param pass the password to send to the server.
|
||||
*/
|
||||
public PSocket(SocketAddress a, String connName, String pass) {
|
||||
super("PSocket " + connName);
|
||||
setDaemon(true);
|
||||
if (a == null) throw new IllegalArgumentException("les arguments ne peuvent pas être null");
|
||||
addr = a;
|
||||
}
|
||||
|
||||
|
||||
/* package */ PSocket(Socket s, String connName, String pass) {
|
||||
this(s.getRemoteSocketAddress(), connName, pass);
|
||||
socket = s;
|
||||
server = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
try {
|
||||
if (socket == null) {
|
||||
socket = new Socket();
|
||||
socket.setReceiveBufferSize(NETWORK_TCP_BUFFER_SIZE);
|
||||
socket.setSendBufferSize(NETWORK_TCP_BUFFER_SIZE);
|
||||
|
||||
socket.setSoTimeout(10000); // initial timeout before login
|
||||
|
||||
socket.connect(addr);
|
||||
|
||||
in = new DataInputStream(socket.getInputStream());
|
||||
out = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream()));
|
||||
}
|
||||
|
||||
// password check
|
||||
if (server) {
|
||||
PPacket packet = readPacket();
|
||||
if (packet == null || packet instanceof PPacketAnswer || !"login".equals(packet.name)) {
|
||||
send(PPacket.buildLoginBadPacket());
|
||||
close();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String receivedPassword = new ByteBuffer(packet.content).getString();
|
||||
if (!Objects.equals(receivedPassword, password)) {
|
||||
send(PPacket.buildLoginBadPacket());
|
||||
close();
|
||||
return;
|
||||
}
|
||||
} catch(Exception e) {
|
||||
send(PPacket.buildLoginBadPacket());
|
||||
close();
|
||||
return;
|
||||
}
|
||||
send(PPacketAnswer.buildLoginOkPacket(packet));
|
||||
// login ok at this point
|
||||
}
|
||||
else {
|
||||
send(PPacket.buildLoginPacket(password));
|
||||
PPacket packet = readPacket();
|
||||
if (packet == null) {
|
||||
Log.severe("bad packet received from server. Disconnecting.");
|
||||
close();
|
||||
return;
|
||||
}
|
||||
if (packet.name.equals("login_bad")) {
|
||||
Log.severe("Wrong password to connect to server. Disconnecting.");
|
||||
close();
|
||||
return;
|
||||
}
|
||||
if (!packet.name.equals("login_ok")) {
|
||||
Log.severe("Unexpected packet from server. Disconnecting.");
|
||||
close();
|
||||
return;
|
||||
}
|
||||
// login ok at this point
|
||||
}
|
||||
password = null;
|
||||
|
||||
socket.setSoTimeout(NETWORK_TIMEOUT);
|
||||
|
||||
Log.info(getName() + " connected.");
|
||||
|
||||
connectionListeners.forEach(l -> {
|
||||
try {
|
||||
l.onConnect(this);
|
||||
} catch (Exception e) {
|
||||
Log.severe("Exception while calling PSocketConnectionListener.onConnect().", e);
|
||||
}
|
||||
});
|
||||
|
||||
while (!socket.isClosed()) {
|
||||
PPacket packet = readPacket();
|
||||
|
||||
if (packet == null) {
|
||||
send(PPacket.buildBadFormatPacket("Bad format for the last packet received. Closing connection."));
|
||||
break;
|
||||
}
|
||||
|
||||
if (packet instanceof PPacketAnswer) {
|
||||
try {
|
||||
answersCallbacks.remove(((PPacketAnswer)packet).answer).onPacketReceive(this, (PPacketAnswer)packet);
|
||||
} catch (Exception e) {
|
||||
Log.severe("Exception while calling PPacketListener.onPacketReceive().", e);
|
||||
send(PPacketAnswer.buildExceptionPacket(packet, e.toString()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
packetListeners.forEach(l -> {
|
||||
try {
|
||||
l.onPacketReceive(this, packet);
|
||||
} catch (Exception e) {
|
||||
Log.severe("Exception while calling PPacketListener.onPacketReceive().", e);
|
||||
sendSilently(PPacketAnswer.buildExceptionPacket(packet, e.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.severe(e);
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the packet read in the socket, or null if the packet is in a bad format.
|
||||
* @return the packet
|
||||
*
|
||||
*/
|
||||
private PPacket readPacket() throws IOException {
|
||||
byte nSize = in.readByte();
|
||||
if (nSize == 0) {
|
||||
return null;
|
||||
}
|
||||
boolean answer = nSize < 0;
|
||||
if (answer)
|
||||
nSize *= -1;
|
||||
|
||||
|
||||
byte[] nBytes = new byte[nSize];
|
||||
in.readFully(nBytes);
|
||||
String name = new String(nBytes, ByteBuffer.NETWORK_CHARSET);
|
||||
|
||||
|
||||
int packetId = in.readInt();
|
||||
|
||||
|
||||
int answerId = (answer) ? in.readInt() : -1;
|
||||
|
||||
|
||||
int cSize = in.readInt();
|
||||
if (cSize < 0 || cSize > 0xFFFFFF) { // can't be more that 16 MiB
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
byte[] content = new byte[cSize];
|
||||
in.readFully(content);
|
||||
|
||||
return answer ? new PPacketAnswer(name, packetId, answerId, content) : new PPacket(name, packetId, content);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Send the provided packet, without waiting for an answer.
|
||||
*/
|
||||
public void send(PPacket packet) throws IOException {
|
||||
if (packet == null)
|
||||
throw new IllegalArgumentException("packet can't be null");
|
||||
if (packet.name == null)
|
||||
throw new IllegalArgumentException("packet.name can't be null");
|
||||
if (packet.content == null)
|
||||
throw new IllegalArgumentException("packet.content can't be null");
|
||||
|
||||
byte[] nameBytes = packet.name.getBytes(ByteBuffer.NETWORK_CHARSET);
|
||||
if (nameBytes.length > 127)
|
||||
throw new IllegalArgumentException("packet.name must take fewer than 128 bytes when converted to UTF-8");
|
||||
byte nameSize = (byte)nameBytes.length;
|
||||
|
||||
boolean answer = packet instanceof PPacketAnswer;
|
||||
|
||||
if (answer) nameSize *= -1;
|
||||
|
||||
synchronized (outSynchronizer) {
|
||||
int packetId = nextSendId++;
|
||||
|
||||
packet.id = packetId;
|
||||
|
||||
out.write(new byte[] {nameSize});
|
||||
out.write(nameBytes);
|
||||
out.write(packetId);
|
||||
if (answer)
|
||||
out.write(((PPacketAnswer)packet).answer);
|
||||
out.write(packet.content.length);
|
||||
out.write(packet.content);
|
||||
out.flush();
|
||||
}
|
||||
}
|
||||
|
||||
public void sendSilently(PPacket packet) {
|
||||
try {
|
||||
send(packet);
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void send(PPacket packet, PPacketListener<PPacketAnswer> answerCallback) throws IOException {
|
||||
synchronized (answersCallbacks) {
|
||||
/*
|
||||
* This synch block ensure that the callback will be put in the listeners Map before
|
||||
* we receve the answer (in case this is really really fast)
|
||||
*/
|
||||
send(packet);
|
||||
answersCallbacks.put(packet.id, answerCallback);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public void addPacketListener(PPacketListener<PPacket> l) {
|
||||
packetListeners.add(l);
|
||||
}
|
||||
|
||||
public boolean removePacketListener(PPacketListener<PPacket> l) {
|
||||
return packetListeners.remove(l);
|
||||
}
|
||||
|
||||
|
||||
public void addConnectionListener(PSocketConnectionListener l) {
|
||||
connectionListeners.add(l);
|
||||
}
|
||||
|
||||
public void removeConnectionListener(PSocketConnectionListener l) {
|
||||
connectionListeners.remove(l);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
synchronized (outSynchronizer) {
|
||||
if (isClosed.get()) return;
|
||||
|
||||
Log.info(getName() + " closing...");
|
||||
|
||||
connectionListeners.forEach(l -> {
|
||||
try {
|
||||
l.onDisconnect(this);
|
||||
} catch (Exception e) {
|
||||
Log.severe("Exception while calling PSocketConnectionListener.onDisconnect().", e);
|
||||
}
|
||||
});
|
||||
|
||||
socket.close();
|
||||
isClosed.set(true);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.warning(e);
|
||||
}
|
||||
}
|
||||
|
||||
public SocketAddress getRemoteAddress() {
|
||||
return addr;
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
return isClosed.get() || socket.isClosed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.getClass().getName() + "{thread=" + getName() + ", socket=" + socket + "}";
|
||||
}
|
||||
|
||||
}
|
@@ -1,20 +0,0 @@
|
||||
package fr.pandacube.lib.net;
|
||||
|
||||
import com.google.common.annotations.Beta;
|
||||
|
||||
@Beta
|
||||
public interface PSocketConnectionListener {
|
||||
|
||||
/**
|
||||
* Called when a socket is connected
|
||||
* @param connection the connection
|
||||
*/
|
||||
void onConnect(PSocket connection);
|
||||
|
||||
/**
|
||||
* Called just before a socket is disconnected
|
||||
* @param connection the connection
|
||||
*/
|
||||
void onDisconnect(PSocket connection);
|
||||
|
||||
}
|
@@ -21,4 +21,8 @@
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
<maven.javadoc.skip>true</maven.javadoc.skip>
|
||||
</properties>
|
||||
|
||||
</project>
|
@@ -18,7 +18,7 @@ public class ResponseAnalyser {
|
||||
if (socket == null || socket.isClosed() || socket.isInputShutdown())
|
||||
throw new IllegalArgumentException("le socket doit être non null et doit être ouvert sur le flux d'entrée");
|
||||
|
||||
// on lis la réponse
|
||||
// on lit la réponse
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
|
||||
String line;
|
||||
|
@@ -5,15 +5,15 @@ import java.io.PrintStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
|
||||
public abstract class AbstractRequestExecutor {
|
||||
|
||||
public final String command;
|
||||
|
||||
public AbstractRequestExecutor(String cmd, NetworkAPIListener napiListener) {
|
||||
public AbstractRequestExecutor(String cmd, NetworkAPIListener nAPIListener) {
|
||||
command = cmd.toLowerCase();
|
||||
napiListener.registerRequestExecutor(command, this);
|
||||
nAPIListener.registerRequestExecutor(command, this);
|
||||
}
|
||||
|
||||
public void execute(String data, Socket socket) throws IOException {
|
||||
@@ -34,9 +34,8 @@ public abstract class AbstractRequestExecutor {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param data La représentation sous forme de String des données envoyés
|
||||
* dans la requête
|
||||
* @return La réponse à retourner au client
|
||||
* @param data The String representation of the request data.
|
||||
* @return The response to send back to the client.
|
||||
*/
|
||||
protected abstract Response run(InetAddress source, String data);
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package fr.pandacube.lib.netapi.server;
|
||||
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
@@ -18,7 +18,7 @@ public class NetworkAPIListener implements Runnable {
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* Instencie le côté serveur du NetworkAPI.
|
||||
* Instancie le côté serveur du NetworkAPI.
|
||||
*
|
||||
* @param n nom du networkAPI (permet l'identification dans les logs)
|
||||
* @param p le port d'écoute
|
||||
@@ -29,7 +29,7 @@ public class NetworkAPIListener implements Runnable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Instencie le côté serveur du NetworkAPI.
|
||||
* Instancie le côté serveur du NetworkAPI.
|
||||
*
|
||||
* @param n nom du networkAPI (permet l'identification dans les logs)
|
||||
* @param p le port d'écoute
|
||||
@@ -56,7 +56,6 @@ public class NetworkAPIListener implements Runnable {
|
||||
Log.info("NetworkAPI '" + name + "' à l'écoute sur le socket " + serverSocket.getLocalSocketAddress());
|
||||
|
||||
try {
|
||||
// réception des connexion client
|
||||
while (!serverSocket.isClosed()) {
|
||||
Thread t = new Thread(new PacketExecutor(serverSocket.accept(), this));
|
||||
t.setDaemon(true);
|
||||
|
@@ -5,7 +5,7 @@ import java.io.PrintStream;
|
||||
import java.net.Socket;
|
||||
|
||||
import fr.pandacube.lib.netapi.server.RequestAnalyser.BadRequestException;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
|
||||
/**
|
||||
* Prends en charge un socket client et le transmet au gestionnaire de paquet
|
||||
@@ -20,9 +20,9 @@ public class PacketExecutor implements Runnable {
|
||||
private final Socket socket;
|
||||
private final NetworkAPIListener networkAPIListener;
|
||||
|
||||
public PacketExecutor(Socket s, NetworkAPIListener napiListener) {
|
||||
public PacketExecutor(Socket s, NetworkAPIListener nAPIListener) {
|
||||
socket = s;
|
||||
networkAPIListener = napiListener;
|
||||
networkAPIListener = nAPIListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -10,23 +10,23 @@ public class RequestAnalyser {
|
||||
public final String command;
|
||||
public final String data;
|
||||
|
||||
public RequestAnalyser(Socket socket, NetworkAPIListener napiListener) throws IOException, BadRequestException {
|
||||
if (socket == null || socket.isClosed() || socket.isInputShutdown() || napiListener == null)
|
||||
public RequestAnalyser(Socket socket, NetworkAPIListener nAPIListener) throws IOException, BadRequestException {
|
||||
if (socket == null || socket.isClosed() || socket.isInputShutdown() || nAPIListener == null)
|
||||
throw new IllegalArgumentException(
|
||||
"le socket doit être non null et doit être ouvert sur le flux d'entrée et napiListener ne doit pas être null");
|
||||
"le socket doit être non null et doit être ouvert sur le flux d'entrée et nAPIListener ne doit pas être null");
|
||||
|
||||
// on lis la réponse
|
||||
// on lit la réponse
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
|
||||
String line;
|
||||
|
||||
// lecture de la première ligne
|
||||
line = in.readLine();
|
||||
if (line == null || !line.equals(napiListener.pass)) throw new BadRequestException("wrong_password");
|
||||
if (line == null || !line.equals(nAPIListener.pass)) throw new BadRequestException("wrong_password");
|
||||
|
||||
// lecture de la deuxième ligne
|
||||
line = in.readLine();
|
||||
if (line == null || napiListener.getRequestExecutor(line) == null)
|
||||
if (line == null || nAPIListener.getRequestExecutor(line) == null)
|
||||
throw new BadRequestException("command_not_exists");
|
||||
command = line;
|
||||
|
||||
|
@@ -16,7 +16,7 @@
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>papermc</id>
|
||||
<url>https://papermc.io/repo/repository/maven-public/</url>
|
||||
<url>https://repo.papermc.io/repository/maven-public/</url>
|
||||
</repository>
|
||||
|
||||
<!-- WorldEdit -->
|
||||
@@ -25,7 +25,7 @@
|
||||
<url>https://maven.enginehub.org/repo/</url>
|
||||
</repository>
|
||||
|
||||
<!-- Vault and maybe other dependecies -->
|
||||
<!-- Vault and maybe other dependencies -->
|
||||
<repository>
|
||||
<id>jitpack.io</id>
|
||||
<url>https://jitpack.io</url>
|
||||
@@ -33,12 +33,12 @@
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<!-- <dependency>
|
||||
<groupId>fr.pandacube.lib</groupId>
|
||||
<artifactId>pandalib-players-permissible</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependency> -->
|
||||
<dependency>
|
||||
<groupId>fr.pandacube.lib</groupId>
|
||||
<artifactId>pandalib-permissions</artifactId>
|
||||
@@ -77,7 +77,7 @@
|
||||
<dependency>
|
||||
<groupId>com.sk89q.worldedit</groupId>
|
||||
<artifactId>worldedit-bukkit</artifactId>
|
||||
<version>7.2.9</version>
|
||||
<version>7.2.19</version>
|
||||
<scope>provided</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
|
@@ -23,11 +23,11 @@ import org.bukkit.permissions.ServerOperator;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import fr.pandacube.lib.permissions.Permissions;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
|
||||
/**
|
||||
* Class that integrates the {@code pandalib-permissions} system into a Bukkit/Spigot/Paper instance.
|
||||
* The integration is made when calling {@link #init(JavaPlugin, String)}.
|
||||
* The integration is made when calling {@link #onLoad(JavaPlugin, String)} and {@link #onEnable()}.
|
||||
* The permission system must be initialized first, using {@link Permissions#init(Function)}.
|
||||
* Don’t forget that the permission system also needs a connection to a database, so don’t forget to call
|
||||
* {@link DB#init(DBConnection, String)} with the appropriate parameters before anything.
|
||||
@@ -38,18 +38,26 @@ public class PandalibPaperPermissions implements Listener {
|
||||
/* package */ static String serverName;
|
||||
/* package */ static final Map<String, String> permissionMap = new HashMap<>();
|
||||
|
||||
|
||||
/**
|
||||
* Integrates the {@code pandalib-permissions} system into the Bukkit server.
|
||||
* Integrates the {@code pandalib-permissions} system into the Bukkit server, during the loading phase of the plugin.
|
||||
* @param plugin a Bukkit plugin.
|
||||
* @param serverName the name of the current server, used to fetch server specific permissions. Cannot be null.
|
||||
* If this server in not in a multi-server configuration, use a dummy server name, like
|
||||
* If this server in not in a multiserver configuration, use a dummy server name, like
|
||||
* {@code ""} (empty string).
|
||||
*/
|
||||
public static void init(JavaPlugin plugin, String serverName) {
|
||||
public static void onLoad(JavaPlugin plugin, String serverName) {
|
||||
PandalibPaperPermissions.plugin = plugin;
|
||||
PandalibPaperPermissions.serverName = serverName;
|
||||
PermissionsInjectorVault.onLoad();
|
||||
}
|
||||
|
||||
/**
|
||||
* Integrates the {@code pandalib-permissions} system into the Bukkit server, during the enabling phase of the plugin.
|
||||
*/
|
||||
public static void onEnable() {
|
||||
PermissionsInjectorBukkit.inject(Bukkit.getConsoleSender());
|
||||
PermissionsInjectorVault.inject();
|
||||
PermissionsInjectorVault.onEnable();
|
||||
PermissionsInjectorWEPIF.inject();
|
||||
|
||||
Bukkit.getPluginManager().registerEvents(new PandalibPaperPermissions(), plugin);
|
||||
@@ -74,6 +82,12 @@ public class PandalibPaperPermissions implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a {@link PandalibPaperPermissions} instance.
|
||||
*/
|
||||
private PandalibPaperPermissions() {}
|
||||
|
||||
/**
|
||||
* Player login event handler.
|
||||
* @param event the event.
|
||||
|
@@ -1,5 +1,23 @@
|
||||
package fr.pandacube.lib.paper.permissions;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import fr.pandacube.lib.permissions.Permissions;
|
||||
import fr.pandacube.lib.reflect.Reflect;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.ConsoleCommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.permissions.Permissible;
|
||||
import org.bukkit.permissions.PermissibleBase;
|
||||
import org.bukkit.permissions.Permission;
|
||||
import org.bukkit.permissions.PermissionAttachment;
|
||||
import org.bukkit.permissions.PermissionAttachmentInfo;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -11,24 +29,6 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.ConsoleCommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.permissions.Permissible;
|
||||
import org.bukkit.permissions.PermissibleBase;
|
||||
import org.bukkit.permissions.Permission;
|
||||
import org.bukkit.permissions.PermissionAttachment;
|
||||
import org.bukkit.permissions.PermissionAttachmentInfo;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import fr.pandacube.lib.permissions.Permissions;
|
||||
import fr.pandacube.lib.reflect.Reflect;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
|
||||
/* package */ class PermissionsInjectorBukkit
|
||||
{
|
||||
|
||||
@@ -58,26 +58,24 @@ import fr.pandacube.lib.util.Log;
|
||||
}
|
||||
}
|
||||
|
||||
private static void setPermissible(CommandSender sender, Permissible newpermissible)
|
||||
private static void setPermissible(CommandSender sender, Permissible newPermissible)
|
||||
{
|
||||
try {
|
||||
Field perm = getPermField(sender);
|
||||
if (perm == null)
|
||||
return;
|
||||
perm.setAccessible(true);
|
||||
perm.set(sender, newpermissible);
|
||||
perm.set(sender, newPermissible);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.severe(e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ static Permissible getPermissible(CommandSender sender)
|
||||
{
|
||||
Field perm = getPermField(sender);
|
||||
if (perm == null)
|
||||
return null;
|
||||
try {
|
||||
Field perm = getPermField(sender);
|
||||
perm.setAccessible(true);
|
||||
Permissible p = (Permissible) perm.get(sender);
|
||||
if (p == null) {
|
||||
@@ -86,26 +84,19 @@ import fr.pandacube.lib.util.Log;
|
||||
return p;
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.severe(e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Field getPermField(CommandSender sender)
|
||||
{
|
||||
private static Field getPermField(CommandSender sender) throws NoSuchFieldException {
|
||||
if (sender == null) {
|
||||
throw new IllegalArgumentException("sender cannot be null");
|
||||
}
|
||||
try {
|
||||
if (sender instanceof Player || sender instanceof ConsoleCommandSender)
|
||||
return Reflect.ofClassOfInstance(sender).field("perm").get();
|
||||
else
|
||||
throw new IllegalArgumentException("Unsupported type for sender: " + sender.getClass());
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.severe(e);
|
||||
}
|
||||
return null;
|
||||
if (sender instanceof Player || sender instanceof ConsoleCommandSender)
|
||||
return Reflect.ofClassOfInstance(sender).field("perm").get();
|
||||
else
|
||||
throw new IllegalArgumentException("Unsupported type for sender: " + sender.getClass());
|
||||
|
||||
}
|
||||
|
||||
/* package */ static class PandaPermissible extends PermissibleBase
|
||||
@@ -118,7 +109,7 @@ import fr.pandacube.lib.util.Log;
|
||||
|
||||
@SuppressWarnings("UnusedAssignment")
|
||||
private boolean init = false;
|
||||
/* assigment to false is necessary because of super class constructor calling the method recalculatePermission()
|
||||
/* assignment to false is necessary because of super class constructor calling the method recalculatePermission()
|
||||
* and we don’t want that.
|
||||
*/
|
||||
|
||||
@@ -143,7 +134,7 @@ import fr.pandacube.lib.util.Log;
|
||||
public boolean hasPermission(String permission)
|
||||
{
|
||||
/*
|
||||
* WARNING: don’t call PermissibleOnlinePlayer#hasPermission(String) here or it will result on a stack overflow
|
||||
* WARNING: don’t call PermissibleOnlinePlayer#hasPermission(String) here, or it will result on a stack overflow
|
||||
*/
|
||||
|
||||
if (permission.toLowerCase().startsWith("minecraft.command."))
|
||||
@@ -180,7 +171,7 @@ import fr.pandacube.lib.util.Log;
|
||||
if (res != null)
|
||||
return res;
|
||||
|
||||
return oldPermissible.hasPermission(permission); // doesn’t need to manage negative permission (should not happend)
|
||||
return oldPermissible.hasPermission(permission); // doesn't need to manage negative permission (should not happen)
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -214,36 +205,34 @@ import fr.pandacube.lib.util.Log;
|
||||
.build();
|
||||
|
||||
@Override
|
||||
public Set<PermissionAttachmentInfo> getEffectivePermissions()
|
||||
public @NotNull Set<PermissionAttachmentInfo> getEffectivePermissions()
|
||||
{
|
||||
// PlotSquared uses this method to optimize permission range (plots.limit.10 for example)
|
||||
// MobArena uses this method when a player leave the arena
|
||||
// LibsDisguises uses this method (and only this one) to parse all the permissions
|
||||
|
||||
//Log.warning("There is a plugin calling CommandSender#getEffectivePermissions(). See the stacktrace to understand the reason for that.", new Throwable());
|
||||
|
||||
String world = null;
|
||||
if (sender instanceof Player player) {
|
||||
world = player.getWorld().getName();
|
||||
String world = player.getWorld().getName();
|
||||
try {
|
||||
return effectivePermissionsListCache.get(world, () -> {
|
||||
// first get the superperms effective permissions (that take isOp into account)
|
||||
Map<String, PermissionAttachmentInfo> perms = oldPermissible.getEffectivePermissions().stream()
|
||||
.collect(Collectors.toMap(PermissionAttachmentInfo::getPermission, Function.identity()));
|
||||
|
||||
// then override them with the permissions from our permission system (that has priority, and take current world into account)
|
||||
for (Map.Entry<String, Boolean> permE : getEffectivePermissionsOnServerInWorld().entrySet()) {
|
||||
perms.put(permE.getKey(), new PermissionAttachmentInfo(this, permE.getKey(), null, permE.getValue()));
|
||||
}
|
||||
|
||||
return new LinkedHashSet<>(perms.values());
|
||||
});
|
||||
} catch (ExecutionException e) {
|
||||
Log.severe(e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return effectivePermissionsListCache.get(world, () -> {
|
||||
// first get the superperms effective permissions (taht take isOp into accound)
|
||||
Map<String, PermissionAttachmentInfo> perms = oldPermissible.getEffectivePermissions().stream()
|
||||
.collect(Collectors.toMap(PermissionAttachmentInfo::getPermission, Function.identity()));
|
||||
|
||||
// then override them with the permissions from our permission system (that has priority, and take current world into account)
|
||||
for (Map.Entry<String, Boolean> permE : getEffectivePermissionsOnServerInWorld().entrySet()) {
|
||||
perms.put(permE.getKey(), new PermissionAttachmentInfo(this, permE.getKey(), null, permE.getValue()));
|
||||
}
|
||||
|
||||
return new LinkedHashSet<>(perms.values());
|
||||
});
|
||||
} catch (ExecutionException e) {
|
||||
Log.severe(e);
|
||||
return oldPermissible.getEffectivePermissions();
|
||||
}
|
||||
|
||||
return oldPermissible.getEffectivePermissions();
|
||||
|
||||
}
|
||||
|
||||
@@ -260,7 +249,7 @@ import fr.pandacube.lib.util.Log;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPermissionSet(String permission)
|
||||
public boolean isPermissionSet(@NotNull String permission)
|
||||
{
|
||||
Boolean res = hasPermissionOnServerInWorld(permission);
|
||||
if (res != null)
|
||||
@@ -278,31 +267,31 @@ import fr.pandacube.lib.util.Log;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PermissionAttachment addAttachment(Plugin plugin)
|
||||
public @NotNull PermissionAttachment addAttachment(@NotNull Plugin plugin)
|
||||
{
|
||||
return oldPermissible.addAttachment(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PermissionAttachment addAttachment(Plugin plugin, int ticks)
|
||||
public PermissionAttachment addAttachment(@NotNull Plugin plugin, int ticks)
|
||||
{
|
||||
return oldPermissible.addAttachment(plugin, ticks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value)
|
||||
public @NotNull PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value)
|
||||
{
|
||||
return oldPermissible.addAttachment(plugin, name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value, int ticks)
|
||||
public PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value, int ticks)
|
||||
{
|
||||
return oldPermissible.addAttachment(plugin, name, value, ticks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAttachment(PermissionAttachment attachment)
|
||||
public void removeAttachment(@NotNull PermissionAttachment attachment)
|
||||
{
|
||||
oldPermissible.removeAttachment(attachment);
|
||||
}
|
||||
|
@@ -1,38 +1,66 @@
|
||||
package fr.pandacube.lib.paper.permissions;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import fr.pandacube.lib.permissions.PermGroup;
|
||||
import fr.pandacube.lib.permissions.Permissions;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
import net.milkbowl.vault.chat.Chat;
|
||||
import net.milkbowl.vault.permission.Permission;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.plugin.ServicePriority;
|
||||
|
||||
import fr.pandacube.lib.permissions.PermGroup;
|
||||
import fr.pandacube.lib.permissions.Permissions;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import java.util.List;
|
||||
|
||||
/* package */ class PermissionsInjectorVault {
|
||||
|
||||
private static final ServicePriority servicePriority = ServicePriority.Highest;
|
||||
|
||||
public static PandaVaultPermission permInstance;
|
||||
|
||||
public static void inject() {
|
||||
|
||||
/**
|
||||
* Vault injection needs to happen as soon as possible so other plugins detects it when they load.
|
||||
*/
|
||||
public static void onLoad() {
|
||||
try {
|
||||
permInstance = new PandaVaultPermission();
|
||||
PandaVaultChat chat = new PandaVaultChat(permInstance);
|
||||
Bukkit.getServicesManager().register(net.milkbowl.vault.permission.Permission.class, permInstance,
|
||||
PandalibPaperPermissions.plugin, ServicePriority.High);
|
||||
Bukkit.getServicesManager().register(net.milkbowl.vault.chat.Chat.class, chat,
|
||||
PandalibPaperPermissions.plugin, ServicePriority.High);
|
||||
Bukkit.getServicesManager().register(Permission.class, permInstance,
|
||||
PandalibPaperPermissions.plugin, servicePriority);
|
||||
Bukkit.getServicesManager().register(Chat.class, chat,
|
||||
PandalibPaperPermissions.plugin, servicePriority);
|
||||
Log.info("Providing permissions and chat prefix/suffix through Vault API.");
|
||||
} catch (NoClassDefFoundError e) {
|
||||
Log.warning("Vault plugin not detected. Not using it to provide permissions and prefix/suffix." + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void onEnable() {
|
||||
Bukkit.getScheduler().runTaskLater(PandalibPaperPermissions.plugin,
|
||||
PermissionsInjectorVault::checkServicesRegistration, 1);
|
||||
}
|
||||
|
||||
|
||||
private static void checkServicesRegistration() {
|
||||
Permission permService = Bukkit.getServicesManager().load(Permission.class);
|
||||
if (!(permService instanceof PandaVaultPermission)) {
|
||||
Log.severe("Check for Vault Permission service failed. "
|
||||
+ (permService == null ? "Service manager returned null."
|
||||
: ("Returned service is " + permService.getName() + " (" + permService.getClass().getName() + ").")));
|
||||
|
||||
}
|
||||
Chat chatService = Bukkit.getServicesManager().load(Chat.class);
|
||||
if (!(chatService instanceof PandaVaultChat)) {
|
||||
Log.severe("Check for Vault Chat service failed. "
|
||||
+ (chatService == null ? "Service manager returned null."
|
||||
: ("Returned service is " + chatService.getName() + " (" + chatService.getClass().getName() + ").")));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* package */ static class PandaVaultPermission extends net.milkbowl.vault.permission.Permission {
|
||||
/* package */ static class PandaVaultPermission extends Permission {
|
||||
|
||||
private PandaVaultPermission() { }
|
||||
|
||||
@@ -46,6 +74,11 @@ import fr.pandacube.lib.util.Log;
|
||||
return PandalibPaperPermissions.plugin != null && PandalibPaperPermissions.plugin.isEnabled();
|
||||
}
|
||||
|
||||
private void checkEnabled() {
|
||||
if (!isEnabled())
|
||||
throw new IllegalStateException("Cannot provide permission service because plugin is disabled.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSuperPermsCompat() {
|
||||
return true;
|
||||
@@ -59,6 +92,7 @@ import fr.pandacube.lib.util.Log;
|
||||
|
||||
@Override
|
||||
public boolean playerHas(String world, OfflinePlayer player, String permission) {
|
||||
checkEnabled();
|
||||
Boolean res = Permissions.getPlayer(player.getUniqueId()).hasPermission(permission, PandalibPaperPermissions.serverName, world);
|
||||
if (res != null)
|
||||
return res;
|
||||
@@ -73,17 +107,38 @@ import fr.pandacube.lib.util.Log;
|
||||
@Deprecated
|
||||
@Override
|
||||
public boolean playerAdd(String world, String player, String permission) {
|
||||
return false;
|
||||
return playerAdd(world, Bukkit.getOfflinePlayer(player), permission);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean playerAdd(String world, OfflinePlayer player, String permission) {
|
||||
checkEnabled();
|
||||
String server = PandalibPaperPermissions.serverName;
|
||||
Permissions.getPlayer(player.getUniqueId()).addSelfPermission(permission, server, world);
|
||||
Permissions.clearPlayerCache(player.getUniqueId());
|
||||
Log.info("A plugin added permission " + permission + " (server=" + server + ",world=" + world + ") to player " + player.getName() + " through Vault.");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public boolean playerRemove(String world, String player, String permission) {
|
||||
return false;
|
||||
return playerRemove(world, Bukkit.getOfflinePlayer(player), permission);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean playerRemove(String world, OfflinePlayer player, String permission) {
|
||||
checkEnabled();
|
||||
String server = PandalibPaperPermissions.serverName;
|
||||
Permissions.getPlayer(player.getUniqueId()).removeSelfPermission(permission, server, world);
|
||||
Permissions.clearPlayerCache(player.getUniqueId());
|
||||
Log.info("A plugin removed permission " + permission + " (server=" + server + ",world=" + world + ") to player " + player.getName() + " through Vault.");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean groupHas(String world, String group, String permission) {
|
||||
checkEnabled();
|
||||
Boolean res = Permissions.getGroup(group).hasPermission(permission, PandalibPaperPermissions.serverName, world);
|
||||
if (res != null)
|
||||
return res;
|
||||
@@ -97,11 +152,15 @@ import fr.pandacube.lib.util.Log;
|
||||
|
||||
@Override
|
||||
public boolean groupAdd(String world, String group, String permission) {
|
||||
Log.severe(new Throwable("A plugin tried to add to group " + group + " (world=" + world + ") the permission " + permission
|
||||
+ " through Vault but Pandalib does not support it."));
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean groupRemove(String world, String group, String permission) {
|
||||
Log.severe(new Throwable("A plugin tried to remove from group " + group + " (world=" + world + ") the permission " + permission
|
||||
+ " through Vault but Pandalib does not support it."));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -113,18 +172,23 @@ import fr.pandacube.lib.util.Log;
|
||||
|
||||
@Override
|
||||
public boolean playerInGroup(String world, OfflinePlayer player, String group) {
|
||||
checkEnabled();
|
||||
return Permissions.getPlayer(player.getUniqueId()).isInGroup(group);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public boolean playerAddGroup(String world, String player, String group) {
|
||||
Log.severe(new Throwable("A plugin tried to add player " + player + " (world=" + world + ") to permission group " + group
|
||||
+ " through Vault but Pandalib does not support it."));
|
||||
return false;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public boolean playerRemoveGroup(String world, String player, String group) {
|
||||
Log.severe(new Throwable("A plugin tried to remove player " + player + " (world=" + world + ") from permission group " + group
|
||||
+ " through Vault but Pandalib does not support it."));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -136,6 +200,7 @@ import fr.pandacube.lib.util.Log;
|
||||
|
||||
@Override
|
||||
public String[] getPlayerGroups(String world, OfflinePlayer player) {
|
||||
checkEnabled();
|
||||
List<String> groups = Permissions.getPlayer(player.getUniqueId()).getGroupsString();
|
||||
return groups.toArray(new String[0]);
|
||||
}
|
||||
@@ -148,12 +213,14 @@ import fr.pandacube.lib.util.Log;
|
||||
|
||||
@Override
|
||||
public String getPrimaryGroup(String world, OfflinePlayer player) {
|
||||
checkEnabled();
|
||||
return Permissions.getPlayer(player.getUniqueId()).getGroupsString().stream()
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getGroups() {
|
||||
checkEnabled();
|
||||
return Permissions.getGroups().stream()
|
||||
.map(PermGroup::getName).toArray(String[]::new);
|
||||
}
|
||||
@@ -166,9 +233,9 @@ import fr.pandacube.lib.util.Log;
|
||||
}
|
||||
|
||||
|
||||
private static class PandaVaultChat extends net.milkbowl.vault.chat.Chat {
|
||||
private static class PandaVaultChat extends Chat {
|
||||
|
||||
public PandaVaultChat(net.milkbowl.vault.permission.Permission perms) {
|
||||
public PandaVaultChat(Permission perms) {
|
||||
super(perms);
|
||||
}
|
||||
|
||||
@@ -182,6 +249,11 @@ import fr.pandacube.lib.util.Log;
|
||||
return PandalibPaperPermissions.plugin != null && PandalibPaperPermissions.plugin.isEnabled();
|
||||
}
|
||||
|
||||
private void checkEnabled() {
|
||||
if (!isEnabled())
|
||||
throw new IllegalStateException("Cannot provide permission service because plugin is disabled.");
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public String getPlayerPrefix(String world, String player) {
|
||||
@@ -190,6 +262,7 @@ import fr.pandacube.lib.util.Log;
|
||||
|
||||
@Override
|
||||
public String getPlayerPrefix(String world, OfflinePlayer player) {
|
||||
checkEnabled();
|
||||
return Permissions.getPlayer(player.getUniqueId()).getPrefix();
|
||||
}
|
||||
|
||||
@@ -201,16 +274,19 @@ import fr.pandacube.lib.util.Log;
|
||||
|
||||
@Override
|
||||
public String getPlayerSuffix(String world, OfflinePlayer player) {
|
||||
checkEnabled();
|
||||
return Permissions.getPlayer(player.getUniqueId()).getSuffix();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroupPrefix(String world, String group) {
|
||||
checkEnabled();
|
||||
return Permissions.getGroup(group).getPrefix();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroupSuffix(String world, String group) {
|
||||
checkEnabled();
|
||||
return Permissions.getGroup(group).getSuffix();
|
||||
}
|
||||
|
||||
|
@@ -11,7 +11,7 @@ import org.bukkit.plugin.ServicePriority;
|
||||
|
||||
import fr.pandacube.lib.permissions.PermPlayer;
|
||||
import fr.pandacube.lib.permissions.Permissions;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
|
||||
/* package */ class PermissionsInjectorWEPIF {
|
||||
|
||||
|
@@ -16,7 +16,7 @@
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>papermc</id>
|
||||
<url>https://papermc.io/repo/repository/maven-public/</url>
|
||||
<url>https://repo.papermc.io/repository/maven-public/</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>fabricmc</id>
|
||||
@@ -71,6 +71,12 @@
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>fr.pandacube.lib</groupId>
|
||||
<artifactId>pandalib-bungee-chat</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>fr.pandacube.lib</groupId>
|
||||
<artifactId>pandalib-paper-permissions</artifactId>
|
||||
@@ -84,19 +90,6 @@
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>${paper.version}-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-mojangapi</artifactId>
|
||||
<version>${paper.version}-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Needed to read obfuscation mapping file. Already included in Paper -->
|
||||
<dependency>
|
||||
<groupId>net.fabricmc</groupId>
|
||||
<artifactId>mapping-io</artifactId>
|
||||
<version>0.3.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
@@ -1,27 +1,51 @@
|
||||
package fr.pandacube.lib.paper;
|
||||
|
||||
import fr.pandacube.lib.paper.event.ServerStopEvent;
|
||||
import fr.pandacube.lib.paper.json.PaperJson;
|
||||
import fr.pandacube.lib.paper.modules.PerformanceAnalysisManager;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
/**
|
||||
* Main class for pandalib-paper.
|
||||
*/
|
||||
public class PandaLibPaper {
|
||||
|
||||
private static Plugin plugin;
|
||||
|
||||
|
||||
/**
|
||||
* Method to call in plugin's {@link Plugin#onLoad()} method.
|
||||
* @param plugin the plugin instance.
|
||||
*/
|
||||
public static void onLoad(Plugin plugin) {
|
||||
PandaLibPaper.plugin = plugin;
|
||||
PaperJson.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to call in plugin's {@link Plugin#onEnable()} method.
|
||||
*/
|
||||
public static void onEnable() {
|
||||
PerformanceAnalysisManager.getInstance(); // initialize
|
||||
ServerStopEvent.init();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Method to call in plugin's {@link Plugin#onDisable()} method.
|
||||
*/
|
||||
public static void disable() {
|
||||
PerformanceAnalysisManager.getInstance().cancelInternalBossBar();
|
||||
PerformanceAnalysisManager.getInstance().deinit();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the plugin instance.
|
||||
* @return the plugin instance provided with {@link #onLoad(Plugin)}.
|
||||
*/
|
||||
public static Plugin getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
private PandaLibPaper() {}
|
||||
|
||||
}
|
||||
|
@@ -6,13 +6,65 @@ import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A basic class holding configuration for {@link PaperBackupManager}.
|
||||
*/
|
||||
@SuppressWarnings("CanBeFinal")
|
||||
public class PaperBackupConfig {
|
||||
|
||||
/**
|
||||
* Creates a new Paper backup config.
|
||||
*/
|
||||
public PaperBackupConfig() {}
|
||||
|
||||
/**
|
||||
* Set to true to enable worlds backup.
|
||||
* Defaults to true.
|
||||
*/
|
||||
public boolean worldBackupEnabled = true;
|
||||
|
||||
/**
|
||||
* Set to true to enable the backup of the working directory.
|
||||
* The workdir backup will already ignore the logs directory and any world folder (folder with a level.dat file in it).
|
||||
* Defaults to true.
|
||||
*/
|
||||
public boolean workdirBackupEnabled = true;
|
||||
|
||||
/**
|
||||
* Set to true to enable the backup of logs.
|
||||
* Defaults to true.
|
||||
*/
|
||||
public boolean logsBackupEnabled = true;
|
||||
public String scheduling = "0 2 * * *"; // cron format, here is everyday at 2am
|
||||
|
||||
/**
|
||||
* The cron-formatted scheduling of the worlds and workdir backups.
|
||||
* The default value is {@code "0 2 * * *"}, that is every day at 2am.
|
||||
*/
|
||||
public String scheduling = "0 2 * * *"; // cron format, here is every day at 2am
|
||||
|
||||
/**
|
||||
* The backup target directory.
|
||||
* Must be set (defaults to null).
|
||||
*/
|
||||
public File backupDirectory = null;
|
||||
|
||||
/**
|
||||
* The backup cleaner for the worlds backup.
|
||||
* Defaults to keep 1 backup every 3 month + the last 5 backups.
|
||||
*/
|
||||
public BackupCleaner worldBackupCleaner = BackupCleaner.KEEPING_1_EVERY_N_MONTH(3).merge(BackupCleaner.KEEPING_N_LAST(5));
|
||||
|
||||
/**
|
||||
* The backup cleaner for the workdir backup.
|
||||
* Defaults to keep 1 backup every 3 month + the last 5 backups.
|
||||
*/
|
||||
public BackupCleaner workdirBackupCleaner = BackupCleaner.KEEPING_1_EVERY_N_MONTH(3).merge(BackupCleaner.KEEPING_N_LAST(5));
|
||||
|
||||
/**
|
||||
* The list of files or directory to ignore.
|
||||
* Defaults to none.
|
||||
* The workdir backup will already ignore the logs directory and any world folder (folder with a level.dat file in it).
|
||||
*/
|
||||
public List<String> workdirIgnoreList = new ArrayList<>();
|
||||
|
||||
}
|
||||
|
@@ -21,13 +21,21 @@ import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
/**
|
||||
* The backup manager for Paper servers.
|
||||
*/
|
||||
public class PaperBackupManager extends BackupManager implements Listener {
|
||||
|
||||
private final Map<String, PaperWorldProcess> compressWorlds = new HashMap<>();
|
||||
|
||||
PaperBackupConfig config;
|
||||
|
||||
|
||||
/**
|
||||
* Instantiate a new backup manager.
|
||||
* @param config the configuration of the backups.
|
||||
*/
|
||||
public PaperBackupManager(PaperBackupConfig config) {
|
||||
super(config.backupDirectory);
|
||||
setConfig(config);
|
||||
@@ -48,13 +56,17 @@ public class PaperBackupManager extends BackupManager implements Listener {
|
||||
super.addProcess(process);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the backups config
|
||||
* @param config the new config.
|
||||
*/
|
||||
public void setConfig(PaperBackupConfig config) {
|
||||
this.config = config;
|
||||
backupQueue.forEach(this::updateProcessConfig);
|
||||
}
|
||||
|
||||
|
||||
public void updateProcessConfig(BackupProcess process) {
|
||||
private void updateProcessConfig(BackupProcess process) {
|
||||
if (process instanceof PaperWorkdirProcess) {
|
||||
process.setEnabled(config.workdirBackupEnabled);
|
||||
process.setBackupCleaner(config.workdirBackupCleaner);
|
||||
@@ -76,7 +88,9 @@ public class PaperBackupManager extends BackupManager implements Listener {
|
||||
public void run() {
|
||||
try {
|
||||
SchedulerUtil.runOnServerThreadAndWait(super::run);
|
||||
} catch (Exception e) {
|
||||
} catch (CancellationException ignored) {
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
@@ -116,12 +130,12 @@ public class PaperBackupManager extends BackupManager implements Listener {
|
||||
private final Set<String> dirtyForSave = new HashSet<>();
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onWorldLoad(WorldLoadEvent event) {
|
||||
void onWorldLoad(WorldLoadEvent event) {
|
||||
initWorldProcess(event.getWorld().getName());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onWorldSave(WorldSaveEvent event) {
|
||||
void onWorldSave(WorldSaveEvent event) {
|
||||
if (event.getWorld().getLoadedChunks().length > 0
|
||||
|| dirtyForSave.contains(event.getWorld().getName())) {
|
||||
compressWorlds.get(event.getWorld().getName()).setDirtyAfterSave();
|
||||
@@ -134,18 +148,18 @@ public class PaperBackupManager extends BackupManager implements Listener {
|
||||
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerChangeWorldEvent(PlayerChangedWorldEvent event) {
|
||||
void onPlayerChangeWorldEvent(PlayerChangedWorldEvent event) {
|
||||
dirtyForSave.add(event.getFrom().getName());
|
||||
dirtyForSave.add(event.getPlayer().getWorld().getName());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
void onPlayerJoin(PlayerJoinEvent event) {
|
||||
dirtyForSave.add(event.getPlayer().getWorld().getName());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||
void onPlayerQuit(PlayerQuitEvent event) {
|
||||
dirtyForSave.add(event.getPlayer().getWorld().getName());
|
||||
}
|
||||
|
||||
|
@@ -10,11 +10,19 @@ import net.kyori.adventure.bossbar.BossBar.Color;
|
||||
import net.kyori.adventure.bossbar.BossBar.Overlay;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
/**
|
||||
* A backup process with specific logic around Paper server.
|
||||
*/
|
||||
public abstract class PaperBackupProcess extends BackupProcess {
|
||||
|
||||
|
||||
private BossBar bossBar;
|
||||
|
||||
|
||||
/**
|
||||
* Instantiates a new backup process.
|
||||
* @param bm the associated backup manager.
|
||||
* @param id the process identifier.
|
||||
*/
|
||||
protected PaperBackupProcess(PaperBackupManager bm, String id) {
|
||||
super(bm, id);
|
||||
}
|
||||
|
@@ -1,31 +1,31 @@
|
||||
package fr.pandacube.lib.paper.backup;
|
||||
|
||||
import fr.pandacube.lib.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.function.BiPredicate;
|
||||
|
||||
/**
|
||||
* A backup process with specific logic around Paper server working directory.
|
||||
*/
|
||||
public class PaperWorkdirProcess extends PaperBackupProcess {
|
||||
|
||||
|
||||
/**
|
||||
* Instantiates a new backup process for the paper server working directory.
|
||||
* @param bm the associated backup manager.
|
||||
*/
|
||||
protected PaperWorkdirProcess(PaperBackupManager bm) {
|
||||
super(bm, "workdir");
|
||||
}
|
||||
|
||||
|
||||
public BiPredicate<File, String> getFilenameFilter() {
|
||||
return new BiPredicate<File, String>() {
|
||||
@Override
|
||||
public boolean test(File file, String path) {
|
||||
if (file.isDirectory() && new File(file, "level.dat").exists())
|
||||
return false;
|
||||
if (new File(getSourceDir(), "logs").equals(file))
|
||||
return false;
|
||||
if (file.isFile() && file.getName().endsWith(".lck"))
|
||||
return false;
|
||||
return PaperWorkdirProcess.super.getFilenameFilter().test(file, path);
|
||||
}
|
||||
return (file, path) -> {
|
||||
if (file.isDirectory() && new File(file, "level.dat").exists())
|
||||
return false;
|
||||
if (new File(getSourceDir(), "logs").equals(file))
|
||||
return false;
|
||||
if (file.isFile() && file.getName().endsWith(".lck"))
|
||||
return false;
|
||||
return PaperWorkdirProcess.super.getFilenameFilter().test(file, path);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,10 +53,4 @@ public class PaperWorkdirProcess extends PaperBackupProcess {
|
||||
return "workdir";
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void displayNextSchedule() {
|
||||
Log.info("[Backup] " + net.md_5.bungee.api.ChatColor.GRAY + getDisplayName() + net.md_5.bungee.api.ChatColor.RESET + " next backup on "
|
||||
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext())));
|
||||
}
|
||||
}
|
||||
|
@@ -1,25 +1,32 @@
|
||||
package fr.pandacube.lib.paper.backup;
|
||||
|
||||
import fr.pandacube.lib.chat.LegacyChatFormat;
|
||||
import fr.pandacube.lib.paper.scheduler.SchedulerUtil;
|
||||
import fr.pandacube.lib.paper.util.WorldUtil;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import fr.pandacube.lib.paper.world.WorldUtil;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.function.BiPredicate;
|
||||
|
||||
/**
|
||||
* A backup process with specific logic around Paper server world.
|
||||
*/
|
||||
public class PaperWorldProcess extends PaperBackupProcess {
|
||||
private final String worldName;
|
||||
|
||||
private boolean autoSave = true;
|
||||
|
||||
protected PaperWorldProcess(PaperBackupManager bm, final String n) {
|
||||
super(bm, "worlds/" + n);
|
||||
worldName = n;
|
||||
private boolean autoSave = true;
|
||||
|
||||
/**
|
||||
* Instantiates a new backup process for a world.
|
||||
* @param bm the associated backup manager.
|
||||
* @param worldName the name of the world.
|
||||
*/
|
||||
protected PaperWorldProcess(PaperBackupManager bm, final String worldName) {
|
||||
super(bm, "worlds/" + worldName);
|
||||
this.worldName = worldName;
|
||||
}
|
||||
|
||||
private World getWorld() {
|
||||
@@ -63,11 +70,11 @@ public class PaperWorldProcess extends PaperBackupProcess {
|
||||
|
||||
public void displayNextSchedule() {
|
||||
if (hasNextScheduled()) {
|
||||
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " is dirty. Next backup on "
|
||||
Log.info("[Backup] " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " is dirty. Next backup on "
|
||||
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext())));
|
||||
}
|
||||
else {
|
||||
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " is clean. Next backup not scheduled.");
|
||||
Log.info("[Backup] " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " is clean. Next backup not scheduled.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +88,7 @@ public class PaperWorldProcess extends PaperBackupProcess {
|
||||
public void setDirtyAfterSave() {
|
||||
if (!isDirty()) { // don't set dirty if it is already
|
||||
setDirtySinceNow();
|
||||
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " was saved and is now dirty. Next backup on "
|
||||
Log.info("[Backup] " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " was saved and is now dirty. Next backup on "
|
||||
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG)
|
||||
.format(new Date(getNext()))
|
||||
);
|
||||
|
@@ -1,8 +1,8 @@
|
||||
package fr.pandacube.lib.paper.commands;
|
||||
|
||||
import com.destroystokyo.paper.brigadier.BukkitBrigadierCommandSource;
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.arguments.ArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.SuggestionProvider;
|
||||
@@ -10,132 +10,153 @@ import com.mojang.brigadier.tree.CommandNode;
|
||||
import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||
import com.mojang.brigadier.tree.RootCommandNode;
|
||||
import fr.pandacube.lib.chat.Chat;
|
||||
import fr.pandacube.lib.commands.BadCommandUsage;
|
||||
import fr.pandacube.lib.commands.BrigadierCommand;
|
||||
import fr.pandacube.lib.commands.SuggestionsSupplier;
|
||||
import fr.pandacube.lib.paper.permissions.PandalibPaperPermissions;
|
||||
import fr.pandacube.lib.paper.reflect.PandalibPaperReflect;
|
||||
import fr.pandacube.lib.paper.reflect.wrapper.craftbukkit.CraftServer;
|
||||
import fr.pandacube.lib.paper.PandaLibPaper;
|
||||
import fr.pandacube.lib.paper.reflect.wrapper.craftbukkit.CraftVector;
|
||||
import fr.pandacube.lib.paper.reflect.wrapper.craftbukkit.VanillaCommandWrapper;
|
||||
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.BlockPosArgument;
|
||||
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.Commands;
|
||||
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.ComponentArgument;
|
||||
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.Coordinates;
|
||||
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.EntityArgument;
|
||||
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.EntitySelector;
|
||||
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.Vec3Argument;
|
||||
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.core.BlockPos;
|
||||
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.server.ServerPlayer;
|
||||
import fr.pandacube.lib.paper.reflect.wrapper.paper.PaperAdventure;
|
||||
import fr.pandacube.lib.paper.reflect.wrapper.paper.commands.BukkitCommandNode;
|
||||
import fr.pandacube.lib.paper.reflect.wrapper.paper.commands.PluginCommandNode;
|
||||
import fr.pandacube.lib.players.standalone.AbstractOffPlayer;
|
||||
import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer;
|
||||
import fr.pandacube.lib.players.standalone.AbstractPlayerManager;
|
||||
import fr.pandacube.lib.reflect.wrapper.ReflectWrapper;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import fr.pandacube.lib.reflect.Reflect;
|
||||
import fr.pandacube.lib.reflect.ReflectClass;
|
||||
import fr.pandacube.lib.util.log.Log;
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandMap;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.ConsoleCommandSender;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.command.defaults.BukkitCommand;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerCommandSendEvent;
|
||||
import org.bukkit.event.server.ServerLoadEvent;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.util.BlockVector;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static fr.pandacube.lib.reflect.wrapper.ReflectWrapper.unwrap;
|
||||
import static fr.pandacube.lib.reflect.wrapper.ReflectWrapper.wrap;
|
||||
|
||||
/**
|
||||
* Abstract class to hold a command to be integrated into a Paper server vanilla command dispatcher.
|
||||
*/
|
||||
public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBrigadierCommandSource> implements Listener {
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public abstract class PaperBrigadierCommand extends BrigadierCommand<CommandSourceStack> implements Listener {
|
||||
|
||||
private static final Commands vanillaCommandDispatcher;
|
||||
private static final CommandDispatcher<BukkitBrigadierCommandSource> nmsDispatcher;
|
||||
private static CommandDispatcher<CommandSourceStack> vanillaPaperDispatcher = null;
|
||||
|
||||
static {
|
||||
PandalibPaperReflect.init();
|
||||
vanillaCommandDispatcher = ReflectWrapper.wrapTyped(Bukkit.getServer(), CraftServer.class)
|
||||
.getServer()
|
||||
.vanillaCommandDispatcher();
|
||||
nmsDispatcher = vanillaCommandDispatcher.dispatcher();
|
||||
/**
|
||||
* Gets the Brigadier dispatcher provided by paper API during {@link LifecycleEvents#COMMANDS}.
|
||||
* <p>
|
||||
* This Dispatcher is not the vanilla one. Instead, Paper implementation wraps the vanilla one to handle proper registration
|
||||
* of commands from plugins.
|
||||
* @return the Brigadier dispatcher.
|
||||
*/
|
||||
public static CommandDispatcher<CommandSourceStack> getVanillaPaperDispatcher() {
|
||||
return vanillaPaperDispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a plugin command that overrides a vanilla command, so the vanilla command functionnalities are fully
|
||||
* restored (so, not only the usage, but also the suggestions and the command structure sent to the client).
|
||||
* @param name the name of the command to restore.
|
||||
* Gets the root node of the dispatcher from {@link #getVanillaPaperDispatcher()}.
|
||||
* @return the root node, or null if {@link #getVanillaPaperDispatcher()} is also null.
|
||||
*/
|
||||
public static void restoreVanillaCommand(String name) {
|
||||
CommandMap bukkitCmdMap = Bukkit.getCommandMap();
|
||||
Command bukkitCommand = bukkitCmdMap.getCommand(name);
|
||||
if (bukkitCommand != null) {
|
||||
if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCommand)) {
|
||||
//Log.info("Command /" + name + " is already a vanilla command.");
|
||||
return;
|
||||
}
|
||||
Log.info("Removing Bukkit command /" + name + " (" + getCommandIdentity(bukkitCommand) + ")");
|
||||
bukkitCmdMap.getKnownCommands().remove(name.toLowerCase(java.util.Locale.ENGLISH));
|
||||
bukkitCommand.unregister(bukkitCmdMap);
|
||||
public static RootCommandNode<CommandSourceStack> getRootNode() {
|
||||
return vanillaPaperDispatcher == null ? null : vanillaPaperDispatcher.getRoot();
|
||||
}
|
||||
|
||||
LiteralCommandNode<BukkitBrigadierCommandSource> node = (LiteralCommandNode<BukkitBrigadierCommandSource>) getRootNode().getChild(name);
|
||||
Command newCommand = new VanillaCommandWrapper(vanillaCommandDispatcher, node).__getRuntimeInstance();
|
||||
bukkitCmdMap.getKnownCommands().put(name.toLowerCase(), newCommand);
|
||||
newCommand.register(bukkitCmdMap);
|
||||
private static void updateVanillaPaperDispatcher(CommandDispatcher<CommandSourceStack> newDispatcher) {
|
||||
if (vanillaPaperDispatcher == null || newDispatcher != vanillaPaperDispatcher) {
|
||||
vanillaPaperDispatcher = newDispatcher;
|
||||
|
||||
// vanillaPaperDispatcher.getRoot() is not the real root but a wrapped root. Trying to map the fake root with the real one to trick the Paper/Brigadier (un)wrapper
|
||||
RootCommandNode<CommandSourceStack> wrappedRoot = vanillaPaperDispatcher.getRoot();
|
||||
ReflectClass<?> apiMirrorRootNodeClass = Reflect.ofClassOfInstance(wrappedRoot);
|
||||
try {
|
||||
RootCommandNode<?> unwrappedRoot = ((CommandDispatcher<?>) apiMirrorRootNodeClass.method("getDispatcher").invoke(wrappedRoot)).getRoot();
|
||||
|
||||
Reflect.ofClass(CommandNode.class).field("unwrappedCached").setValue(wrappedRoot, unwrappedRoot);
|
||||
Reflect.ofClass(CommandNode.class).field("wrappedCached").setValue(unwrappedRoot, wrappedRoot);
|
||||
|
||||
} catch (InvocationTargetException|IllegalAccessException|NoSuchMethodException|NoSuchFieldException e) {
|
||||
Log.severe("Unable to trick the Paper/Brigadier unwrapper to properly handle commands redirecting to root command node.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the vanilla instance of the Brigadier dispatcher.
|
||||
* @return the vanilla instance of the Brigadier dispatcher.
|
||||
*/
|
||||
public static CommandDispatcher<BukkitBrigadierCommandSource> getNMSDispatcher() {
|
||||
return nmsDispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root command node of the Brigadier dispatcher.
|
||||
* @return the root command node of the Brigadier dispatcher.
|
||||
* Removes a plugin command that overrides a vanilla command, so the vanilla command functionalities are fully
|
||||
* restored (so, not only the usage, but also the suggestions and the command structure sent to the client).
|
||||
* @param name the name of the command to restore.
|
||||
*/
|
||||
protected static RootCommandNode<BukkitBrigadierCommandSource> getRootNode() {
|
||||
return nmsDispatcher.getRoot();
|
||||
public static void restoreVanillaCommand(String name) {
|
||||
|
||||
PandaLibPaper.getPlugin().getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS,
|
||||
event -> updateVanillaPaperDispatcher(event.registrar().getDispatcher()));
|
||||
|
||||
|
||||
Bukkit.getServer().getScheduler().runTask(PandaLibPaper.getPlugin(), () -> {
|
||||
if (vanillaPaperDispatcher == null)
|
||||
return;
|
||||
|
||||
CommandNode<CommandSourceStack> targetCommand = vanillaPaperDispatcher.getRoot().getChild("minecraft:" + name);
|
||||
if (targetCommand == null) {
|
||||
Log.warning("There is no vanilla command '" + name + "' to restore.");
|
||||
return;
|
||||
}
|
||||
|
||||
CommandNode<CommandSourceStack> eventuallyBadCommandToReplace = vanillaPaperDispatcher.getRoot().getChild(name);
|
||||
Boolean isPluginCommand = isPluginCommand(eventuallyBadCommandToReplace);
|
||||
if (isPluginCommand != null && isPluginCommand) {
|
||||
Log.info(getCommandIdentity(eventuallyBadCommandToReplace) + " found in the dispatcher. Restoring the vanilla command.");
|
||||
vanillaPaperDispatcher.getRoot().getChildren().removeIf(c -> c.getName().equals(name));
|
||||
vanillaPaperDispatcher.getRoot().addChild(getAliasNode(targetCommand, name));
|
||||
}
|
||||
/*else if (isPluginCommand == null) {
|
||||
Log.info(getCommandIdentity(eventuallyBadCommandToReplace) + " found in the dispatcher. Unsure if we restore the vanilla command.");
|
||||
}*/
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private final Plugin plugin;
|
||||
|
||||
/**
|
||||
* The command node of this command.
|
||||
*/
|
||||
protected final LiteralCommandNode<BukkitBrigadierCommandSource> commandNode;
|
||||
protected LiteralCommandNode<CommandSourceStack> commandNode;
|
||||
/**
|
||||
* The command requested aliases.
|
||||
*/
|
||||
protected final String[] aliases;
|
||||
|
||||
/**
|
||||
* The command description.
|
||||
*/
|
||||
protected final String description;
|
||||
|
||||
private final RegistrationPolicy registrationPolicy;
|
||||
|
||||
private Set<String> registeredAliases;
|
||||
|
||||
/**
|
||||
* Instanciate this command instance.
|
||||
* Instantiate this command instance.
|
||||
*
|
||||
* @param pl the plugin instance.
|
||||
* @param regPolicy the registration policy for this command.
|
||||
@@ -143,17 +164,17 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
|
||||
public PaperBrigadierCommand(Plugin pl, RegistrationPolicy regPolicy) {
|
||||
plugin = pl;
|
||||
registrationPolicy = regPolicy;
|
||||
commandNode = buildCommand().build();
|
||||
postBuildCommand(commandNode);
|
||||
String[] aliasesTmp = getAliases();
|
||||
aliases = aliasesTmp == null ? new String[0] : aliasesTmp;
|
||||
description = getDescription();
|
||||
register();
|
||||
Bukkit.getPluginManager().registerEvents(this, plugin);
|
||||
try {
|
||||
PandalibPaperPermissions.addPermissionMapping("minecraft.command." + commandNode.getLiteral().toLowerCase(), getTargetPermission().toLowerCase());
|
||||
} catch (NoClassDefFoundError ignored) { }
|
||||
//try {
|
||||
// PandalibPaperPermissions.addPermissionMapping("minecraft.command." + commandNode.getLiteral().toLowerCase(), getTargetPermission().toLowerCase());
|
||||
//} catch (NoClassDefFoundError ignored) { }
|
||||
}
|
||||
|
||||
/**
|
||||
* Instanciate this command isntance with a registration policy of {@link RegistrationPolicy#ONLY_BASE_COMMAND}.
|
||||
* Instantiate this command instance with a registration policy of {@link RegistrationPolicy#ONLY_BASE_COMMAND}.
|
||||
* @param pl the plugin instance.
|
||||
*/
|
||||
public PaperBrigadierCommand(Plugin pl) {
|
||||
@@ -164,163 +185,179 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
|
||||
|
||||
|
||||
private void register() {
|
||||
plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> {
|
||||
updateVanillaPaperDispatcher(event.registrar().getDispatcher());
|
||||
|
||||
String[] aliases = getAliases();
|
||||
if (aliases == null)
|
||||
aliases = new String[0];
|
||||
commandNode = buildCommand().build();
|
||||
postBuildCommand(commandNode);
|
||||
|
||||
String pluginName = plugin.getName().toLowerCase();
|
||||
if (vanillaPaperDispatcher.getRoot().getChild(commandNode.getName()) != null) {
|
||||
Log.info("Command /" + commandNode.getName() + " found in the vanilla dispatcher during initial command registration. Replacing it by force.");
|
||||
vanillaPaperDispatcher.getRoot().getChildren().removeIf(c -> c.getName().equals(commandNode.getName()));
|
||||
}
|
||||
|
||||
registeredAliases = new HashSet<>();
|
||||
registerNode(commandNode, false);
|
||||
registerAlias(pluginName + ":" + commandNode.getLiteral(), true);
|
||||
registeredAliases = new HashSet<>(event.registrar().register(commandNode, description, List.of(aliases)));
|
||||
doPostRegistrationFixes();
|
||||
|
||||
for (String alias : aliases) {
|
||||
registerAlias(alias, false);
|
||||
registerAlias(pluginName + ":" + alias, true);
|
||||
if (registrationPolicy == RegistrationPolicy.ALL) {
|
||||
// enforce registration of aliases
|
||||
for (String alias : aliases) {
|
||||
if (!registeredAliases.contains(alias)) {
|
||||
Log.info("Command /" + commandNode.getName() + ": forcing registration of alias " + alias);
|
||||
registeredAliases.addAll(event.registrar().register(getAliasNode(commandNode, alias), description));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Bukkit.getServer().getScheduler().runTask(plugin, () -> {
|
||||
if (vanillaPaperDispatcher == null)
|
||||
return;
|
||||
|
||||
Set<String> forceRegistrationAgain = new HashSet<>();
|
||||
forceRegistrationAgain.add(commandNode.getName());
|
||||
if (registrationPolicy == RegistrationPolicy.ALL)
|
||||
forceRegistrationAgain.addAll(List.of(aliases));
|
||||
|
||||
for (String aliasToForce : forceRegistrationAgain) {
|
||||
CommandNode<CommandSourceStack> actualNode = vanillaPaperDispatcher.getRoot().getChild(aliasToForce);
|
||||
if (actualNode != null) {
|
||||
//Log.info("Forcing registration of alias /" + aliasToForce + " for command /" + commandNode.getName() + ": replacing " + getCommandIdentity(actualNode) + "?");
|
||||
if (PluginCommandNode.REFLECT.get().isInstance(actualNode)) {
|
||||
PluginCommandNode pcn = wrap(actualNode, PluginCommandNode.class);
|
||||
if (pcn.getPlugin().equals(plugin))
|
||||
return;
|
||||
}
|
||||
else if (BukkitCommandNode.REFLECT.get().isInstance(actualNode)) {
|
||||
BukkitCommandNode bcn = wrap(actualNode, BukkitCommandNode.class);
|
||||
if (bcn.getBukkitCommand() instanceof PluginCommand pc && pc.getPlugin().equals(plugin))
|
||||
return;
|
||||
}
|
||||
vanillaPaperDispatcher.getRoot().getChildren().removeIf(c -> c.getName().equals(aliasToForce));
|
||||
}
|
||||
/*else {
|
||||
Log.info("Forcing registration of alias /" + aliasToForce + " for command /" + commandNode.getName() + ": no command found for alias. Adding alias.");
|
||||
}*/
|
||||
LiteralCommandNode<CommandSourceStack> newPCN = unwrap(new PluginCommandNode(aliasToForce, plugin.getPluginMeta(), commandNode, description));
|
||||
vanillaPaperDispatcher.getRoot().addChild(newPCN);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private void doPostRegistrationFixes() {
|
||||
postRegistrationFixNode(new HashSet<>(), commandNode);
|
||||
}
|
||||
|
||||
private void postRegistrationFixNode(Set<CommandNode<CommandSourceStack>> fixedNodes, CommandNode<CommandSourceStack> originalNode) {
|
||||
if (originalNode instanceof RootCommandNode)
|
||||
return;
|
||||
if (fixedNodes.contains(originalNode))
|
||||
return;
|
||||
fixedNodes.add(originalNode);
|
||||
if (originalNode.getRedirect() != null) {
|
||||
try {
|
||||
@SuppressWarnings("rawtypes")
|
||||
ReflectClass<CommandNode> cmdNodeClass = Reflect.ofClass(CommandNode.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
CommandNode<CommandSourceStack> unwrappedNode = (CommandNode<CommandSourceStack>) cmdNodeClass.field("unwrappedCached").getValue(originalNode);
|
||||
if (unwrappedNode != null) {
|
||||
cmdNodeClass.field("modifier").setValue(unwrappedNode, cmdNodeClass.field("modifier").getValue(originalNode));
|
||||
cmdNodeClass.field("forks").setValue(unwrappedNode, cmdNodeClass.field("forks").getValue(originalNode));
|
||||
}
|
||||
} catch (IllegalAccessException | NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
postRegistrationFixNode(fixedNodes, originalNode.getRedirect());
|
||||
}
|
||||
else {
|
||||
try {
|
||||
for (CommandNode<CommandSourceStack> child : originalNode.getChildren())
|
||||
postRegistrationFixNode(fixedNodes, child);
|
||||
} catch (UnsupportedOperationException ignored) {
|
||||
// in case getChildren is not possible (vanilla commands are wrapped by Paper API)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void registerAlias(String alias, boolean prefixed) {
|
||||
LiteralCommandNode<BukkitBrigadierCommandSource> node = literal(alias)
|
||||
private static LiteralCommandNode<CommandSourceStack> getAliasNode(CommandNode<CommandSourceStack> commandNode, String alias) {
|
||||
return LiteralArgumentBuilder.<CommandSourceStack>literal(alias)
|
||||
.requires(commandNode.getRequirement())
|
||||
.executes(commandNode.getCommand())
|
||||
.redirect(commandNode)
|
||||
.build();
|
||||
registerNode(node, prefixed);
|
||||
}
|
||||
|
||||
|
||||
private void registerNode(LiteralCommandNode<BukkitBrigadierCommandSource> node, boolean prefixed) {
|
||||
RootCommandNode<BukkitBrigadierCommandSource> root = getRootNode();
|
||||
String name = node.getLiteral();
|
||||
boolean isAlias = node.getRedirect() == commandNode;
|
||||
boolean forceRegistration = switch (registrationPolicy) {
|
||||
case NONE -> false;
|
||||
case ONLY_BASE_COMMAND -> prefixed || !isAlias;
|
||||
case ALL -> true;
|
||||
};
|
||||
|
||||
// nmsDispatcher integration and conflit resolution
|
||||
boolean nmsRegister = false, nmsRegistered = false;
|
||||
CommandNode<BukkitBrigadierCommandSource> nmsConflited = root.getChild(name);
|
||||
if (nmsConflited != null) {
|
||||
|
||||
if (isFromThisCommand(nmsConflited)) {
|
||||
// this command is already registered in NMS. Don’t need to register again
|
||||
nmsRegistered = true;
|
||||
private static String getCommandIdentity(CommandNode<CommandSourceStack> command) {
|
||||
if (PluginCommandNode.REFLECT.get().isInstance(command)) {
|
||||
PluginCommandNode wrappedPCN = wrap(command, PluginCommandNode.class);
|
||||
return "Node /" + command.getName() + " from plugin " + wrappedPCN.getPlugin().getName();
|
||||
}
|
||||
else if (BukkitCommandNode.REFLECT.get().isInstance(command)) {
|
||||
BukkitCommandNode wrappedBCN = wrap(command, BukkitCommandNode.class);
|
||||
Command bukkitCmd = wrappedBCN.getBukkitCommand();
|
||||
if (bukkitCmd instanceof PluginCommand cmd) {
|
||||
return "Node /" + command.getName() + " wrapping Bukkit command /" + bukkitCmd.getName() + " from plugin " + cmd.getPlugin().getName();
|
||||
}
|
||||
else if (forceRegistration) {
|
||||
nmsRegister = true;
|
||||
Log.info("Overwriting Brigadier command /" + name);
|
||||
}
|
||||
else if (prefixed || !isAlias) {
|
||||
Log.severe("/" + name + " already in NMS Brigadier instance."
|
||||
+ " Wont replace it because registration is not forced for prefixed or initial name of a command.");
|
||||
}
|
||||
else { // conflict, wont replace, not forced but only an alias anyway
|
||||
Log.info("/" + name + " already in NMS Brigadier instance."
|
||||
+ " Wont replace it because registration is not forced for a non-prefixed alias.");
|
||||
else if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCmd)) {
|
||||
VanillaCommandWrapper vcw = wrap(bukkitCmd, VanillaCommandWrapper.class);
|
||||
CommandNode<CommandSourceStack> vanillaCmd = vcw.vanillaCommand();
|
||||
if (vanillaCmd != command)
|
||||
return "Node /" + command.getName() + " wrapping non-plugin command /" + bukkitCmd.getName() + " wrapping: " + getCommandIdentity(vcw.vanillaCommand());
|
||||
else
|
||||
return "Node /" + command.getName() + " wrapping non-plugin command /" + bukkitCmd.getName() + " wrapping back the node (risk of StackOverflow?)";
|
||||
}
|
||||
else
|
||||
return "Node /" + command.getName() + " wrapping " + bukkitCmd.getClass().getName() + " /" + bukkitCmd.getName();
|
||||
}
|
||||
else {
|
||||
nmsRegister = true;
|
||||
return "Node /" + command.getName() + " (unspecific)";
|
||||
}
|
||||
}
|
||||
|
||||
if (nmsRegister) {
|
||||
@SuppressWarnings("unchecked")
|
||||
var rCommandNode = ReflectWrapper.wrapTyped(root, fr.pandacube.lib.paper.reflect.wrapper.brigadier.CommandNode.class);
|
||||
rCommandNode.removeCommand(name);
|
||||
root.addChild(node);
|
||||
nmsRegistered = true;
|
||||
|
||||
private static Boolean isPluginCommand(CommandNode<CommandSourceStack> command) {
|
||||
if (PluginCommandNode.REFLECT.get().isInstance(command)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!nmsRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
registeredAliases.add(name);
|
||||
|
||||
// bukkit dispatcher conflict resolution
|
||||
boolean bukkitRegister = false;
|
||||
CommandMap bukkitCmdMap = Bukkit.getCommandMap();
|
||||
Command bukkitConflicted = bukkitCmdMap.getCommand(name);
|
||||
if (bukkitConflicted != null) {
|
||||
if (!isFromThisCommand(bukkitConflicted)) {
|
||||
if (forceRegistration) {
|
||||
bukkitRegister = true;
|
||||
Log.info("Overwriting Bukkit command /" + name
|
||||
+ " (" + getCommandIdentity(bukkitConflicted) + ")");
|
||||
}
|
||||
else if (prefixed || !isAlias) {
|
||||
Log.severe("/" + name + " already in Bukkit dispatcher (" + getCommandIdentity(bukkitConflicted) + ")." +
|
||||
" Wont replace it because registration is not forced for prefixed or initial name of a command.");
|
||||
}
|
||||
else {
|
||||
Log.info("/" + name + " already in Bukkit dispatcher (" + getCommandIdentity(bukkitConflicted) + ")." +
|
||||
" Wont replace it because registration is not forced for a non-prefixed alias.");
|
||||
}
|
||||
else if (BukkitCommandNode.REFLECT.get().isInstance(command)) {
|
||||
BukkitCommandNode wrappedBCN = wrap(command, BukkitCommandNode.class);
|
||||
Command bukkitCmd = wrappedBCN.getBukkitCommand();
|
||||
if (bukkitCmd instanceof PluginCommand) {
|
||||
return true;
|
||||
}
|
||||
else if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCmd)) {
|
||||
VanillaCommandWrapper vcw = wrap(bukkitCmd, VanillaCommandWrapper.class);
|
||||
CommandNode<CommandSourceStack> vanillaCmd = vcw.vanillaCommand();
|
||||
if (vanillaCmd != command)
|
||||
return isPluginCommand(vcw.vanillaCommand());
|
||||
else
|
||||
return false;
|
||||
}
|
||||
else
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
bukkitRegister = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bukkitRegister) {
|
||||
bukkitCmdMap.getKnownCommands().remove(name.toLowerCase());
|
||||
if (bukkitConflicted != null)
|
||||
bukkitConflicted.unregister(bukkitCmdMap);
|
||||
|
||||
Command newCommand = new VanillaCommandWrapper(vanillaCommandDispatcher, node).__getRuntimeInstance();
|
||||
bukkitCmdMap.getKnownCommands().put(name.toLowerCase(), newCommand);
|
||||
newCommand.register(bukkitCmdMap);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean isFromThisCommand(CommandNode<BukkitBrigadierCommandSource> node) {
|
||||
return node == commandNode || node.getRedirect() == commandNode;
|
||||
}
|
||||
|
||||
private boolean isFromThisCommand(Command bukkitCmd) {
|
||||
if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCmd)) {
|
||||
return isFromThisCommand(ReflectWrapper.wrapTyped((BukkitCommand) bukkitCmd, VanillaCommandWrapper.class).vanillaCommand());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String getCommandIdentity(Command bukkitCmd) {
|
||||
if (bukkitCmd instanceof PluginCommand cmd) {
|
||||
return "Bukkit command: /" + cmd.getName() + " from plugin " + cmd.getPlugin().getName();
|
||||
}
|
||||
else if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCmd)) {
|
||||
return "Vanilla command: /" + bukkitCmd.getName();
|
||||
}
|
||||
else
|
||||
return bukkitCmd.getClass().getName() + ": /" + bukkitCmd.getName();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Player command sender event handler.
|
||||
* @param event the event.
|
||||
* Gets the aliases that are actually registered in the server.
|
||||
* @return the actually registered aliases.
|
||||
*/
|
||||
@EventHandler
|
||||
public void onPlayerCommandSend(PlayerCommandSendEvent event) {
|
||||
event.getCommands().removeAll(registeredAliases.stream().map(s -> "minecraft:" + s).toList());
|
||||
protected Set<String> getRegisteredAliases() {
|
||||
return Set.copyOf(registeredAliases);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Server load event handler.
|
||||
* @param event the event.
|
||||
*/
|
||||
@EventHandler
|
||||
public void onServerLoad(ServerLoadEvent event) {
|
||||
register();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -339,6 +376,15 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
|
||||
*/
|
||||
protected abstract String getTargetPermission();
|
||||
|
||||
/**
|
||||
* Returns the permission that should be tested instead of "minecraft.command.cmdName". The conversion from the
|
||||
* minecraft prefixed permission node to the returned node is done by the {@code pandalib-paper-permissions} if it
|
||||
* is present in the classpath during runtime.
|
||||
* @return the permission that should be tested instead of "minecraft.command.cmdName".
|
||||
*/
|
||||
protected String getDescription() {
|
||||
return "A command from " + plugin.getName();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -352,13 +398,17 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
|
||||
|
||||
|
||||
|
||||
public boolean isConsole(BukkitBrigadierCommandSource wrapper) {
|
||||
|
||||
@Override
|
||||
public boolean isConsole(CommandSourceStack wrapper) {
|
||||
return isConsole(getCommandSender(wrapper));
|
||||
}
|
||||
public boolean isPlayer(BukkitBrigadierCommandSource wrapper) {
|
||||
@Override
|
||||
public boolean isPlayer(CommandSourceStack wrapper) {
|
||||
return isPlayer(getCommandSender(wrapper));
|
||||
}
|
||||
public Predicate<BukkitBrigadierCommandSource> hasPermission(String permission) {
|
||||
@Override
|
||||
public Predicate<CommandSourceStack> hasPermission(String permission) {
|
||||
return wrapper -> getCommandSender(wrapper).hasPermission(permission);
|
||||
}
|
||||
|
||||
@@ -392,7 +442,7 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
|
||||
* @param context the command context from which to get the Bukkit command sender.
|
||||
* @return the Bukkit command sender.
|
||||
*/
|
||||
public static CommandSender getCommandSender(CommandContext<BukkitBrigadierCommandSource> context) {
|
||||
public static CommandSender getCommandSender(CommandContext<CommandSourceStack> context) {
|
||||
return getCommandSender(context.getSource());
|
||||
}
|
||||
|
||||
@@ -401,8 +451,8 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
|
||||
* @param wrapper the wrapper from which to get the Bukkit command sender.
|
||||
* @return the Bukkit command sender.
|
||||
*/
|
||||
public static CommandSender getCommandSender(BukkitBrigadierCommandSource wrapper) {
|
||||
return wrapper.getBukkitSender();
|
||||
public static CommandSender getCommandSender(CommandSourceStack wrapper) {
|
||||
return wrapper.getSender();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -410,13 +460,13 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
|
||||
* @param sender the command sender.
|
||||
* @return a new instance of a command sender wrapper for the provided command sender.
|
||||
*/
|
||||
public static BukkitBrigadierCommandSource getBrigadierCommandSource(CommandSender sender) {
|
||||
return VanillaCommandWrapper.getListener(sender);
|
||||
public static CommandSourceStack getBrigadierCommandSource(CommandSender sender) {
|
||||
throw new UnsupportedOperationException("The 1.20.6 Paper API update uses a different wrapper for Brigadier command sender.");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A suggestions supplier that suggests the names of the currently connected players (that the command sender can see).
|
||||
* A suggestion supplier that suggests the names of the currently connected players (that the command sender can see).
|
||||
*/
|
||||
public static final SuggestionsSupplier<CommandSender> TAB_PLAYER_CURRENT_SERVER = (sender, ti, token, a) -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -430,7 +480,7 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
|
||||
};
|
||||
|
||||
/**
|
||||
* A suggestions supplier that suggests the names of the worlds currently loaded on this server.
|
||||
* A suggestion supplier that suggests the names of the worlds currently loaded on this server.
|
||||
*/
|
||||
public static final SuggestionsSupplier<CommandSender> TAB_WORLDS = SuggestionsSupplier.fromStreamSupplier(() -> Bukkit.getWorlds().stream().map(World::getName));
|
||||
|
||||
@@ -440,7 +490,7 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
|
||||
* @param suggestions the suggestions to wrap.
|
||||
* @return a {@link SuggestionProvider} generating the suggestions from the provided {@link SuggestionsSupplier}.
|
||||
*/
|
||||
protected SuggestionProvider<BukkitBrigadierCommandSource> wrapSuggestions(SuggestionsSupplier<CommandSender> suggestions) {
|
||||
public SuggestionProvider<CommandSourceStack> wrapSuggestions(SuggestionsSupplier<CommandSender> suggestions) {
|
||||
return wrapSuggestions(suggestions, PaperBrigadierCommand::getCommandSender);
|
||||
}
|
||||
|
||||
@@ -453,12 +503,15 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
|
||||
* @param cmd the command executor to wrap.
|
||||
* @return a wrapper command executor.
|
||||
*/
|
||||
protected static com.mojang.brigadier.Command<BukkitBrigadierCommandSource> wrapCommand(com.mojang.brigadier.Command<BukkitBrigadierCommandSource> cmd) {
|
||||
protected static com.mojang.brigadier.Command<CommandSourceStack> wrapCommand(com.mojang.brigadier.Command<CommandSourceStack> cmd) {
|
||||
return context -> {
|
||||
try {
|
||||
return cmd.run(context);
|
||||
} catch(CommandSyntaxException e) {
|
||||
throw e;
|
||||
} catch (BadCommandUsage e) {
|
||||
getCommandSender(context).sendMessage(Chat.failureText("Error while using the command: " + e.getMessage()));
|
||||
return 0;
|
||||
} catch (Throwable t) {
|
||||
Log.severe(t);
|
||||
getCommandSender(context).sendMessage(Chat.failureText("Error while executing the command: " + t));
|
||||
@@ -475,120 +528,9 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
|
||||
|
||||
|
||||
/*
|
||||
* Minecraft argument type
|
||||
* Minecraft's argument type
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new instance of the Brigadier argument type {@code minecraft:entity}.
|
||||
* @param singleTarget if this argument takes only a single target.
|
||||
* @param playersOnly if this argument takes players only.
|
||||
* @return the {@code minecraft:entity} argument type with the specified parameters.
|
||||
*/
|
||||
public static ArgumentType<Object> argumentMinecraftEntity(boolean singleTarget, boolean playersOnly) {
|
||||
if (playersOnly) {
|
||||
return singleTarget ? EntityArgument.player() : EntityArgument.players();
|
||||
}
|
||||
else {
|
||||
return singleTarget ? EntityArgument.entity() : EntityArgument.entities();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of the provided argument of type {@code minecraft:entity} (list of entities), from the provided context.
|
||||
* @param context the command execution context.
|
||||
* @param argument the argument name.
|
||||
* @return the value of the argument, or null if not found.
|
||||
*/
|
||||
public List<Entity> tryGetMinecraftEntityArgument(CommandContext<BukkitBrigadierCommandSource> context, String argument) {
|
||||
EntitySelector es = ReflectWrapper.wrap(tryGetArgument(context, argument, EntitySelector.MAPPING.runtimeClass()), EntitySelector.class);
|
||||
if (es == null)
|
||||
return null;
|
||||
List<fr.pandacube.lib.paper.reflect.wrapper.minecraft.world.Entity> nmsEntityList = es.findEntities(context.getSource());
|
||||
List<Entity> entityList = new ArrayList<>(nmsEntityList.size());
|
||||
for (fr.pandacube.lib.paper.reflect.wrapper.minecraft.world.Entity nmsEntity : nmsEntityList) {
|
||||
entityList.add(nmsEntity.getBukkitEntity());
|
||||
}
|
||||
return entityList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of the provided argument of type {@code minecraft:entity} (list of players), from the provided context.
|
||||
* @param context the command execution context.
|
||||
* @param argument the argument name.
|
||||
* @return the value of the argument, or null if not found.
|
||||
*/
|
||||
public List<Player> tryGetMinecraftEntityArgumentPlayers(CommandContext<BukkitBrigadierCommandSource> context, String argument) {
|
||||
EntitySelector es = ReflectWrapper.wrap(tryGetArgument(context, argument, EntitySelector.MAPPING.runtimeClass()), EntitySelector.class);
|
||||
if (es == null)
|
||||
return null;
|
||||
List<ServerPlayer> nmsPlayerList = es.findPlayers(context.getSource());
|
||||
List<Player> playerList = new ArrayList<>(nmsPlayerList.size());
|
||||
for (ServerPlayer nmsPlayer : nmsPlayerList) {
|
||||
playerList.add(nmsPlayer.getBukkitEntity());
|
||||
}
|
||||
return playerList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of the provided argument of type {@code minecraft:entity} (one entity), from the provided context.
|
||||
* @param context the command execution context.
|
||||
* @param argument the argument name.
|
||||
* @return the value of the argument, or null if not found.
|
||||
*/
|
||||
public Entity tryGetMinecraftEntityArgumentOneEntity(CommandContext<BukkitBrigadierCommandSource> context, String argument) {
|
||||
EntitySelector es = ReflectWrapper.wrap(tryGetArgument(context, argument, EntitySelector.MAPPING.runtimeClass()), EntitySelector.class);
|
||||
if (es == null)
|
||||
return null;
|
||||
fr.pandacube.lib.paper.reflect.wrapper.minecraft.world.Entity nmsEntity = es.findSingleEntity(context.getSource());
|
||||
return nmsEntity == null ? null : nmsEntity.getBukkitEntity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of the provided argument of type {@code minecraft:entity} (one player), from the provided context.
|
||||
* @param context the command execution context.
|
||||
* @param argument the argument name.
|
||||
* @return the value of the argument, or null if not found.
|
||||
*/
|
||||
public Player tryGetMinecraftEntityArgumentOnePlayer(CommandContext<BukkitBrigadierCommandSource> context, String argument) {
|
||||
EntitySelector es = ReflectWrapper.wrap(tryGetArgument(context, argument, EntitySelector.MAPPING.runtimeClass()), EntitySelector.class);
|
||||
if (es == null)
|
||||
return null;
|
||||
ServerPlayer nmsPlayer = es.findSinglePlayer(context.getSource());
|
||||
return nmsPlayer == null ? null : nmsPlayer.getBukkitEntity();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new instance of the Brigadier argument type {@code minecraft:block_pos}.
|
||||
* @return the {@code minecraft:block_pos} argument type.
|
||||
*/
|
||||
public static ArgumentType<Object> argumentMinecraftBlockPosition() {
|
||||
return BlockPosArgument.blockPos();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of the provided argument of type {@code minecraft:block_pos}, from the provided context.
|
||||
* @param context the command execution context.
|
||||
* @param argument the argument name.
|
||||
* @param deflt a defualt value if the argument is not found.
|
||||
* @return the value of the argument.
|
||||
*/
|
||||
public BlockVector tryGetMinecraftBlockPositionArgument(CommandContext<BukkitBrigadierCommandSource> context,
|
||||
String argument, BlockVector deflt) {
|
||||
return tryGetArgument(context, argument, Coordinates.MAPPING.runtimeClass(), nmsCoord -> {
|
||||
BlockPos bp = ReflectWrapper.wrap(nmsCoord, Coordinates.class).getBlockPos(context.getSource());
|
||||
return new BlockVector(bp.getX(), bp.getY(), bp.getZ());
|
||||
}, deflt);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new instance of the Brigadier argument type {@code minecraft:vec3}.
|
||||
* @return the {@code minecraft:vec3} argument type.
|
||||
@@ -601,14 +543,14 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
|
||||
* Gets the value of the provided argument of type {@code minecraft:vec3}, from the provided context.
|
||||
* @param context the command execution context.
|
||||
* @param argument the argument name.
|
||||
* @param deflt a defualt value if the argument is not found.
|
||||
* @param deflt a default value if the argument is not found.
|
||||
* @return the value of the argument.
|
||||
*/
|
||||
public Vector tryGetMinecraftVec3Argument(CommandContext<BukkitBrigadierCommandSource> context, String argument,
|
||||
public Vector tryGetMinecraftVec3Argument(CommandContext<CommandSourceStack> context, String argument,
|
||||
Vector deflt) {
|
||||
return tryGetArgument(context, argument, Coordinates.MAPPING.runtimeClass(),
|
||||
nmsCoord -> CraftVector.toBukkit(
|
||||
ReflectWrapper.wrap(nmsCoord, Coordinates.class).getPosition(context.getSource())
|
||||
return tryGetArgument(context, argument, Coordinates.REFLECT.get(),
|
||||
nmsCoordinate -> CraftVector.toBukkit(
|
||||
wrap(nmsCoordinate, Coordinates.class).getPosition(context.getSource())
|
||||
),
|
||||
deflt);
|
||||
}
|
||||
@@ -616,44 +558,11 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new instance of the Brigadier argument type {@code minecraft:component}.
|
||||
* @return the {@code minecraft:component} argument type.
|
||||
*/
|
||||
public static ArgumentType<Object> argumentMinecraftChatComponent() {
|
||||
return ComponentArgument.textComponent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of the provided argument of type {@code minecraft:component}, from the provided context.
|
||||
* @param context the command execution context.
|
||||
* @param argument the argument name.
|
||||
* @param deflt a defualt value if the argument is not found.
|
||||
* @return the value of the argument.
|
||||
*/
|
||||
public Component tryGetMinecraftChatComponentArgument(CommandContext<BukkitBrigadierCommandSource> context,
|
||||
String argument, Component deflt) {
|
||||
return tryGetArgument(context, argument,
|
||||
fr.pandacube.lib.paper.reflect.wrapper.minecraft.network.chat.Component.MAPPING.runtimeClass(),
|
||||
nmsComp -> PaperAdventure.asAdventure(
|
||||
ReflectWrapper.wrap(nmsComp,
|
||||
fr.pandacube.lib.paper.reflect.wrapper.minecraft.network.chat.Component.class)
|
||||
),
|
||||
deflt);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* All possible choices on how to force the registration of a command, based on certain conditions.
|
||||
*/
|
||||
public enum RegistrationPolicy {
|
||||
/**
|
||||
* Do not force to register a command node or an alias if there is already a command with that name in the
|
||||
* vanilla Brigadier dispatcher.
|
||||
* Note that all plugin-name-prefixed aliases will be registered anyway.
|
||||
*/
|
||||
NONE,
|
||||
/**
|
||||
* Force only the base command (but not the aliases) to be registered, even if a command with that name already
|
||||
* exists in the vanilla Brigadier dispatcher.
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user