1.8
JaTiTV 2 years ago
commit 3195097356

2
.gitattributes vendored

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

113
.gitignore vendored

@ -0,0 +1,113 @@
# User-specific stuff
.idea/
*.iml
*.ipr
*.iws
# IntelliJ
out/
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
.flattened-pom.xml
# Common working directory
run/

@ -0,0 +1,24 @@
<p align="center">
<img src="https://i.imgur.com/UitOjTI.png" width="300">
</p>
---
# Links
* [Download](https://www.spigotmc.org/resources/wonderbagshop.89234/)
* [Discord](http://dc.T2Code.net)
--
### English
With PaPiTest you can query and test all placeholders of PlaceholderAPI and other plugins that use the PlaceholderAPI. This can also be very useful for programming testing.
Just come to the support Discord for further questions.
---
### German
Mit PaPiTest kannst du sämtliche Placeholder von PlaceholderAPI und anderen Plugins die die PlaceholderAPI nutzen abfragen und testen. Dies kann auch zum Programmieren zum Testen sehr nützlich sein.
Komm für weitere Fragen einfach auf den Support Discord.

@ -0,0 +1,84 @@
<?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">
<modelVersion>4.0.0</modelVersion>
<groupId>de.jatitv</groupId>
<artifactId>PaPiTest</artifactId>
<version>0.0.1</version>
<packaging>jar</packaging>
<name>PaPiTest</name>
<description>Test your Placeholder</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<repositories>
<repository>
<id>spigotmc-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/groups/public/</url>
</repository>
<repository>
<id>T2Code</id>
<url>https://repo.t2code.net/repository/T2Code/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.16.5-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.t2code</groupId>
<artifactId>T2CodeLib</artifactId>
<version>11.4</version>
</dependency>
</dependencies>
</project>

@ -0,0 +1,36 @@
package de.jatitv.papitest;
import net.t2code.lib.Spigot.Lib.minecraftVersion.MCVersion;
public class Util {
private static double requiredT2CodeLibVersion = 11.4;
private static String prefix = "§8[§5PaPi§6Test§8]";
private static Integer spigotID = 90439;
private static Integer bstatsID = 10342;
private static String spigot = "https://www.spigotmc.org/resources/" + spigotID;
private static String discord = "http://dc.t2code.net";
public static double getRequiredT2CodeLibVersion() {
return requiredT2CodeLibVersion;
}
public static String getPrefix() {
return prefix;
}
public static Integer getSpigotID() {
return spigotID;
}
public static Integer getBstatsID() {
return bstatsID;
}
public static String getSpigot() {
return spigot;
}
public static String getDiscord() {
return discord;
}
}

@ -0,0 +1,53 @@
// This claas was created by JaTiTV
package de.jatitv.papitest.commands;
import de.jatitv.papitest.Util;
import de.jatitv.papitest.config.Config;
import de.jatitv.papitest.system.Main;
import me.clip.placeholderapi.PlaceholderAPI;
import net.t2code.lib.Spigot.Lib.minecraftVersion.MCVersion;
import net.t2code.lib.Spigot.Lib.plugins.PluginCheck;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.awt.color.CMMException;
public class CmdExecuter implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (sender.hasPermission("papitest.admin") || sender.isOp()) {
if (args.length == 0) {
sender.sendMessage(Util.getPrefix() + " §cPlease use §6/papitest §7%placeholder%");
} else {
switch (args[0].toLowerCase()) {
case "<placeholder>":
sender.sendMessage(Util.getPrefix() + " §cPlease use §6/papitest §7%placeholder%");
break;
case "info":
case "plugin":
case "pl":
case "version":
case "ver":
Commands.info(sender);
break;
case "reload":
case "rl":
Commands.reload(sender);
break;
default:
Commands.use(sender, args);
break;
}
}
} else {
sender.sendMessage(Util.getPrefix() + " §cYou do not have permission for §5PaPi§6Test§c!");
}
return false;
}
}

@ -0,0 +1,69 @@
package de.jatitv.papitest.commands;
import de.jatitv.papitest.Util;
import de.jatitv.papitest.config.Config;
import de.jatitv.papitest.system.Main;
import me.clip.placeholderapi.PlaceholderAPI;
import net.t2code.lib.Spigot.Lib.messages.T2CodeTemplate;
import net.t2code.lib.Spigot.Lib.minecraftVersion.MCVersion;
import net.t2code.lib.Spigot.Lib.plugins.PluginCheck;
import net.t2code.lib.Spigot.Lib.update.UpdateAPI;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Commands {
protected static void info(CommandSender sender) {
T2CodeTemplate.sendInfo(sender, Util.getPrefix(), Util.getSpigot(), Util.getDiscord(), Main.autor, Main.version, UpdateAPI.PluginVersionen.get(Main.plugin.getName()).publicVersion);
}
protected static void reload(CommandSender sender) {
try {
Reload.reload(sender);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
protected static void use(CommandSender sender, String[] args) {
if (!PluginCheck.papi()) {
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + "§4\n" + Util.getPrefix() + " §4PlaceholderAPI could not be connected / found! " +
"§9Please download it here: §6https://www.spigotmc.org/resources/placeholderapi.6245/§4\n" + Util.getPrefix());
if (sender instanceof Player) {
sender.sendMessage(Util.getPrefix() + "§4\n" + Util.getPrefix() + " §4PlaceholderAPI could not be connected / found! §9Please download it here: " +
"§6https://www.spigotmc.org/resources/placeholderapi.6245/§4\n" + Util.getPrefix());
}
return;
}
List<String> results = Arrays.stream(args).filter(a -> a.contains("-p:")).collect(Collectors.toList());
CommandSender executor;
if(results.size() == 0){
executor = sender;
}else {
String targetSt = results.get(0).replace("-p:","");
Player target = Bukkit.getPlayer(targetSt);
if (target == null) {
sender.sendMessage(Util.getPrefix()+ " §4Player §6" + targetSt + " §4is not Online!");
return;
}
executor = target;
}
if(executor instanceof Player){
execute(sender,(Player)executor, args[0]);
}else {
sender.sendMessage(Util.getPrefix() + " §4If you want to execute the command via console, then you have to write §6-p:<player> §4to it, as which player you want to query the placeholder."); //todo
}
}
protected static void execute(CommandSender sender, Player target, String placeholder) {
sender.sendMessage(Util.getPrefix() + " §b" + placeholder + " §7-> " + "§6" + PlaceholderAPI.setPlaceholders(target, placeholder));
if (!MCVersion.minecraft1_8 && Config.Titel && sender instanceof Player) {
((Player)sender).sendTitle("§5PaPi§6Test§8: " + "§b" + placeholder, "§6" + PlaceholderAPI.setPlaceholders(target, placeholder));
}
}
}

@ -0,0 +1,49 @@
// This claas was created by JaTiTV
package de.jatitv.papitest.commands;
import de.jatitv.papitest.Util;
import de.jatitv.papitest.config.Config;
import de.jatitv.papitest.system.Main;
import net.t2code.lib.Spigot.Lib.plugins.PluginCheck;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
public class Reload {
public static void reload(CommandSender sender) throws InterruptedException {
if (sender instanceof Player) {
sender.sendMessage(Util.getPrefix() + " §6Plugin reload...");
}
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §8-------------------------------");
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §6Plugin reload...");
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §8-------------------------------");
Config.configCreate();
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §2");
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §8-------------------------------");
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §2");
if (PluginCheck.papi()) {
if (sender instanceof Player) {
sender.sendMessage(Util.getPrefix() + " §2PlaceholderAPI successfully connected!");
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §2PlaceholderAPI successfully connected!");
} else Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §2PlaceholderAPI successfully connected!");
} else {
if (sender instanceof Player) {
sender.sendMessage(Util.getPrefix() + "§4\n" + Util.getPrefix() + " §4PlaceholderAPI could not be connected / found! " +
"§9Please download it here: §6https://www.spigotmc.org/resources/placeholderapi.6245/§4\n" + Util.getPrefix());
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + "§4\n" + Util.getPrefix() + " §4PlaceholderAPI could not be connected / found! " +
"§9Please download it here: §6https://www.spigotmc.org/resources/placeholderapi.6245/§4\n" + Util.getPrefix());
} else Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + "§4\n" + Util.getPrefix() + " §4PlaceholderAPI could not be connected / found! " +
"§9Please download it here: §6https://www.spigotmc.org/resources/placeholderapi.6245/§4\n" + Util.getPrefix());
}
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + "§2");
if (sender instanceof Player) {
sender.sendMessage(Util.getPrefix() + " §2Plugin successfully reloaded.");
}
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §8-------------------------------");
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §2Plugin successfully reloaded.");
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §8-------------------------------");
}
}

@ -0,0 +1,82 @@
// This claas was created by JaTiTV
package de.jatitv.papitest.commands;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class TabComplete implements TabCompleter {
private static final HashMap<String, String> arg1 = new HashMap<String, String>() {{
put("<placeholder>", "papitest.admin");
put("info", "papitest.admin");
put("reload", "papitest.admin");
}};
@Override
public List<String> onTabComplete(CommandSender sender, Command cmd, String s, String[] args) {
List<String> list = new ArrayList<>();
if (sender instanceof Player) {
Player p = (Player) sender;
switch (args.length) {
case 1:
for (String command : arg1.keySet()) {
if (hasPermission(p, arg1.get(command)) && passend(command, args[0])) {
list.add(command);
}
}
break;
case 2:
if (sender.hasPermission("papitest.admin")) {
if (args[1].contains("-")) {
if (args[1].contains("-p:")) {
for (Player player : Bukkit.getOnlinePlayers()) {
if (passend("-p:" + player.getName(), args[1])) {
list.add("-p:" + player.getName());
}
}
} else {
list.add("-p:");
}
}
}
break;
}
}
return list;
}
private static Boolean passend(String command, String arg) {
for (int i = 0; i < arg.toUpperCase().length(); i++) {
if (arg.toUpperCase().length() >= command.toUpperCase().length()) {
return false;
} else {
if (arg.toUpperCase().charAt(i) != command.toUpperCase().charAt(i)) {
return false;
}
}
}
return true;
}
public static boolean hasPermission(Player player, String permission) {
if (player.isOp()) {
return true;
}
String[] Permissions = permission.split(";");
for (String perm : Permissions) {
if (player.hasPermission(perm)) {
return true;
}
}
return false;
}
}

@ -0,0 +1,53 @@
// This claas was created by JaTiTV
package de.jatitv.papitest.config;
import de.jatitv.papitest.Util;
import de.jatitv.papitest.system.Main;
import net.t2code.lib.Spigot.Lib.minecraftVersion.MCVersion;
import org.bukkit.Bukkit;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
public class Config {
public static Integer ConfigVersion = 1;
public static Boolean UpdateCheckOnJoin = true;
public static Boolean Titel = true;
public static void configCreate() {
File configYML = new File(Main.thisp().getDataFolder().getPath(), "Config.yml");
YamlConfiguration yamlConfiguration = YamlConfiguration.loadConfiguration(configYML);
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §4Config.yml load...");
yamlConfiguration.set("ConfigVersion", ConfigVersion);
if (yamlConfiguration.contains("Plugin.UpdateCheckOnJoin")) {
UpdateCheckOnJoin = yamlConfiguration.getBoolean("Plugin.UpdateCheckOnJoin");
} else {
yamlConfiguration.set("Plugin.UpdateCheckOnJoin", true);
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §4Setting §6UpdateCheckOnJoin §4was added to §9Config.yml§4!");
}
if (!MCVersion.minecraft1_8) {
if (yamlConfiguration.contains("Titel.Enable")) {
Titel = yamlConfiguration.getBoolean("Titel.Enable");
} else {
yamlConfiguration.set("Titel.Enable", true);
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §4Setting §6Titel Enable §4was added to §9Config.yml§4!");
}
}
try {
yamlConfiguration.save(configYML);
} catch (
IOException e) {
e.printStackTrace();
}
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §2Config.yml loaded successfully.");
}
}

@ -0,0 +1,23 @@
// This claas was created by JaTiTV
package de.jatitv.papitest.system;
import de.jatitv.papitest.Util;
import de.jatitv.papitest.config.Config;
import net.t2code.lib.Spigot.Lib.update.UpdateAPI;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.scheduler.BukkitRunnable;
public class JoinEvent implements Listener {
@EventHandler
public void onJoinEvent(PlayerJoinEvent event) {
Player player = event.getPlayer();
UpdateAPI.join(Main.plugin, Util.getPrefix(),"papitest.admin",player,Util.getSpigot(), Util.getDiscord());
}
}

@ -0,0 +1,96 @@
// This claas was created by JaTiTV
package de.jatitv.papitest.system;
import de.jatitv.papitest.Util;
import de.jatitv.papitest.commands.CmdExecuter;
import de.jatitv.papitest.commands.TabComplete;
import de.jatitv.papitest.config.Config;
import net.t2code.lib.Spigot.Lib.plugins.PluginCheck;
import net.t2code.lib.Spigot.Lib.update.UpdateAPI;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
public final class Main extends JavaPlugin {
public static Main plugin;
public static String version;
public static List<String> autor;
public static Main getPlugin() {
return plugin;
}
public static Plugin thisp() {
return plugin;
}
@Override
public void onEnable() {
// Plugin startup logic
plugin = this;
autor = plugin.getDescription().getAuthors();
version = plugin.getDescription().getVersion();
if (pluginNotFound("T2CodeLib", 96388, Util.getRequiredT2CodeLibVersion())) return;
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §4Plugin load...");
UpdateAPI.onUpdateCheck(Main.plugin, Util.getPrefix(), Util.getSpigot(), Util.getSpigotID(), Util.getDiscord());
Metrics.Bstats(Main.plugin, Util.getBstatsID());
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §8-------------------------------");
Config.configCreate();
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §8-------------------------------");
if (PluginCheck.papi()) {
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §2PlaceholderAPI successfully connected!");
} else {
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + "§4\n" + Util.getPrefix() + " §4PlaceholderAPI could not be connected / found! " +
"§9Please download it here: §6https://www.spigotmc.org/resources/placeholderapi.6245/§4\n" + Util.getPrefix());
}
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §8-------------------------------");
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §2Plugin loaded successfully.");
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §8-------------------------------");
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §2Plugin loaded successfully.");
getCommand("papitest").setExecutor(new CmdExecuter());
getCommand("papitest").setTabCompleter(new TabComplete());
Bukkit.getServer().getPluginManager().registerEvents(new JoinEvent(), this);
int pluginId = 10767; // <-- Replace with the id of your plugin!
Metrics metrics = new Metrics(this, pluginId);
}
@Override
public void onDisable() {
// Plugin shutdown logic
}
public static Boolean pluginNotFound(String pl, Integer spigotID, double ver) {
if (Bukkit.getPluginManager().getPlugin(pl) == null) {
plugin.getLogger().log(Level.SEVERE, "Plugin can not be loaded!");
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §e" + pl + " §4could not be found. Please download it here: " +
"§6https://spigotmc.org/resources/" + pl + "." + spigotID + " §4to be able to use this plugin.");
plugin.getPluginLoader().disablePlugin(plugin);
return true;
} else {
if (Double.parseDouble(Objects.requireNonNull(Bukkit.getPluginManager().getPlugin(pl)).getDescription().getVersion()) < ver) {
plugin.getLogger().log(Level.SEVERE, "Plugin can not be loaded!");
Bukkit.getConsoleSender().sendMessage(Util.getPrefix() + " §e" + pl + " §4is out of date! This plugin requires at least version §2" +
ver + " §4of §6" + pl + " §4Please update it here: §6https://spigotmc.org/resources/" + pl + "." + spigotID + " §4to use this version of " +
plugin.getDescription().getName() + ".");
plugin.getPluginLoader().disablePlugin(plugin);
return true;
}
return false;
}
}
}

@ -0,0 +1,856 @@
// This claas was created by JaTiTV
package de.jatitv.papitest.system;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.zip.GZIPOutputStream;
import javax.net.ssl.HttpsURLConnection;
import org.bukkit.Bukkit;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
public class Metrics {
public static void Bstats(Plugin plugin, int bstatsID) {
int pluginId = bstatsID; // <-- Replace with the id of your plugin!
Metrics metrics = new Metrics((JavaPlugin) plugin, pluginId);
}
private final Plugin plugin;
private final MetricsBase metricsBase;
/**
* Creates a new Metrics instance.
*
* @param plugin Your plugin instance.
* @param serviceId The id of the service. It can be found at <a
* href="https://bstats.org/what-is-my-plugin-id">What is my plugin id?</a>
*/
public Metrics(JavaPlugin plugin, int serviceId) {
this.plugin = plugin;
// Get the config file
File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats");
File configFile = new File(bStatsFolder, "config.yml");
YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile);
if (!config.isSet("serverUuid")) {
config.addDefault("enabled", true);
config.addDefault("serverUuid", UUID.randomUUID().toString());
config.addDefault("logFailedRequests", false);
config.addDefault("logSentData", false);
config.addDefault("logResponseStatusText", false);
// Inform the server owners about bStats
config
.options()
.header(
"bStats (https://bStats.org) collects some basic information for plugin authors, like how\n"
+ "many people use their plugin and their total player count. It's recommended to keep bStats\n"
+ "enabled, but if you're not comfortable with this, you can turn this setting off. There is no\n"
+ "performance penalty associated with having metrics enabled, and data sent to bStats is fully\n"
+ "anonymous.")
.copyDefaults(true);
try {
config.save(configFile);
} catch (IOException ignored) {
}
}
// Load the data
boolean enabled = config.getBoolean("enabled", true);
String serverUUID = config.getString("serverUuid");
boolean logErrors = config.getBoolean("logFailedRequests", false);
boolean logSentData = config.getBoolean("logSentData", false);
boolean logResponseStatusText = config.getBoolean("logResponseStatusText", false);
metricsBase =
new MetricsBase(
"bukkit",
serverUUID,
serviceId,
enabled,
this::appendPlatformData,
this::appendServiceData,
submitDataTask -> Bukkit.getScheduler().runTask(plugin, submitDataTask),
plugin::isEnabled,
(message, error) -> this.plugin.getLogger().log(Level.WARNING, message, error),
(message) -> this.plugin.getLogger().log(Level.INFO, message),
logErrors,
logSentData,
logResponseStatusText);
}
/**
* Adds a custom chart.
*
* @param chart The chart to add.
*/
public void addCustomChart(CustomChart chart) {
metricsBase.addCustomChart(chart);
}
private void appendPlatformData(JsonObjectBuilder builder) {
builder.appendField("playerAmount", getPlayerAmount());
builder.appendField("onlineMode", Bukkit.getOnlineMode() ? 1 : 0);
builder.appendField("bukkitVersion", Bukkit.getVersion());
builder.appendField("bukkitName", Bukkit.getName());
builder.appendField("javaVersion", System.getProperty("java.version"));
builder.appendField("osName", System.getProperty("os.name"));
builder.appendField("osArch", System.getProperty("os.arch"));
builder.appendField("osVersion", System.getProperty("os.version"));
builder.appendField("coreCount", Runtime.getRuntime().availableProcessors());
}
private void appendServiceData(JsonObjectBuilder builder) {
builder.appendField("pluginVersion", plugin.getDescription().getVersion());
}
private int getPlayerAmount() {
try {
// Around MC 1.8 the return type was changed from an array to a collection,
// This fixes java.lang.NoSuchMethodError:
// org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection;
Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers");
return onlinePlayersMethod.getReturnType().equals(Collection.class)
? ((Collection<?>) onlinePlayersMethod.invoke(Bukkit.getServer())).size()
: ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length;
} catch (Exception e) {
// Just use the new method if the reflection failed
return Bukkit.getOnlinePlayers().size();
}
}
public static class MetricsBase {
/** The version of the Metrics class. */
public static final String METRICS_VERSION = "2.2.1";
private static final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(1, task -> new Thread(task, "bStats-Metrics"));
private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s";
private final String platform;
private final String serverUuid;
private final int serviceId;
private final Consumer<JsonObjectBuilder> appendPlatformDataConsumer;
private final Consumer<JsonObjectBuilder> appendServiceDataConsumer;
private final Consumer<Runnable> submitTaskConsumer;
private final Supplier<Boolean> checkServiceEnabledSupplier;
private final BiConsumer<String, Throwable> errorLogger;
private final Consumer<String> infoLogger;
private final boolean logErrors;
private final boolean logSentData;
private final boolean logResponseStatusText;
private final Set<CustomChart> customCharts = new HashSet<>();
private final boolean enabled;
/**
* Creates a new MetricsBase class instance.
*
* @param platform The platform of the service.
* @param serviceId The id of the service.
* @param serverUuid The server uuid.
* @param enabled Whether or not data sending is enabled.
* @param appendPlatformDataConsumer A consumer that receives a {@code JsonObjectBuilder} and
* appends all platform-specific data.
* @param appendServiceDataConsumer A consumer that receives a {@code JsonObjectBuilder} and
* appends all service-specific data.
* @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be
* used to delegate the data collection to a another thread to prevent errors caused by
* concurrency. Can be {@code null}.
* @param checkServiceEnabledSupplier A supplier to check if the service is still enabled.
* @param errorLogger A consumer that accepts log message and an error.
* @param infoLogger A consumer that accepts info log messages.
* @param logErrors Whether or not errors should be logged.
* @param logSentData Whether or not the sent data should be logged.
* @param logResponseStatusText Whether or not the response status text should be logged.
*/
public MetricsBase(
String platform,
String serverUuid,
int serviceId,
boolean enabled,
Consumer<JsonObjectBuilder> appendPlatformDataConsumer,
Consumer<JsonObjectBuilder> appendServiceDataConsumer,
Consumer<Runnable> submitTaskConsumer,
Supplier<Boolean> checkServiceEnabledSupplier,
BiConsumer<String, Throwable> errorLogger,
Consumer<String> infoLogger,
boolean logErrors,
boolean logSentData,
boolean logResponseStatusText) {
this.platform = platform;
this.serverUuid = serverUuid;
this.serviceId = serviceId;
this.enabled = enabled;
this.appendPlatformDataConsumer = appendPlatformDataConsumer;
this.appendServiceDataConsumer = appendServiceDataConsumer;
this.submitTaskConsumer = submitTaskConsumer;
this.checkServiceEnabledSupplier = checkServiceEnabledSupplier;
this.errorLogger = errorLogger;
this.infoLogger = infoLogger;
this.logErrors = logErrors;
this.logSentData = logSentData;
this.logResponseStatusText = logResponseStatusText;
checkRelocation();
if (enabled) {
startSubmitting();
}
}
public void addCustomChart(CustomChart chart) {
this.customCharts.add(chart);
}
private void startSubmitting() {
final Runnable submitTask =
() -> {
if (!enabled || !checkServiceEnabledSupplier.get()) {
// Submitting data or service is disabled
scheduler.shutdown();
return;
}
if (submitTaskConsumer != null) {
submitTaskConsumer.accept(this::submitData);
} else {
this.submitData();
}
};
// Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution
// of requests on the
// bStats backend. To circumvent this problem, we introduce some randomness into the initial
// and second delay.
// WARNING: You must not modify and part of this Metrics class, including the submit delay or
// frequency!
// WARNING: Modifying this code will get your plugin banned on bStats. Just don't do it!
long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3));
long secondDelay = (long) (1000 * 60 * (Math.random() * 30));
scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS);
scheduler.scheduleAtFixedRate(
submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS);
}
private void submitData() {
final JsonObjectBuilder baseJsonBuilder = new JsonObjectBuilder();
appendPlatformDataConsumer.accept(baseJsonBuilder);
final JsonObjectBuilder serviceJsonBuilder = new JsonObjectBuilder();
appendServiceDataConsumer.accept(serviceJsonBuilder);
JsonObjectBuilder.JsonObject[] chartData =
customCharts.stream()
.map(customChart -> customChart.getRequestJsonObject(errorLogger, logErrors))
.filter(Objects::nonNull)
.toArray(JsonObjectBuilder.JsonObject[]::new);
serviceJsonBuilder.appendField("id", serviceId);
serviceJsonBuilder.appendField("customCharts", chartData);
baseJsonBuilder.appendField("service", serviceJsonBuilder.build());
baseJsonBuilder.appendField("serverUUID", serverUuid);
baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION);
JsonObjectBuilder.JsonObject data = baseJsonBuilder.build();
scheduler.execute(
() -> {
try {
// Send the data
sendData(data);
} catch (Exception e) {
// Something went wrong! :(
if (logErrors) {
errorLogger.accept("Could not submit bStats metrics data", e);
}
}
});
}
private void sendData(JsonObjectBuilder.JsonObject data) throws Exception {
if (logSentData) {
infoLogger.accept("Sent bStats metrics data: " + data.toString());
}
String url = String.format(REPORT_URL, platform);
HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
// Compress the data to save bandwidth
byte[] compressedData = compress(data.toString());
connection.setRequestMethod("POST");
connection.addRequestProperty("Accept", "application/json");
connection.addRequestProperty("Connection", "close");
connection.addRequestProperty("Content-Encoding", "gzip");
connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length));
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("User-Agent", "Metrics-Service/1");
connection.setDoOutput(true);
try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) {
outputStream.write(compressedData);
}
StringBuilder builder = new StringBuilder();
try (BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
String line;
while ((line = bufferedReader.readLine()) != null) {
builder.append(line);
}
}
if (logResponseStatusText) {
infoLogger.accept("Sent data to bStats and received response: " + builder);
}
}
/** Checks that the class was properly relocated. */
private void checkRelocation() {
// You can use the property to disable the check in your test environment
if (System.getProperty("bstats.relocatecheck") == null
|| !System.getProperty("bstats.relocatecheck").equals("false")) {
// Maven's Relocate is clever and changes strings, too. So we have to use this little
// "trick" ... :D
final String defaultPackage =
new String(new byte[] {'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'});
final String examplePackage =
new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'});
// We want to make sure no one just copy & pastes the example and uses the wrong package
// names
if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage)
|| MetricsBase.class.getPackage().getName().startsWith(examplePackage)) {
throw new IllegalStateException("bStats Metrics class has not been relocated correctly!");
}
}
}
/**
* Gzips the given string.
*
* @param str The string to gzip.
* @return The gzipped string.
*/
private static byte[] compress(final String str) throws IOException {
if (str == null) {
return null;
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) {
gzip.write(str.getBytes(StandardCharsets.UTF_8));
}
return outputStream.toByteArray();
}
}
public static class AdvancedBarChart extends CustomChart {
private final Callable<Map<String, int[]>> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public AdvancedBarChart(String chartId, Callable<Map<String, int[]>> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
Map<String, int[]> map = callable.call();
if (map == null || map.isEmpty()) {
// Null = skip the chart
return null;
}
boolean allSkipped = true;
for (Map.Entry<String, int[]> entry : map.entrySet()) {
if (entry.getValue().length == 0) {
// Skip this invalid
continue;
}
allSkipped = false;
valuesBuilder.appendField(entry.getKey(), entry.getValue());
}
if (allSkipped) {
// Null = skip the chart
return null;
}
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
}
}
public static class SimpleBarChart extends CustomChart {
private final Callable<Map<String, Integer>> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public SimpleBarChart(String chartId, Callable<Map<String, Integer>> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
Map<String, Integer> map = callable.call();
if (map == null || map.isEmpty()) {
// Null = skip the chart
return null;
}
for (Map.Entry<String, Integer> entry : map.entrySet()) {
valuesBuilder.appendField(entry.getKey(), new int[] {entry.getValue()});
}
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
}
}
public static class MultiLineChart extends CustomChart {
private final Callable<Map<String, Integer>> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public MultiLineChart(String chartId, Callable<Map<String, Integer>> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
Map<String, Integer> map = callable.call();
if (map == null || map.isEmpty()) {
// Null = skip the chart
return null;
}
boolean allSkipped = true;
for (Map.Entry<String, Integer> entry : map.entrySet()) {
if (entry.getValue() == 0) {
// Skip this invalid
continue;
}
allSkipped = false;
valuesBuilder.appendField(entry.getKey(), entry.getValue());
}
if (allSkipped) {
// Null = skip the chart
return null;
}
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
}
}
public static class AdvancedPie extends CustomChart {
private final Callable<Map<String, Integer>> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public AdvancedPie(String chartId, Callable<Map<String, Integer>> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
Map<String, Integer> map = callable.call();
if (map == null || map.isEmpty()) {
// Null = skip the chart
return null;
}
boolean allSkipped = true;
for (Map.Entry<String, Integer> entry : map.entrySet()) {
if (entry.getValue() == 0) {
// Skip this invalid
continue;
}
allSkipped = false;
valuesBuilder.appendField(entry.getKey(), entry.getValue());
}
if (allSkipped) {
// Null = skip the chart
return null;
}
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
}
}
public abstract static class CustomChart {
private final String chartId;
protected CustomChart(String chartId) {
if (chartId == null) {
throw new IllegalArgumentException("chartId must not be null");
}
this.chartId = chartId;
}
public JsonObjectBuilder.JsonObject getRequestJsonObject(
BiConsumer<String, Throwable> errorLogger, boolean logErrors) {
JsonObjectBuilder builder = new JsonObjectBuilder();
builder.appendField("chartId", chartId);
try {
JsonObjectBuilder.JsonObject data = getChartData();
if (data == null) {
// If the data is null we don't send the chart.
return null;
}
builder.appendField("data", data);
} catch (Throwable t) {
if (logErrors) {
errorLogger.accept("Failed to get data for custom chart with id " + chartId, t);
}
return null;
}
return builder.build();
}
protected abstract JsonObjectBuilder.JsonObject getChartData() throws Exception;
}
public static class SingleLineChart extends CustomChart {
private final Callable<Integer> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public SingleLineChart(String chartId, Callable<Integer> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
int value = callable.call();
if (value == 0) {
// Null = skip the chart
return null;
}
return new JsonObjectBuilder().appendField("value", value).build();
}
}
public static class SimplePie extends CustomChart {
private final Callable<String> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public SimplePie(String chartId, Callable<String> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
String value = callable.call();
if (value == null || value.isEmpty()) {
// Null = skip the chart
return null;
}
return new JsonObjectBuilder().appendField("value", value).build();
}
}
public static class DrilldownPie extends CustomChart {
private final Callable<Map<String, Map<String, Integer>>> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public DrilldownPie(String chartId, Callable<Map<String, Map<String, Integer>>> callable) {
super(chartId);
this.callable = callable;
}
@Override
public JsonObjectBuilder.JsonObject getChartData() throws Exception {
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
Map<String, Map<String, Integer>> map = callable.call();
if (map == null || map.isEmpty()) {
// Null = skip the chart
return null;
}
boolean reallyAllSkipped = true;
for (Map.Entry<String, Map<String, Integer>> entryValues : map.entrySet()) {
JsonObjectBuilder valueBuilder = new JsonObjectBuilder();
boolean allSkipped = true;
for (Map.Entry<String, Integer> valueEntry : map.get(entryValues.getKey()).entrySet()) {
valueBuilder.appendField(valueEntry.getKey(), valueEntry.getValue());
allSkipped = false;
}
if (!allSkipped) {
reallyAllSkipped = false;
valuesBuilder.appendField(entryValues.getKey(), valueBuilder.build());
}
}
if (reallyAllSkipped) {
// Null = skip the chart
return null;
}
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
}
}
/**
* An extremely simple JSON builder.
*
* <p>While this class is neither feature-rich nor the most performant one, it's sufficient enough
* for its use-case.
*/
public static class JsonObjectBuilder {
private StringBuilder builder = new StringBuilder();
private boolean hasAtLeastOneField = false;
public JsonObjectBuilder() {
builder.append("{");
}
/**
* Appends a null field to the JSON.
*
* @param key The key of the field.
* @return A reference to this object.
*/
public JsonObjectBuilder appendNull(String key) {
appendFieldUnescaped(key, "null");
return this;
}
/**
* Appends a string field to the JSON.
*
* @param key The key of the field.
* @param value The value of the field.
* @return A reference to this object.
*/
public JsonObjectBuilder appendField(String key, String value) {
if (value == null) {
throw new IllegalArgumentException("JSON value must not be null");
}
appendFieldUnescaped(key, "\"" + escape(value) + "\"");
return this;
}
/**
* Appends an integer field to the JSON.
*
* @param key The key of the field.
* @param value The value of the field.
* @return A reference to this object.
*/
public JsonObjectBuilder appendField(String key, int value) {
appendFieldUnescaped(key, String.valueOf(value));
return this;
}
/**
* Appends an object to the JSON.
*
* @param key The key of the field.
* @param object The object.
* @return A reference to this object.
*/
public JsonObjectBuilder appendField(String key, JsonObject object) {
if (object == null) {
throw new IllegalArgumentException("JSON object must not be null");
}
appendFieldUnescaped(key, object.toString());
return this;
}
/**
* Appends a string array to the JSON.
*
* @param key The key of the field.
* @param values The string array.
* @return A reference to this object.
*/
public JsonObjectBuilder appendField(String key, String[] values) {
if (values == null) {
throw new IllegalArgumentException("JSON values must not be null");
}
String escapedValues =
Arrays.stream(values)
.map(value -> "\"" + escape(value) + "\"")
.collect(Collectors.joining(","));
appendFieldUnescaped(key, "[" + escapedValues + "]");
return this;
}
/**
* Appends an integer array to the JSON.
*
* @param key The key of the field.
* @param values The integer array.
* @return A reference to this object.
*/
public JsonObjectBuilder appendField(String key, int[] values) {
if (values == null) {
throw new IllegalArgumentException("JSON values must not be null");
}
String escapedValues =
Arrays.stream(values).mapToObj(String::valueOf).collect(Collectors.joining(","));
appendFieldUnescaped(key, "[" + escapedValues + "]");
return this;
}
/**
* Appends an object array to the JSON.
*
* @param key The key of the field.
* @param values The integer array.
* @return A reference to this object.
*/
public JsonObjectBuilder appendField(String key, JsonObject[] values) {
if (values == null) {
throw new IllegalArgumentException("JSON values must not be null");
}
String escapedValues =
Arrays.stream(values).map(JsonObject::toString).collect(Collectors.joining(","));
appendFieldUnescaped(key, "[" + escapedValues + "]");
return this;
}
/**
* Appends a field to the object.
*
* @param key The key of the field.
* @param escapedValue The escaped value of the field.
*/
private void appendFieldUnescaped(String key, String escapedValue) {